summaryrefslogtreecommitdiff
path: root/src/silx/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui')
-rw-r--r--src/silx/gui/__init__.py1
-rw-r--r--src/silx/gui/_glutils/Context.py3
-rw-r--r--src/silx/gui/_glutils/FramebufferTexture.py69
-rw-r--r--src/silx/gui/_glutils/OpenGLWidget.py145
-rw-r--r--src/silx/gui/_glutils/Program.py34
-rw-r--r--src/silx/gui/_glutils/Texture.py107
-rw-r--r--src/silx/gui/_glutils/VertexBuffer.py83
-rw-r--r--src/silx/gui/_glutils/__init__.py3
-rw-r--r--src/silx/gui/_glutils/font.py125
-rw-r--r--src/silx/gui/_glutils/gl.py85
-rw-r--r--src/silx/gui/_glutils/test/__init__.py (renamed from src/silx/gui/widgets/setup.py)20
-rw-r--r--src/silx/gui/_glutils/test/test_gl.py (renamed from src/silx/gui/hdf5/setup.py)23
-rw-r--r--src/silx/gui/_glutils/utils.py8
-rwxr-xr-xsrc/silx/gui/colors.py688
-rw-r--r--src/silx/gui/conftest.py42
-rw-r--r--src/silx/gui/console.py44
-rw-r--r--src/silx/gui/constants.py (renamed from src/silx/gui/fit/setup.py)24
-rw-r--r--src/silx/gui/data/ArrayTableModel.py116
-rw-r--r--src/silx/gui/data/ArrayTableWidget.py13
-rw-r--r--src/silx/gui/data/DataViewer.py57
-rw-r--r--src/silx/gui/data/DataViewerFrame.py16
-rw-r--r--src/silx/gui/data/DataViewerSelector.py6
-rw-r--r--src/silx/gui/data/DataViews.py427
-rw-r--r--src/silx/gui/data/Hdf5TableView.py120
-rw-r--r--src/silx/gui/data/HexaTableView.py16
-rw-r--r--src/silx/gui/data/NXdataWidgets.py286
-rw-r--r--src/silx/gui/data/NumpyAxesSelector.py51
-rw-r--r--src/silx/gui/data/RecordTableView.py30
-rw-r--r--src/silx/gui/data/TextFormatter.py45
-rw-r--r--src/silx/gui/data/_RecordPlot.py39
-rw-r--r--src/silx/gui/data/_VolumeWindow.py25
-rw-r--r--src/silx/gui/data/__init__.py1
-rw-r--r--src/silx/gui/data/setup.py41
-rw-r--r--src/silx/gui/data/test/__init__.py1
-rw-r--r--src/silx/gui/data/test/test_arraywidget.py67
-rw-r--r--src/silx/gui/data/test/test_dataviewer.py54
-rw-r--r--src/silx/gui/data/test/test_numpyaxesselector.py3
-rw-r--r--src/silx/gui/data/test/test_textformatter.py47
-rw-r--r--src/silx/gui/dialog/AbstractDataFileDialog.py179
-rw-r--r--src/silx/gui/dialog/ColormapDialog.py631
-rw-r--r--src/silx/gui/dialog/DataFileDialog.py5
-rw-r--r--src/silx/gui/dialog/DatasetDialog.py28
-rw-r--r--src/silx/gui/dialog/FileTypeComboBox.py25
-rw-r--r--src/silx/gui/dialog/GroupDialog.py34
-rw-r--r--src/silx/gui/dialog/ImageFileDialog.py11
-rw-r--r--src/silx/gui/dialog/SafeFileIconProvider.py2
-rw-r--r--src/silx/gui/dialog/SafeFileSystemModel.py26
-rw-r--r--src/silx/gui/dialog/__init__.py1
-rw-r--r--src/silx/gui/dialog/setup.py40
-rw-r--r--src/silx/gui/dialog/test/__init__.py1
-rw-r--r--src/silx/gui/dialog/test/test_colormapdialog.py700
-rw-r--r--src/silx/gui/dialog/test/test_datafiledialog.py216
-rw-r--r--src/silx/gui/dialog/test/test_imagefiledialog.py185
-rw-r--r--src/silx/gui/dialog/utils.py3
-rw-r--r--src/silx/gui/fit/BackgroundWidget.py150
-rw-r--r--src/silx/gui/fit/FitConfig.py186
-rw-r--r--src/silx/gui/fit/FitWidget.py287
-rw-r--r--src/silx/gui/fit/FitWidgets.py102
-rw-r--r--src/silx/gui/fit/Parameters.py476
-rw-r--r--src/silx/gui/fit/__init__.py1
-rw-r--r--src/silx/gui/fit/test/__init__.py1
-rw-r--r--src/silx/gui/fit/test/testBackgroundWidget.py20
-rw-r--r--src/silx/gui/fit/test/testFitConfig.py37
-rw-r--r--src/silx/gui/fit/test/testFitWidget.py19
-rw-r--r--src/silx/gui/hdf5/Hdf5Formatter.py10
-rw-r--r--src/silx/gui/hdf5/Hdf5HeaderView.py61
-rwxr-xr-xsrc/silx/gui/hdf5/Hdf5Item.py176
-rw-r--r--src/silx/gui/hdf5/Hdf5LoadingItem.py12
-rw-r--r--src/silx/gui/hdf5/Hdf5Node.py18
-rw-r--r--src/silx/gui/hdf5/Hdf5TreeModel.py155
-rw-r--r--src/silx/gui/hdf5/Hdf5TreeView.py18
-rw-r--r--src/silx/gui/hdf5/NexusSortFilterProxyModel.py8
-rw-r--r--src/silx/gui/hdf5/__init__.py9
-rw-r--r--src/silx/gui/hdf5/_utils.py16
-rw-r--r--src/silx/gui/hdf5/test/__init__.py1
-rwxr-xr-xsrc/silx/gui/hdf5/test/test_hdf5.py268
-rw-r--r--src/silx/gui/icons.py36
-rw-r--r--src/silx/gui/plot/AlphaSlider.py31
-rw-r--r--src/silx/gui/plot/ColorBar.py217
-rw-r--r--src/silx/gui/plot/Colormap.py42
-rw-r--r--src/silx/gui/plot/ColormapDialog.py43
-rw-r--r--src/silx/gui/plot/Colors.py90
-rw-r--r--src/silx/gui/plot/CompareImages.py1041
-rw-r--r--src/silx/gui/plot/ComplexImageView.py120
-rw-r--r--src/silx/gui/plot/CurvesROIWidget.py487
-rw-r--r--src/silx/gui/plot/ImageStack.py349
-rw-r--r--src/silx/gui/plot/ImageView.py294
-rw-r--r--src/silx/gui/plot/Interaction.py52
-rw-r--r--src/silx/gui/plot/ItemsSelectionDialog.py47
-rwxr-xr-xsrc/silx/gui/plot/LegendSelector.py513
-rw-r--r--src/silx/gui/plot/LimitsHistory.py5
-rw-r--r--src/silx/gui/plot/MaskToolsWidget.py277
-rw-r--r--src/silx/gui/plot/PlotActions.py67
-rw-r--r--src/silx/gui/plot/PlotEvents.py186
-rw-r--r--src/silx/gui/plot/PlotInteraction.py1035
-rw-r--r--src/silx/gui/plot/PlotToolButtons.py164
-rw-r--r--src/silx/gui/plot/PlotTools.py43
-rwxr-xr-xsrc/silx/gui/plot/PlotWidget.py1667
-rw-r--r--src/silx/gui/plot/PlotWindow.py313
-rw-r--r--src/silx/gui/plot/PrintPreviewToolButton.py108
-rw-r--r--src/silx/gui/plot/Profile.py176
-rw-r--r--src/silx/gui/plot/ProfileMainWindow.py110
-rw-r--r--src/silx/gui/plot/ROIStatsWidget.py183
-rw-r--r--src/silx/gui/plot/ScatterMaskToolsWidget.py218
-rw-r--r--src/silx/gui/plot/ScatterView.py78
-rw-r--r--src/silx/gui/plot/StackView.py404
-rw-r--r--src/silx/gui/plot/StatsWidget.py320
-rw-r--r--src/silx/gui/plot/_BaseMaskToolsWidget.py361
-rw-r--r--src/silx/gui/plot/__init__.py13
-rw-r--r--src/silx/gui/plot/_utils/__init__.py28
-rw-r--r--src/silx/gui/plot/_utils/delaunay.py62
-rw-r--r--src/silx/gui/plot/_utils/dtime_ticklayout.py180
-rw-r--r--src/silx/gui/plot/_utils/panzoom.py137
-rw-r--r--src/silx/gui/plot/_utils/test/__init__.py1
-rw-r--r--src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py80
-rw-r--r--src/silx/gui/plot/_utils/test/test_ticklayout.py22
-rw-r--r--src/silx/gui/plot/_utils/ticklayout.py19
-rw-r--r--src/silx/gui/plot/actions/PlotAction.py28
-rw-r--r--src/silx/gui/plot/actions/PlotToolAction.py34
-rw-r--r--src/silx/gui/plot/actions/__init__.py1
-rwxr-xr-xsrc/silx/gui/plot/actions/control.py324
-rw-r--r--src/silx/gui/plot/actions/fit.py97
-rw-r--r--src/silx/gui/plot/actions/histogram.py136
-rw-r--r--src/silx/gui/plot/actions/io.py439
-rw-r--r--src/silx/gui/plot/actions/medfilt.py49
-rw-r--r--src/silx/gui/plot/actions/mode.py83
-rwxr-xr-xsrc/silx/gui/plot/backends/BackendBase.py144
-rwxr-xr-xsrc/silx/gui/plot/backends/BackendMatplotlib.py885
-rwxr-xr-xsrc/silx/gui/plot/backends/BackendOpenGL.py1020
-rw-r--r--src/silx/gui/plot/backends/__init__.py1
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotCurve.py848
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotFrame.py733
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotImage.py383
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotItem.py18
-rw-r--r--src/silx/gui/plot/backends/glutils/GLPlotTriangles.py46
-rw-r--r--src/silx/gui/plot/backends/glutils/GLSupport.py110
-rw-r--r--src/silx/gui/plot/backends/glutils/GLText.py210
-rw-r--r--src/silx/gui/plot/backends/glutils/GLTexture.py210
-rw-r--r--src/silx/gui/plot/backends/glutils/PlotImageFile.py100
-rw-r--r--src/silx/gui/plot/backends/glutils/__init__.py1
-rw-r--r--src/silx/gui/plot/items/__init__.py49
-rw-r--r--src/silx/gui/plot/items/_arc_roi.py262
-rw-r--r--src/silx/gui/plot/items/_band_roi.py376
-rw-r--r--src/silx/gui/plot/items/_pick.py1
-rw-r--r--src/silx/gui/plot/items/_roi_base.py169
-rw-r--r--src/silx/gui/plot/items/axis.py113
-rw-r--r--src/silx/gui/plot/items/complex.py68
-rw-r--r--src/silx/gui/plot/items/core.py455
-rw-r--r--src/silx/gui/plot/items/curve.py210
-rw-r--r--src/silx/gui/plot/items/histogram.py140
-rw-r--r--src/silx/gui/plot/items/image.py166
-rw-r--r--src/silx/gui/plot/items/image_aggregated.py31
-rwxr-xr-xsrc/silx/gui/plot/items/marker.py96
-rw-r--r--src/silx/gui/plot/items/roi.py324
-rw-r--r--src/silx/gui/plot/items/scatter.py472
-rw-r--r--src/silx/gui/plot/items/shape.py256
-rw-r--r--src/silx/gui/plot/matplotlib/Colormap.py249
-rw-r--r--src/silx/gui/plot/setup.py54
-rw-r--r--src/silx/gui/plot/stats/__init__.py1
-rw-r--r--src/silx/gui/plot/stats/stats.py243
-rw-r--r--src/silx/gui/plot/stats/statshandler.py66
-rw-r--r--src/silx/gui/plot/test/__init__.py1
-rw-r--r--src/silx/gui/plot/test/conftest.py (renamed from src/silx/gui/plot/_utils/setup.py)29
-rw-r--r--src/silx/gui/plot/test/testAlphaSlider.py41
-rw-r--r--src/silx/gui/plot/test/testAxis.py147
-rw-r--r--src/silx/gui/plot/test/testColorBar.py147
-rw-r--r--src/silx/gui/plot/test/testCompareImages.py272
-rw-r--r--src/silx/gui/plot/test/testComplexImageView.py4
-rw-r--r--src/silx/gui/plot/test/testCurvesROIWidget.py268
-rw-r--r--src/silx/gui/plot/test/testImageStack.py114
-rw-r--r--src/silx/gui/plot/test/testImageView.py57
-rw-r--r--src/silx/gui/plot/test/testInteraction.py33
-rw-r--r--src/silx/gui/plot/test/testItem.py383
-rw-r--r--src/silx/gui/plot/test/testLegendSelector.py51
-rw-r--r--src/silx/gui/plot/test/testLimitConstraints.py1
-rw-r--r--src/silx/gui/plot/test/testMaskToolsWidget.py123
-rw-r--r--src/silx/gui/plot/test/testPixelIntensityHistoAction.py28
-rw-r--r--src/silx/gui/plot/test/testPlotActions.py42
-rw-r--r--src/silx/gui/plot/test/testPlotInteraction.py164
-rwxr-xr-xsrc/silx/gui/plot/test/testPlotWidget.py1600
-rwxr-xr-xsrc/silx/gui/plot/test/testPlotWidgetActiveItem.py416
-rw-r--r--src/silx/gui/plot/test/testPlotWidgetDataMargins.py135
-rw-r--r--src/silx/gui/plot/test/testPlotWidgetNoBackend.py529
-rw-r--r--src/silx/gui/plot/test/testPlotWindow.py39
-rw-r--r--src/silx/gui/plot/test/testRoiStatsWidget.py179
-rw-r--r--src/silx/gui/plot/test/testSaveAction.py61
-rw-r--r--src/silx/gui/plot/test/testScatterMaskToolsWidget.py125
-rw-r--r--src/silx/gui/plot/test/testScatterView.py21
-rw-r--r--src/silx/gui/plot/test/testStackView.py160
-rw-r--r--src/silx/gui/plot/test/testStats.py702
-rw-r--r--src/silx/gui/plot/test/testUtilsAxis.py78
-rw-r--r--src/silx/gui/plot/test/utils.py3
-rw-r--r--src/silx/gui/plot/tools/CurveLegendsWidget.py24
-rw-r--r--src/silx/gui/plot/tools/LimitsToolBar.py26
-rw-r--r--src/silx/gui/plot/tools/PlotToolButton.py92
-rw-r--r--src/silx/gui/plot/tools/PositionInfo.py132
-rw-r--r--src/silx/gui/plot/tools/RadarView.py54
-rw-r--r--src/silx/gui/plot/tools/RulerToolButton.py183
-rw-r--r--src/silx/gui/plot/tools/__init__.py1
-rw-r--r--src/silx/gui/plot/tools/compare/__init__.py (renamed from src/silx/gui/plot/matplotlib/__init__.py)18
-rw-r--r--src/silx/gui/plot/tools/compare/core.py198
-rw-r--r--src/silx/gui/plot/tools/compare/profile.py173
-rw-r--r--src/silx/gui/plot/tools/compare/statusbar.py218
-rw-r--r--src/silx/gui/plot/tools/compare/toolbar.py390
-rw-r--r--src/silx/gui/plot/tools/menus.py93
-rw-r--r--src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py14
-rw-r--r--src/silx/gui/plot/tools/profile/__init__.py1
-rw-r--r--src/silx/gui/plot/tools/profile/core.py314
-rw-r--r--src/silx/gui/plot/tools/profile/editors.py34
-rw-r--r--src/silx/gui/plot/tools/profile/manager.py178
-rw-r--r--src/silx/gui/plot/tools/profile/rois.py262
-rw-r--r--src/silx/gui/plot/tools/profile/toolbar.py4
-rw-r--r--src/silx/gui/plot/tools/roi.py382
-rw-r--r--src/silx/gui/plot/tools/test/__init__.py1
-rw-r--r--src/silx/gui/plot/tools/test/testCurveLegendsWidget.py32
-rw-r--r--src/silx/gui/plot/tools/test/testProfile.py156
-rw-r--r--src/silx/gui/plot/tools/test/testRoiCore.py (renamed from src/silx/gui/plot/tools/test/testROI.py)381
-rw-r--r--src/silx/gui/plot/tools/test/testRoiItems.py313
-rw-r--r--src/silx/gui/plot/tools/test/testScatterProfileToolBar.py19
-rw-r--r--src/silx/gui/plot/tools/test/testTools.py38
-rw-r--r--src/silx/gui/plot/tools/toolbars.py81
-rw-r--r--src/silx/gui/plot/utils/__init__.py1
-rw-r--r--src/silx/gui/plot/utils/axis.py32
-rw-r--r--src/silx/gui/plot/utils/intersections.py27
-rw-r--r--src/silx/gui/plot3d/ParamTreeView.py371
-rw-r--r--src/silx/gui/plot3d/Plot3DWidget.py161
-rw-r--r--src/silx/gui/plot3d/Plot3DWindow.py3
-rw-r--r--src/silx/gui/plot3d/SFViewParamTree.py371
-rw-r--r--src/silx/gui/plot3d/ScalarFieldView.py356
-rw-r--r--src/silx/gui/plot3d/SceneWidget.py105
-rw-r--r--src/silx/gui/plot3d/SceneWindow.py56
-rw-r--r--src/silx/gui/plot3d/__init__.py4
-rw-r--r--src/silx/gui/plot3d/_model/__init__.py3
-rw-r--r--src/silx/gui/plot3d/_model/core.py31
-rw-r--r--src/silx/gui/plot3d/_model/items.py571
-rw-r--r--src/silx/gui/plot3d/_model/model.py5
-rw-r--r--src/silx/gui/plot3d/actions/Plot3DAction.py3
-rw-r--r--src/silx/gui/plot3d/actions/__init__.py1
-rw-r--r--src/silx/gui/plot3d/actions/io.py111
-rw-r--r--src/silx/gui/plot3d/actions/mode.py38
-rw-r--r--src/silx/gui/plot3d/actions/viewpoint.py90
-rw-r--r--src/silx/gui/plot3d/conftest.py1
-rw-r--r--src/silx/gui/plot3d/items/__init__.py12
-rw-r--r--src/silx/gui/plot3d/items/_pick.py53
-rw-r--r--src/silx/gui/plot3d/items/clipplane.py31
-rw-r--r--src/silx/gui/plot3d/items/core.py127
-rw-r--r--src/silx/gui/plot3d/items/image.py95
-rw-r--r--src/silx/gui/plot3d/items/mesh.py370
-rw-r--r--src/silx/gui/plot3d/items/mixins.py81
-rw-r--r--src/silx/gui/plot3d/items/scatter.py227
-rw-r--r--src/silx/gui/plot3d/items/volume.py138
-rw-r--r--src/silx/gui/plot3d/scene/__init__.py1
-rw-r--r--src/silx/gui/plot3d/scene/axes.py73
-rw-r--r--src/silx/gui/plot3d/scene/camera.py120
-rw-r--r--src/silx/gui/plot3d/scene/core.py39
-rw-r--r--src/silx/gui/plot3d/scene/cutplane.py140
-rw-r--r--src/silx/gui/plot3d/scene/event.py44
-rw-r--r--src/silx/gui/plot3d/scene/function.py155
-rw-r--r--src/silx/gui/plot3d/scene/interaction.py343
-rw-r--r--src/silx/gui/plot3d/scene/primitives.py994
-rw-r--r--src/silx/gui/plot3d/scene/test/__init__.py1
-rw-r--r--src/silx/gui/plot3d/scene/test/test_transform.py42
-rw-r--r--src/silx/gui/plot3d/scene/test/test_utils.py171
-rw-r--r--src/silx/gui/plot3d/scene/text.py244
-rw-r--r--src/silx/gui/plot3d/scene/transform.py307
-rw-r--r--src/silx/gui/plot3d/scene/utils.py115
-rw-r--r--src/silx/gui/plot3d/scene/viewport.py150
-rw-r--r--src/silx/gui/plot3d/scene/window.py110
-rw-r--r--src/silx/gui/plot3d/setup.py50
-rw-r--r--src/silx/gui/plot3d/test/__init__.py1
-rw-r--r--src/silx/gui/plot3d/test/testGL.py22
-rw-r--r--src/silx/gui/plot3d/test/testScalarFieldView.py18
-rw-r--r--src/silx/gui/plot3d/test/testSceneWidget.py6
-rw-r--r--src/silx/gui/plot3d/test/testSceneWidgetPicking.py106
-rw-r--r--src/silx/gui/plot3d/test/testSceneWindow.py73
-rw-r--r--src/silx/gui/plot3d/test/testStatsWidget.py48
-rw-r--r--src/silx/gui/plot3d/tools/GroupPropertiesWidget.py28
-rw-r--r--src/silx/gui/plot3d/tools/PositionInfoWidget.py80
-rw-r--r--src/silx/gui/plot3d/tools/ViewpointTools.py7
-rw-r--r--src/silx/gui/plot3d/tools/__init__.py1
-rw-r--r--src/silx/gui/plot3d/tools/test/__init__.py1
-rw-r--r--src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py8
-rw-r--r--src/silx/gui/plot3d/tools/toolbars.py11
-rw-r--r--src/silx/gui/plot3d/utils/__init__.py1
-rw-r--r--src/silx/gui/plot3d/utils/mng.py58
-rw-r--r--src/silx/gui/printer.py3
-rw-r--r--src/silx/gui/qt/__init__.py14
-rw-r--r--src/silx/gui/qt/_pyqt6.py79
-rw-r--r--src/silx/gui/qt/_pyside_dynamic.py291
-rw-r--r--src/silx/gui/qt/_qt.py224
-rw-r--r--src/silx/gui/qt/_utils.py27
-rw-r--r--src/silx/gui/qt/inspect.py31
-rw-r--r--src/silx/gui/setup.py55
-rw-r--r--src/silx/gui/test/__init__.py1
-rwxr-xr-xsrc/silx/gui/test/test_colors.py473
-rw-r--r--src/silx/gui/test/test_console.py9
-rw-r--r--src/silx/gui/test/test_icons.py13
-rw-r--r--src/silx/gui/test/test_qt.py19
-rw-r--r--src/silx/gui/test/utils.py43
-rwxr-xr-xsrc/silx/gui/utils/__init__.py8
-rw-r--r--src/silx/gui/utils/concurrent.py3
-rw-r--r--src/silx/gui/utils/glutils/__init__.py171
-rw-r--r--src/silx/gui/utils/image.py82
-rw-r--r--src/silx/gui/utils/matplotlib.py156
-rw-r--r--src/silx/gui/utils/projecturl.py6
-rwxr-xr-xsrc/silx/gui/utils/qtutils.py1
-rw-r--r--src/silx/gui/utils/signal.py14
-rwxr-xr-xsrc/silx/gui/utils/test/__init__.py1
-rw-r--r--src/silx/gui/utils/test/test.py4
-rw-r--r--src/silx/gui/utils/test/test_async.py12
-rw-r--r--src/silx/gui/utils/test/test_glutils.py35
-rw-r--r--src/silx/gui/utils/test/test_image.py84
-rwxr-xr-xsrc/silx/gui/utils/test/test_qtutils.py4
-rw-r--r--src/silx/gui/utils/test/test_testutils.py6
-rw-r--r--src/silx/gui/utils/testutils.py104
-rw-r--r--src/silx/gui/widgets/BoxLayoutDockWidget.py1
-rw-r--r--src/silx/gui/widgets/ColormapNameComboBox.py3
-rw-r--r--src/silx/gui/widgets/ElidedLabel.py44
-rw-r--r--src/silx/gui/widgets/FloatEdit.py103
-rw-r--r--src/silx/gui/widgets/FlowLayout.py11
-rw-r--r--src/silx/gui/widgets/FormGridLayout.py79
-rw-r--r--src/silx/gui/widgets/FrameBrowser.py26
-rw-r--r--src/silx/gui/widgets/HierarchicalTableView.py1
-rwxr-xr-xsrc/silx/gui/widgets/LegendIconWidget.py147
-rw-r--r--src/silx/gui/widgets/MedianFilterDialog.py17
-rw-r--r--src/silx/gui/widgets/MultiModeAction.py1
-rw-r--r--src/silx/gui/widgets/PeriodicTable.py298
-rw-r--r--src/silx/gui/widgets/PrintGeometryDialog.py44
-rw-r--r--src/silx/gui/widgets/PrintPreview.py174
-rw-r--r--src/silx/gui/widgets/RangeSlider.py152
-rw-r--r--src/silx/gui/widgets/StackedProgressBar.py314
-rw-r--r--src/silx/gui/widgets/TableWidget.py69
-rw-r--r--src/silx/gui/widgets/ThreadPoolPushButton.py5
-rw-r--r--src/silx/gui/widgets/UrlList.py139
-rw-r--r--src/silx/gui/widgets/UrlSelectionTable.py312
-rw-r--r--src/silx/gui/widgets/WaitingOverlay.py111
-rw-r--r--src/silx/gui/widgets/WaitingPushButton.py11
-rw-r--r--src/silx/gui/widgets/__init__.py1
-rw-r--r--src/silx/gui/widgets/test/__init__.py1
-rw-r--r--src/silx/gui/widgets/test/test_boxlayoutdockwidget.py7
-rw-r--r--src/silx/gui/widgets/test/test_elidedlabel.py33
-rw-r--r--src/silx/gui/widgets/test/test_floatedit.py82
-rw-r--r--src/silx/gui/widgets/test/test_flowlayout.py7
-rw-r--r--src/silx/gui/widgets/test/test_framebrowser.py3
-rw-r--r--src/silx/gui/widgets/test/test_hierarchicaltableview.py4
-rw-r--r--src/silx/gui/widgets/test/test_legendiconwidget.py3
-rw-r--r--src/silx/gui/widgets/test/test_periodictable.py21
-rw-r--r--src/silx/gui/widgets/test/test_printpreview.py12
-rw-r--r--src/silx/gui/widgets/test/test_rangeslider.py29
-rw-r--r--src/silx/gui/widgets/test/test_stackedprogressbar.py60
-rw-r--r--src/silx/gui/widgets/test/test_tablewidget.py2
-rw-r--r--src/silx/gui/widgets/test/test_threadpoolpushbutton.py5
-rw-r--r--src/silx/gui/widgets/test/test_urlselectiontable.py72
-rw-r--r--src/silx/gui/widgets/test/test_waitingoverlay.py31
354 files changed, 27440 insertions, 20179 deletions
diff --git a/src/silx/gui/__init__.py b/src/silx/gui/__init__.py
index b796e20..31bb38e 100644
--- a/src/silx/gui/__init__.py
+++ b/src/silx/gui/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/_glutils/Context.py b/src/silx/gui/_glutils/Context.py
index c62dbb9..c0def5c 100644
--- a/src/silx/gui/_glutils/Context.py
+++ b/src/silx/gui/_glutils/Context.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
@@ -37,8 +36,10 @@ import contextlib
class _DEFAULT_CONTEXT(object):
"""The default value for OpenGL context"""
+
pass
+
_context = _DEFAULT_CONTEXT
"""The current OpenGL context"""
diff --git a/src/silx/gui/_glutils/FramebufferTexture.py b/src/silx/gui/_glutils/FramebufferTexture.py
index d12a6e0..6d1a8d9 100644
--- a/src/silx/gui/_glutils/FramebufferTexture.py
+++ b/src/silx/gui/_glutils/FramebufferTexture.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
@@ -54,13 +53,14 @@ class FramebufferTexture(object):
_PACKED_FORMAT = gl.GL_DEPTH24_STENCIL8, gl.GL_DEPTH_STENCIL
- def __init__(self,
- internalFormat,
- shape,
- stencilFormat=gl.GL_DEPTH24_STENCIL8,
- depthFormat=gl.GL_DEPTH24_STENCIL8,
- **kwargs):
-
+ def __init__(
+ self,
+ internalFormat,
+ shape,
+ stencilFormat=gl.GL_DEPTH24_STENCIL8,
+ depthFormat=gl.GL_DEPTH24_STENCIL8,
+ **kwargs,
+ ):
self._texture = Texture(internalFormat, shape=shape, **kwargs)
self._texture.prepare()
@@ -70,24 +70,28 @@ class FramebufferTexture(object):
with self: # Bind FBO
# Attachments
- gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER,
- gl.GL_COLOR_ATTACHMENT0,
- gl.GL_TEXTURE_2D,
- self._texture.name,
- 0)
+ 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)
+ 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
@@ -97,13 +101,15 @@ class FramebufferTexture(object):
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)
+ 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
@@ -111,7 +117,8 @@ class FramebufferTexture(object):
if status != gl.GL_FRAMEBUFFER_COMPLETE:
_logger.error(
"OpenGL framebuffer initialization not complete, display may fail (error %d)",
- status)
+ status,
+ )
@property
def shape(self):
@@ -131,8 +138,10 @@ class FramebufferTexture(object):
if self._name is not None:
return self._name
else:
- raise RuntimeError("No OpenGL framebuffer resource, \
- discard has already been called")
+ raise RuntimeError(
+ "No OpenGL framebuffer resource, \
+ discard has already been called"
+ )
def bind(self):
"""Bind this framebuffer for rendering"""
diff --git a/src/silx/gui/_glutils/OpenGLWidget.py b/src/silx/gui/_glutils/OpenGLWidget.py
index 2ca4649..59fa4f0 100644
--- a/src/silx/gui/_glutils/OpenGLWidget.py
+++ b/src/silx/gui/_glutils/OpenGLWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -44,16 +43,16 @@ from .._glutils import gl
_logger = logging.getLogger(__name__)
-if not hasattr(qt, 'QOpenGLWidget') and not hasattr(qt, 'QGLWidget'):
- OpenGLWidget = None
+if not hasattr(qt, "QOpenGLWidget") and not hasattr(qt, "QGLWidget"):
+ _OpenGLWidget = None
else:
- if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4
- _logger.info('Using QOpenGLWidget')
+ if hasattr(qt, "QOpenGLWidget"): # PyQt>=5.4
+ _logger.info("Using QOpenGLWidget")
_BaseOpenGLWidget = qt.QOpenGLWidget
else:
- _logger.info('Using QGLWidget')
+ _logger.info("Using QGLWidget")
_BaseOpenGLWidget = qt.QGLWidget
class _OpenGLWidget(_BaseOpenGLWidget):
@@ -65,14 +64,17 @@ else:
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()):
+ def __init__(
+ self,
+ parent,
+ alphaBufferSize=0,
+ depthBufferSize=24,
+ stencilBufferSize=8,
+ version=(2, 0),
+ f=qt.Qt.Widget,
+ ):
# True if using QGLWidget, False if using QOpenGLWidget
- self.__legacy = not hasattr(qt, 'QOpenGLWidget')
+ self.__legacy = not hasattr(qt, "QOpenGLWidget")
self.__devicePixelRatio = 1.0
self.__requestedOpenGLVersion = int(version[0]), int(version[1])
@@ -132,12 +134,23 @@ else:
# 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)
+ 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
@@ -172,13 +185,13 @@ else:
def initializeGL(self):
parent = self.parent()
if parent is None:
- _logger.error('_OpenGLWidget has no parent')
+ _logger.error("_OpenGLWidget has no parent")
return
# Check OpenGL version
if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion():
try:
- gl.glGetError() # clear any previous error (if any)
+ gl.glGetError() # clear any previous error (if any)
version = gl.glGetString(gl.GL_VERSION)
except:
version = None
@@ -186,18 +199,19 @@ else:
if version:
self.__isValid = True
else:
- errMsg = 'OpenGL not available'
- if sys.platform.startswith('linux'):
- errMsg += ': If connected remotely, ' \
- 'GLX forwarding might be disabled.'
+ 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)
+ errMsg = "OpenGL %d.%d not available" % self.getRequestedOpenGLVersion()
+ _logger.error("OpenGL widget disabled: %s", errMsg)
self.sigOpenGLContextError.emit(errMsg)
self.__isValid = False
@@ -207,7 +221,7 @@ else:
def paintGL(self):
parent = self.parent()
if parent is None:
- _logger.error('_OpenGLWidget has no parent')
+ _logger.error("_OpenGLWidget has no parent")
return
devicePixelRatio = self.window().windowHandle().devicePixelRatio()
@@ -225,7 +239,7 @@ else:
def resizeGL(self, width, height):
parent = self.parent()
if parent is None:
- _logger.error('_OpenGLWidget has no parent')
+ _logger.error("_OpenGLWidget has no parent")
return
if self.isValid():
@@ -257,12 +271,15 @@ class OpenGLWidget(qt.QWidget):
:param f: see :class:`QWidget`
"""
- def __init__(self, parent=None,
- alphaBufferSize=0,
- depthBufferSize=24,
- stencilBufferSize=8,
- version=(2, 0),
- f=qt.Qt.WindowFlags()):
+ def __init__(
+ self,
+ parent=None,
+ alphaBufferSize=0,
+ depthBufferSize=24,
+ stencilBufferSize=8,
+ version=(2, 0),
+ f=qt.Qt.Widget,
+ ):
super(OpenGLWidget, self).__init__(parent, f)
layout = qt.QHBoxLayout(self)
@@ -273,24 +290,26 @@ class OpenGLWidget(qt.QWidget):
_check = isOpenGLAvailable(version=version, runtimeCheck=False)
if _OpenGLWidget is None or not _check:
- _logger.error('OpenGL-based widget disabled: %s', _check.error)
+ _logger.error("OpenGL-based widget disabled: %s", _check.error)
self.__openGLWidget = None
label = self._createErrorQLabel(_check.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)
+ return
+
+ 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):
@@ -298,7 +317,7 @@ class OpenGLWidget(qt.QWidget):
:param str error: The error message to display"""
label = qt.QLabel()
- label.setText('OpenGL-based widget disabled:\n%s' % error)
+ label.setText("OpenGL-based widget disabled:\n%s" % error)
label.setAlignment(qt.Qt.AlignCenter)
label.setWordWrap(True)
return label
@@ -324,7 +343,7 @@ class OpenGLWidget(qt.QWidget):
:rtype: float
"""
if self.__openGLWidget is None:
- return 1.
+ return 1.0
else:
return self.__openGLWidget.getDevicePixelRatio()
@@ -334,13 +353,17 @@ class OpenGLWidget(qt.QWidget):
:rtype: float
"""
screen = self.window().windowHandle().screen()
- if screen is not None:
- # TODO check if this is correct on different OS/screen
- # OK on macOS10.12/qt5.13.2
- dpi = screen.physicalDotsPerInch() * self.getDevicePixelRatio()
- else: # Fallback
- dpi = 96. * self.getDevicePixelRatio()
- return dpi
+ if screen is None:
+ return 96.0 * self.getDevicePixelRatio()
+
+ physicalDPI = screen.physicalDotsPerInch()
+ if physicalDPI > 1000.0:
+ _logger.error(
+ "Reported screen DPI too high: %f, using default value instead",
+ physicalDPI,
+ )
+ physicalDPI = 96.0
+ return physicalDPI * self.getDevicePixelRatio()
def getOpenGLVersion(self):
"""Returns the available OpenGL version.
diff --git a/src/silx/gui/_glutils/Program.py b/src/silx/gui/_glutils/Program.py
index 87eec5f..b2adacf 100644
--- a/src/silx/gui/_glutils/Program.py
+++ b/src/silx/gui/_glutils/Program.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
@@ -56,8 +55,7 @@ class Program(object):
array attached to it in order for the rendering to occur....
"""
- def __init__(self, vertexShader, fragmentShader,
- attrib0='position'):
+ def __init__(self, vertexShader, fragmentShader, attrib0="position"):
self._vertexShader = vertexShader
self._fragmentShader = fragmentShader
self._attrib0 = attrib0
@@ -67,7 +65,7 @@ class Program(object):
def _compileGL(vertexShader, fragmentShader, attrib0):
program = gl.glCreateProgram()
- gl.glBindAttribLocation(program, 0, attrib0.encode('ascii'))
+ gl.glBindAttribLocation(program, 0, attrib0.encode("ascii"))
vertex = gl.glCreateShader(gl.GL_VERTEX_SHADER)
gl.glShaderSource(vertex, vertexShader)
@@ -80,8 +78,7 @@ class Program(object):
fragment = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
gl.glShaderSource(fragment, fragmentShader)
gl.glCompileShader(fragment)
- if gl.glGetShaderiv(fragment,
- gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
+ if gl.glGetShaderiv(fragment, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
raise RuntimeError(gl.glGetShaderInfoLog(fragment))
gl.glAttachShader(program, fragment)
gl.glDeleteShader(fragment)
@@ -91,16 +88,15 @@ class Program(object):
raise RuntimeError(gl.glGetProgramInfoLog(program))
attributes = {}
- for index in range(gl.glGetProgramiv(program,
- gl.GL_ACTIVE_ATTRIBUTES)):
+ for index in range(gl.glGetProgramiv(program, gl.GL_ACTIVE_ATTRIBUTES)):
name = gl.glGetActiveAttrib(program, index)[0]
- namestr = name.decode('ascii')
+ namestr = name.decode("ascii")
attributes[namestr] = gl.glGetAttribLocation(program, name)
uniforms = {}
for index in range(gl.glGetProgramiv(program, gl.GL_ACTIVE_UNIFORMS)):
name = gl.glGetActiveUniform(program, index)[0]
- namestr = name.decode('ascii')
+ namestr = name.decode("ascii")
uniforms[namestr] = gl.glGetUniformLocation(program, name)
return program, attributes, uniforms
@@ -108,8 +104,7 @@ class Program(object):
def _getProgramInfo(self):
glcontext = Context.getCurrent()
if glcontext not in self._programs:
- raise RuntimeError(
- "Program was not compiled for current OpenGL context.")
+ raise RuntimeError("Program was not compiled for current OpenGL context.")
return self._programs[glcontext]
@property
@@ -153,16 +148,15 @@ class Program(object):
if glcontext not in self._programs:
self._programs[glcontext] = self._compileGL(
- self._vertexShader,
- self._fragmentShader,
- self._attrib0)
+ self._vertexShader, self._fragmentShader, self._attrib0
+ )
if _logger.getEffectiveLevel() <= logging.DEBUG:
gl.glValidateProgram(self.program)
- if gl.glGetProgramiv(
- self.program, gl.GL_VALIDATE_STATUS) != gl.GL_TRUE:
- _logger.debug('Cannot validate program: %s',
- gl.glGetProgramInfoLog(self.program))
+ if gl.glGetProgramiv(self.program, gl.GL_VALIDATE_STATUS) != gl.GL_TRUE:
+ _logger.debug(
+ "Cannot validate program: %s", gl.glGetProgramInfoLog(self.program)
+ )
gl.glUseProgram(self.program)
@@ -199,4 +193,4 @@ class Program(object):
gl.glUniformMatrix4fv(location, count, transpose, value)
elif not safe:
- raise KeyError('No uniform: %s' % name)
+ raise KeyError("No uniform: %s" % name)
diff --git a/src/silx/gui/_glutils/Texture.py b/src/silx/gui/_glutils/Texture.py
index c72135a..aac380d 100644
--- a/src/silx/gui/_glutils/Texture.py
+++ b/src/silx/gui/_glutils/Texture.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,11 +28,7 @@ __license__ = "MIT"
__date__ = "04/10/2016"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-
+from collections import abc
from ctypes import c_void_p
import logging
@@ -63,10 +58,17 @@ class Texture(object):
:type wrap: OpenGL wrap mode or 2 or 3-tuple of wrap mode
"""
- def __init__(self, internalFormat, data=None, format_=None,
- shape=None, texUnit=0,
- minFilter=None, magFilter=None, wrap=None):
-
+ def __init__(
+ self,
+ internalFormat,
+ data=None,
+ format_=None,
+ shape=None,
+ texUnit=0,
+ minFilter=None,
+ magFilter=None,
+ wrap=None,
+ ):
self._internalFormat = internalFormat
if format_ is None:
format_ = self.internalFormat
@@ -75,7 +77,7 @@ class Texture(object):
assert shape is not None
else:
assert shape is None
- data = numpy.array(data, copy=False, order='C')
+ data = numpy.array(data, copy=False, order="C")
if format_ != gl.GL_RED:
shape = data.shape[:-1] # Last dimension is channels
else:
@@ -165,9 +167,7 @@ class Texture(object):
:rtype: bool
"""
- return (self._name is None or
- self._texParameterUpdates or
- self._deferredUpdates)
+ return self._name is None or self._texParameterUpdates or self._deferredUpdates
def _prepareAndBind(self, texUnit=None):
"""Synchronizes the OpenGL texture"""
@@ -201,10 +201,14 @@ class Texture(object):
if offset is None: # Initialize texture
if self.ndim == 2:
_logger.debug(
- 'Creating 2D texture shape: (%d, %d),'
- ' internal format: %s, format: %s, type: %s',
- self.shape[0], self.shape[1],
- str(self.internalFormat), str(format_), str(type_))
+ "Creating 2D texture shape: (%d, %d),"
+ " internal format: %s, format: %s, type: %s",
+ self.shape[0],
+ self.shape[1],
+ str(self.internalFormat),
+ str(format_),
+ str(type_),
+ )
gl.glTexImage2D(
gl.GL_TEXTURE_2D,
@@ -215,14 +219,20 @@ class Texture(object):
0,
format_,
type_,
- data)
+ data,
+ )
else:
_logger.debug(
- 'Creating 3D texture shape: (%d, %d, %d),'
- ' internal format: %s, format: %s, type: %s',
- self.shape[0], self.shape[1], self.shape[2],
- str(self.internalFormat), str(format_), str(type_))
+ "Creating 3D texture shape: (%d, %d, %d),"
+ " internal format: %s, format: %s, type: %s",
+ self.shape[0],
+ self.shape[1],
+ self.shape[2],
+ str(self.internalFormat),
+ str(format_),
+ str(type_),
+ )
gl.glTexImage3D(
gl.GL_TEXTURE_3D,
@@ -234,32 +244,37 @@ class Texture(object):
0,
format_,
type_,
- data)
+ data,
+ )
else: # Update already existing texture
if self.ndim == 2:
- gl.glTexSubImage2D(gl.GL_TEXTURE_2D,
- 0,
- offset[1],
- offset[0],
- data.shape[1],
- data.shape[0],
- format_,
- type_,
- data)
+ gl.glTexSubImage2D(
+ gl.GL_TEXTURE_2D,
+ 0,
+ offset[1],
+ offset[0],
+ data.shape[1],
+ data.shape[0],
+ format_,
+ type_,
+ data,
+ )
else:
- gl.glTexSubImage3D(gl.GL_TEXTURE_3D,
- 0,
- offset[2],
- offset[1],
- offset[0],
- data.shape[2],
- data.shape[1],
- data.shape[0],
- format_,
- type_,
- data)
+ gl.glTexSubImage3D(
+ gl.GL_TEXTURE_3D,
+ 0,
+ offset[2],
+ offset[1],
+ offset[0],
+ data.shape[2],
+ data.shape[1],
+ data.shape[0],
+ format_,
+ type_,
+ data,
+ )
self._deferredUpdates = []
@@ -341,7 +356,7 @@ class Texture(object):
:param bool copy:
True (default) to copy data, False to use as is (do not modify)
"""
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
offset = tuple(offset)
assert data.ndim == self.ndim
diff --git a/src/silx/gui/_glutils/VertexBuffer.py b/src/silx/gui/_glutils/VertexBuffer.py
index b74b748..d71bbeb 100644
--- a/src/silx/gui/_glutils/VertexBuffer.py
+++ b/src/silx/gui/_glutils/VertexBuffer.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
@@ -51,15 +50,12 @@ class VertexBuffer(object):
:param target: Target buffer:
GL_ARRAY_BUFFER (default) or GL_ELEMENT_ARRAY_BUFFER
"""
+
# OpenGL|ES 2.0 subset:
_USAGES = gl.GL_STREAM_DRAW, gl.GL_STATIC_DRAW, gl.GL_DYNAMIC_DRAW
_TARGETS = gl.GL_ARRAY_BUFFER, gl.GL_ELEMENT_ARRAY_BUFFER
- def __init__(self,
- data=None,
- size=None,
- usage=None,
- target=None):
+ def __init__(self, data=None, size=None, usage=None, target=None):
if usage is None:
usage = gl.GL_STATIC_DRAW
assert usage in self._USAGES
@@ -77,20 +73,14 @@ class VertexBuffer(object):
if data is None:
assert size is not None
self._size = size
- gl.glBufferData(self._target,
- self._size,
- c_void_p(0),
- self._usage)
+ gl.glBufferData(self._target, self._size, c_void_p(0), self._usage)
else:
- data = numpy.array(data, copy=False, order='C')
+ data = numpy.array(data, copy=False, order="C")
if size is not None:
assert size <= data.nbytes
self._size = size or data.nbytes
- gl.glBufferData(self._target,
- self._size,
- data,
- self._usage)
+ gl.glBufferData(self._target, self._size, data, self._usage)
gl.glBindBuffer(self._target, 0)
@@ -110,8 +100,10 @@ class VertexBuffer(object):
if self._name is not None:
return self._name
else:
- raise RuntimeError("No OpenGL buffer resource, \
- discard has already been called")
+ raise RuntimeError(
+ "No OpenGL buffer resource, \
+ discard has already been called"
+ )
@property
def size(self):
@@ -119,8 +111,10 @@ class VertexBuffer(object):
if self._size is not None:
return self._size
else:
- raise RuntimeError("No OpenGL buffer resource, \
- discard has already been called")
+ raise RuntimeError(
+ "No OpenGL buffer resource, \
+ discard has already been called"
+ )
def bind(self):
"""Bind the vertex buffer"""
@@ -133,7 +127,7 @@ class VertexBuffer(object):
:param int offset: Offset in bytes in the buffer where to put the data
:param int size: If provided, size of data to copy
"""
- data = numpy.array(data, copy=False, order='C')
+ data = numpy.array(data, copy=False, order="C")
if size is None:
size = data.nbytes
assert offset + size <= self.size
@@ -173,14 +167,9 @@ class VertexBufferAttrib(object):
_GL_TYPES = gl.GL_UNSIGNED_BYTE, gl.GL_FLOAT, gl.GL_INT
- def __init__(self,
- vbo,
- type_,
- size,
- dimension=1,
- offset=0,
- stride=0,
- normalization=False):
+ def __init__(
+ self, vbo, type_, size, dimension=1, offset=0, stride=0, normalization=False
+ ):
self.vbo = vbo
assert type_ in self._GL_TYPES
self.type_ = type_
@@ -202,21 +191,25 @@ class VertexBufferAttrib(object):
"""Call glVertexAttribPointer with objects information"""
normalization = gl.GL_TRUE if self.normalization else gl.GL_FALSE
with self.vbo:
- gl.glVertexAttribPointer(attribute,
- self.dimension,
- self.type_,
- normalization,
- self.stride,
- c_void_p(self.offset))
+ gl.glVertexAttribPointer(
+ attribute,
+ self.dimension,
+ self.type_,
+ normalization,
+ self.stride,
+ c_void_p(self.offset),
+ )
def copy(self):
- return VertexBufferAttrib(self.vbo,
- self.type_,
- self.size,
- self.dimension,
- self.offset,
- self.stride,
- self.normalization)
+ return VertexBufferAttrib(
+ self.vbo,
+ self.type_,
+ self.size,
+ self.dimension,
+ self.offset,
+ self.stride,
+ self.normalization,
+ )
def vertexBuffer(arrays, prefix=None, suffix=None, usage=None):
@@ -242,7 +235,7 @@ def vertexBuffer(arrays, prefix=None, suffix=None, usage=None):
suffix = (0,) * len(arrays)
for data, pre, post in zip(arrays, prefix, suffix):
- data = numpy.array(data, copy=False, order='C')
+ data = numpy.array(data, copy=False, order="C")
shape = data.shape
assert len(shape) <= 2
type_ = numpyToGLType(data.dtype)
@@ -251,8 +244,7 @@ def vertexBuffer(arrays, prefix=None, suffix=None, usage=None):
sizeinbytes = size * dimension * sizeofGLType(type_)
sizeinbytes = 4 * ((sizeinbytes + 3) >> 2) # 4 bytes alignment
copyoffset = vbosize + pre * dimension * sizeofGLType(type_)
- info.append((data, type_, size, dimension,
- vbosize, sizeinbytes, copyoffset))
+ info.append((data, type_, size, dimension, vbosize, sizeinbytes, copyoffset))
vbosize += sizeinbytes
vbo = VertexBuffer(size=vbosize, usage=usage)
@@ -261,6 +253,5 @@ def vertexBuffer(arrays, prefix=None, suffix=None, usage=None):
for data, type_, size, dimension, offset, sizeinbytes, copyoffset in info:
copysize = data.shape[0] * dimension * sizeofGLType(type_)
vbo.update(data, offset=copyoffset, size=copysize)
- result.append(
- VertexBufferAttrib(vbo, type_, size, dimension, offset, 0))
+ result.append(VertexBufferAttrib(vbo, type_, size, dimension, offset, 0))
return result
diff --git a/src/silx/gui/_glutils/__init__.py b/src/silx/gui/_glutils/__init__.py
index e88affd..9526ba4 100644
--- a/src/silx/gui/_glutils/__init__.py
+++ b/src/silx/gui/_glutils/__init__.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2022 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
diff --git a/src/silx/gui/_glutils/font.py b/src/silx/gui/_glutils/font.py
index 3ea474d..4c0268e 100644
--- a/src/silx/gui/_glutils/font.py
+++ b/src/silx/gui/_glutils/font.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,128 +28,12 @@ __license__ = "MIT"
__date__ = "13/10/2016"
-import logging
-import numpy
-
-from ..utils.image import convertQImageToArray
from .. import qt
-_logger = logging.getLogger(__name__)
+# Expose rasterMathText as part of this module
+from ..utils.matplotlib import rasterMathText as rasterText # noqa
-def getDefaultFontFamily():
+def getDefaultFontFamily() -> str:
"""Returns the default font family of the application"""
return qt.QApplication.instance().font().family()
-
-
-# Font weights
-ULTRA_LIGHT = 0
-"""Lightest characters: Minimum font weight"""
-
-LIGHT = 25
-"""Light characters"""
-
-NORMAL = 50
-"""Normal characters"""
-
-SEMI_BOLD = 63
-"""Between normal and bold characters"""
-
-BOLD = 74
-"""Thicker characters"""
-
-BLACK = 87
-"""Really thick characters"""
-
-ULTRA_BLACK = 99
-"""Thickest characters: Maximum font weight"""
-
-
-def rasterText(text, font,
- size=-1,
- weight=-1,
- italic=False,
- devicePixelRatio=1.0):
- """Raster text using Qt.
-
- It supports multiple lines.
-
- :param str text: The text to raster
- :param font: Font name or QFont to use
- :type font: str or :class:`QFont`
- :param int size:
- Font size in points
- Used only if font is given as name.
- :param int weight:
- Font weight in [0, 99], see QFont.Weight.
- Used only if font is given as name.
- :param bool italic:
- True for italic font (default: False).
- Used only if font is given as name.
- :param float devicePixelRatio:
- The current ratio between device and device-independent pixel
- (default: 1.0)
- :return: Corresponding image in gray scale and baseline offset from top
- :rtype: (HxW numpy.ndarray of uint8, int)
- """
- if not text:
- _logger.info("Trying to raster empty text, replaced by white space")
- text = ' ' # Replace empty text by white space to produce an image
-
- 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)
-
- # 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(int(width),
- int(bounds.height() * devicePixelRatio + 2),
- qt.QImage.Format_RGB888)
- image.setDevicePixelRatio(devicePixelRatio)
-
- # TODO if Qt5 use Format_Grayscale8 instead
- image.fill(0)
-
- # Raster text
- painter = qt.QPainter()
- painter.begin(image)
- painter.setPen(qt.Qt.white)
- painter.setFont(font)
- painter.drawText(bounds, qt.Qt.TextExpandTabs, text)
- painter.end()
-
- array = convertQImageToArray(image)
-
- # RGB to R
- array = numpy.ascontiguousarray(array[:, :, 0])
-
- # Remove leading and trailing empty columns but one on each side
- column_cumsum = numpy.cumsum(numpy.sum(array, axis=0))
- array = array[:, column_cumsum.argmin():column_cumsum.argmax() + 2]
-
- # Remove leading and trailing empty rows but one on each side
- row_cumsum = numpy.cumsum(numpy.sum(array, axis=1))
- min_row = row_cumsum.argmin()
- array = array[min_row:row_cumsum.argmax() + 2, :]
-
- return array, metrics.ascent() - min_row
diff --git a/src/silx/gui/_glutils/gl.py b/src/silx/gui/_glutils/gl.py
index 608d9ce..aff7617 100644
--- a/src/silx/gui/_glutils/gl.py
+++ b/src/silx/gui/_glutils/gl.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2022 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,13 +31,19 @@ __date__ = "25/07/2016"
from contextlib import contextmanager as _contextmanager
from ctypes import c_uint
import logging
+import sys
+from typing import Optional
+
+from packaging.version import Version
+
_logger = logging.getLogger(__name__)
import OpenGL
+
# Set the following to true for debugging
if _logger.getEffectiveLevel() <= logging.DEBUG:
- _logger.debug('Enabling PyOpenGL debug flags')
+ _logger.debug("Enabling PyOpenGL debug flags")
OpenGL.ERROR_LOGGING = True
OpenGL.ERROR_CHECKING = True
OpenGL.ERROR_ON_COPY = True
@@ -47,8 +52,14 @@ else:
OpenGL.ERROR_CHECKING = False
OpenGL.ERROR_ON_COPY = False
+if sys.version_info >= (3, 12) and Version(OpenGL.__version__) <= Version("3.1.7"):
+ # Python3.12 patch: see https://github.com/mcfletch/pyopengl/pull/100
+ OpenGL.FormatHandler.by_name("ctypesparameter").check.append("_ctypes.CArgObject")
+
+
import OpenGL.GL as _GL
from OpenGL.GL import * # noqa
+import OpenGL.platform
# Extentions core in OpenGL 3
from OpenGL.GL.ARB import framebuffer_object as _FBO
@@ -61,33 +72,66 @@ try:
GLchar
except NameError:
from ctypes import c_char
+
GLchar = c_char
-def testGL():
+def getPlatform() -> Optional[str]:
+ """Returns the name of the PyOpenGL class handling the platform.
+
+ E.g., GLXPlatform, EGLPlatform
+ """
+ try:
+ platform = OpenGL.platform.PLATFORM
+ except AttributeError:
+ return None
+ return platform.__class__.__name__
+
+
+def getVersion() -> tuple:
+ """Returns the GL version as tuple of integers.
+
+ Raises:
+ ValueError: If the version returned by the driver is not supported
+ """
+ try:
+ desc = glGetString(GL_VERSION)
+ if isinstance(desc, bytes):
+ desc = desc.decode("ascii")
+ version = desc.split(" ", 1)[0]
+ return tuple([int(i) for i in version.split(".")])
+ except Exception as e:
+ raise ValueError("GL version not properly formatted") from e
+
+
+def testGL() -> bool:
"""Test if required OpenGL version and extensions are available.
This MUST be run with an active OpenGL context.
"""
- version = glGetString(GL_VERSION).split()[0] # get version number
- major, minor = int(version[0]), int(version[2])
+ version = getVersion()
+ major, minor = version[0], version[1]
if major < 2 or (major == 2 and minor < 1):
- raise RuntimeError(
- "Requires at least OpenGL version 2.1, running with %s" % version)
+ _logger.error("OpenGL version >=2.1 required, running with %s" % version)
+ return False
- from OpenGL.GL.ARB.framebuffer_object import glInitFramebufferObjectARB
- from OpenGL.GL.ARB.texture_rg import glInitTextureRgARB
+ if major == 2:
+ from OpenGL.GL.ARB.framebuffer_object import glInitFramebufferObjectARB
+ from OpenGL.GL.ARB.texture_rg import glInitTextureRgARB
- if not glInitFramebufferObjectARB():
- raise RuntimeError(
- "OpenGL GL_ARB_framebuffer_object extension required !")
+ if not glInitFramebufferObjectARB():
+ _logger.error("OpenGL GL_ARB_framebuffer_object extension required!")
+ return False
- if not glInitTextureRgARB():
- raise RuntimeError("OpenGL GL_ARB_texture_rg extension required !")
+ if not glInitTextureRgARB():
+ _logger.error("OpenGL GL_ARB_texture_rg extension required!")
+ return False
+
+ return True
# Additional setup
-if hasattr(glget, 'addGLGetConstant'):
+if hasattr(glget, "addGLGetConstant"):
glget.addGLGetConstant(GL_FRAMEBUFFER_BINDING, (1,))
@@ -128,6 +172,7 @@ def disabled(capacity, disable=True):
# Additional OpenGL wrapping
+
def glGetActiveAttrib(program, index):
"""Wrap PyOpenGL glGetActiveAttrib"""
bufsize = glGetProgramiv(program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH)
@@ -141,28 +186,28 @@ def glGetActiveAttrib(program, index):
def glDeleteRenderbuffers(buffers):
- if not hasattr(buffers, '__len__'): # Support single int argument
+ if not hasattr(buffers, "__len__"): # Support single int argument
buffers = [buffers]
length = len(buffers)
_FBO.glDeleteRenderbuffers(length, (c_uint * length)(*buffers))
def glDeleteFramebuffers(buffers):
- if not hasattr(buffers, '__len__'): # Support single int argument
+ if not hasattr(buffers, "__len__"): # Support single int argument
buffers = [buffers]
length = len(buffers)
_FBO.glDeleteFramebuffers(length, (c_uint * length)(*buffers))
def glDeleteBuffers(buffers):
- if not hasattr(buffers, '__len__'): # Support single int argument
+ if not hasattr(buffers, "__len__"): # Support single int argument
buffers = [buffers]
length = len(buffers)
_GL.glDeleteBuffers(length, (c_uint * length)(*buffers))
def glDeleteTextures(textures):
- if not hasattr(textures, '__len__'): # Support single int argument
+ if not hasattr(textures, "__len__"): # Support single int argument
textures = [textures]
length = len(textures)
_GL.glDeleteTextures((c_uint * length)(*textures))
diff --git a/src/silx/gui/widgets/setup.py b/src/silx/gui/_glutils/test/__init__.py
index e96ac8d..e9dd44d 100644
--- a/src/silx/gui/widgets/setup.py
+++ b/src/silx/gui/_glutils/test/__init__.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,20 +21,3 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "11/10/2016"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('widgets', parent_package, top_path)
- config.add_subpackage('test')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
diff --git a/src/silx/gui/hdf5/setup.py b/src/silx/gui/_glutils/test/test_gl.py
index 786a851..d719c08 100644
--- a/src/silx/gui/hdf5/setup.py
+++ b/src/silx/gui/_glutils/test/test_gl.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,20 +21,16 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "28/09/2016"
+from .. import gl
-from numpy.distutils.misc_util import Configuration
+def test_version_bytes(mocker):
+ mocker.patch("silx.gui._glutils.gl.glGetString", return_value=b"3.0 Mock")
+ assert gl.getVersion() == (3, 0)
-def configuration(parent_package='', top_path=None):
- config = Configuration('hdf5', parent_package, top_path)
- config.add_subpackage('test')
- return config
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
+def test_version_str(mocker):
+ """In case glGetString returns str"""
+ mocker.patch("silx.gui._glutils.gl.glGetString", return_value="3.0 Mock")
+ assert gl.getVersion() == (3, 0)
diff --git a/src/silx/gui/_glutils/utils.py b/src/silx/gui/_glutils/utils.py
index 5886599..56ac935 100644
--- a/src/silx/gui/_glutils/utils.py
+++ b/src/silx/gui/_glutils/utils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
@@ -95,8 +94,9 @@ def segmentTrianglesIntersection(segment, triangles):
subVolumes[:, 2] = numpy.sum(t0s0CrossEdge01 * d, axis=1)
subVolumes[:, 0] = volume - subVolumes[:, 1] - subVolumes[:, 2]
intersect = numpy.logical_or(
- numpy.all(subVolumes >= 0., axis=1), # All positive
- numpy.all(subVolumes <= 0., axis=1)) # All negative
+ numpy.all(subVolumes >= 0.0, axis=1), # All positive
+ numpy.all(subVolumes <= 0.0, axis=1),
+ ) # All negative
intersect = numpy.where(intersect)[0] # Indices of intersected triangles
# Get barycentric coordinates
@@ -113,7 +113,7 @@ def segmentTrianglesIntersection(segment, triangles):
del volAlpha
del volume
- inSegmentMask = numpy.logical_and(t >= 0., t <= 1.)
+ inSegmentMask = numpy.logical_and(t >= 0.0, t <= 1.0)
intersect = intersect[inSegmentMask]
t = t[inSegmentMask]
barycentric = barycentric[inSegmentMask]
diff --git a/src/silx/gui/colors.py b/src/silx/gui/colors.py
index 12046cf..b47fa85 100755
--- a/src/silx/gui/colors.py
+++ b/src/silx/gui/colors.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 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,28 +24,39 @@
"""This module provides API to manage colors.
"""
-from __future__ import absolute_import
+from __future__ import annotations
__authors__ = ["T. Vincent", "H.Payno"]
__license__ = "MIT"
__date__ = "29/01/2019"
+
import numpy
import logging
+import numbers
+import re
+from collections.abc import Iterable
+from typing import Any, Sequence, Tuple, Union
+import silx
from silx.gui import qt
from silx.gui.utils import blockSignals
from silx.math import colormap as _colormap
from silx.utils.exceptions import NotEditableError
-from silx.utils import deprecation
_logger = logging.getLogger(__name__)
try:
import silx.gui.utils.matplotlib # noqa Initalize matplotlib
- from matplotlib import cm as _matplotlib_cm
- from matplotlib.pyplot import colormaps as _matplotlib_colormaps
+
+ try:
+ from matplotlib import colormaps as _matplotlib_colormaps
+ except ImportError: # For matplotlib < 3.5
+ from matplotlib import cm as _matplotlib_cm
+ from matplotlib.pyplot import colormaps as _matplotlib_colormaps
+ else:
+ _matplotlib_cm = None
except ImportError:
_logger.info("matplotlib not available, only embedded colormaps available")
_matplotlib_cm = None
@@ -56,29 +66,29 @@ except ImportError:
_COLORDICT = {}
"""Dictionary of common colors."""
-_COLORDICT['b'] = _COLORDICT['blue'] = '#0000ff'
-_COLORDICT['r'] = _COLORDICT['red'] = '#ff0000'
-_COLORDICT['g'] = _COLORDICT['green'] = '#00ff00'
-_COLORDICT['k'] = _COLORDICT['black'] = '#000000'
-_COLORDICT['w'] = _COLORDICT['white'] = '#ffffff'
-_COLORDICT['pink'] = '#ff66ff'
-_COLORDICT['brown'] = '#a52a2a'
-_COLORDICT['orange'] = '#ff9900'
-_COLORDICT['violet'] = '#6600ff'
-_COLORDICT['gray'] = _COLORDICT['grey'] = '#a0a0a4'
+_COLORDICT["b"] = _COLORDICT["blue"] = "#0000ff"
+_COLORDICT["r"] = _COLORDICT["red"] = "#ff0000"
+_COLORDICT["g"] = _COLORDICT["green"] = "#00ff00"
+_COLORDICT["k"] = _COLORDICT["black"] = "#000000"
+_COLORDICT["w"] = _COLORDICT["white"] = "#ffffff"
+_COLORDICT["pink"] = "#ff66ff"
+_COLORDICT["brown"] = "#a52a2a"
+_COLORDICT["orange"] = "#ff9900"
+_COLORDICT["violet"] = "#6600ff"
+_COLORDICT["gray"] = _COLORDICT["grey"] = "#a0a0a4"
# _COLORDICT['darkGray'] = _COLORDICT['darkGrey'] = '#808080'
# _COLORDICT['lightGray'] = _COLORDICT['lightGrey'] = '#c0c0c0'
-_COLORDICT['y'] = _COLORDICT['yellow'] = '#ffff00'
-_COLORDICT['m'] = _COLORDICT['magenta'] = '#ff00ff'
-_COLORDICT['c'] = _COLORDICT['cyan'] = '#00ffff'
-_COLORDICT['darkBlue'] = '#000080'
-_COLORDICT['darkRed'] = '#800000'
-_COLORDICT['darkGreen'] = '#008000'
-_COLORDICT['darkBrown'] = '#660000'
-_COLORDICT['darkCyan'] = '#008080'
-_COLORDICT['darkYellow'] = '#808000'
-_COLORDICT['darkMagenta'] = '#800080'
-_COLORDICT['transparent'] = '#00000000'
+_COLORDICT["y"] = _COLORDICT["yellow"] = "#ffff00"
+_COLORDICT["m"] = _COLORDICT["magenta"] = "#ff00ff"
+_COLORDICT["c"] = _COLORDICT["cyan"] = "#00ffff"
+_COLORDICT["darkBlue"] = "#000080"
+_COLORDICT["darkRed"] = "#800000"
+_COLORDICT["darkGreen"] = "#008000"
+_COLORDICT["darkBrown"] = "#660000"
+_COLORDICT["darkCyan"] = "#008080"
+_COLORDICT["darkYellow"] = "#808000"
+_COLORDICT["darkMagenta"] = "#800080"
+_COLORDICT["transparent"] = "#00000000"
# FIXME: It could be nice to expose a functional API instead of that attribute
@@ -91,109 +101,149 @@ DEFAULT_MAX_LIN = 1
"""Default max value if in linear normalization"""
-def rgba(color, colorDict=None):
- """Convert color code '#RRGGBB' and '#RRGGBBAA' to a tuple (R, G, B, A)
- of floats.
+_INDEXED_COLOR_PATTERN = re.compile(r"C(?P<index>[0-9]+)")
- It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and
- QColor as color argument.
- :param str color: The color to convert
- :param dict colorDict: A dictionary of color name conversion to color code
- :returns: RGBA colors as floats in [0., 1.]
- :rtype: tuple
- """
- if colorDict is None:
- colorDict = _COLORDICT
+ColorType = Union[str, Sequence[numbers.Real], qt.QColor]
+"""Type of :func:`rgba`'s color argument"""
- if hasattr(color, 'getRgb'): # QColor support
- color = color.getRgb()
- values = numpy.asarray(color).ravel()
+RGBAColorType = Tuple[float, float, float, float]
+"""Type of :func:`rgba` return value"""
- if values.dtype.kind in 'iuf': # integer or float
- # Color is an array
- assert len(values) in (3, 4)
- # Convert from integers in [0, 255] to float in [0, 1]
- if values.dtype.kind in 'iu':
- values = values / 255.
+def rgba(
+ color: ColorType,
+ colorDict: dict[str, str] | None = None,
+ colors: Sequence[str] | None = None,
+) -> RGBAColorType:
+ """Convert different kind of color definition to a tuple (R, G, B, A) of floats.
- # Clip to [0, 1]
- values[values < 0.] = 0.
- values[values > 1.] = 1.
+ It supports:
+ - color names: e.g., 'green'
+ - color codes: '#RRGGBB' and '#RRGGBBAA'
+ - indexed color names: e.g., 'C0'
+ - RGB(A) sequence of uint8 in [0, 255] or float in [0, 1]
+ - QColor
- if len(values) == 3:
- return values[0], values[1], values[2], 1.
- else:
- return tuple(values)
+ :param color: The color to convert
+ :param colorDict: A dictionary of color name conversion to color code
+ :param colors: Sequence of colors to use for `
+ :returns: RGBA colors as floats in [0., 1.]
+ :raises ValueError: if the input is not a valid color
+ """
+ if isinstance(color, str):
+ # From name
+ colorFromDict = (_COLORDICT if colorDict is None else colorDict).get(color)
+ if colorFromDict is not None:
+ return rgba(colorFromDict, colorDict, colors)
+
+ # From indexed color name: color{index}
+ match = _INDEXED_COLOR_PATTERN.fullmatch(color)
+ if match is not None:
+ if colors is None:
+ colors = silx.config.DEFAULT_PLOT_CURVE_COLORS
+ index = int(match["index"]) % len(colors)
+ return rgba(colors[index], colorDict, colors)
+
+ # From #code
+ if len(color) in (7, 9) and color[0] == "#":
+ r = int(color[1:3], 16) / 255.0
+ g = int(color[3:5], 16) / 255.0
+ b = int(color[5:7], 16) / 255.0
+ a = int(color[7:9], 16) / 255.0 if len(color) == 9 else 1.0
+ return r, g, b, a
+
+ raise ValueError(f"The string '{color}' is not a valid color")
+
+ # From QColor
+ if isinstance(color, qt.QColor):
+ return rgba(color.getRgb(), colorDict, colors)
+
+ # From array
+ values = numpy.asarray(color).ravel()
+
+ if values.dtype.kind not in "iuf":
+ raise ValueError(
+ f"The array color must be integer/unsigned or float. Found '{values.dtype.kind}'"
+ )
+ if len(values) not in (3, 4):
+ raise ValueError(
+ f"The array color must have 3 or 4 compound. Found '{len(values)}'"
+ )
+
+ # Convert from integers in [0, 255] to float in [0, 1]
+ if values.dtype.kind in "iu":
+ values = values / 255.0
- # We assume color is a string
- if not color.startswith('#'):
- color = colorDict[color]
+ values = numpy.clip(values, 0.0, 1.0)
- assert len(color) in (7, 9) and color[0] == '#'
- r = int(color[1:3], 16) / 255.
- g = int(color[3:5], 16) / 255.
- b = int(color[5:7], 16) / 255.
- a = int(color[7:9], 16) / 255. if len(color) == 9 else 1.
- return r, g, b, a
+ if len(values) == 3:
+ return values[0], values[1], values[2], 1.0
+ return tuple(values)
-def greyed(color, colorDict=None):
+def greyed(
+ color: ColorType,
+ colorDict: dict[str, str] | None = None,
+) -> RGBAColorType:
"""Convert color code '#RRGGBB' and '#RRGGBBAA' to a grey color
(R, G, B, A).
It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and
QColor as color argument.
- :param str color: The color to convert
- :param dict colorDict: A dictionary of color name conversion to color code
+ :param color: The color to convert
+ :param colorDict: A dictionary of color name conversion to color code
:returns: RGBA colors as floats in [0., 1.]
- :rtype: tuple
"""
r, g, b, a = rgba(color=color, colorDict=colorDict)
g = 0.21 * r + 0.72 * g + 0.07 * b
return g, g, g, a
-def asQColor(color):
+def asQColor(color: ColorType) -> qt.QColor:
"""Convert color code '#RRGGBB' and '#RRGGBBAA' to a `qt.QColor`.
It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and
QColor as color argument.
- :param str color: The color to convert
- :rtype: qt.QColor
+ :param color: The color to convert
"""
color = rgba(color)
return qt.QColor.fromRgbF(*color)
-def cursorColorForColormap(colormapName):
+def cursorColorForColormap(colormapName: str) -> str:
"""Get a color suitable for overlay over a colormap.
- :param str colormapName: The name of the colormap.
+ :param colormapName: The name of the colormap.
:return: Name of the color.
- :rtype: str
"""
return _colormap.get_colormap_cursor_color(colormapName)
# Colormap loader
-def _registerColormapFromMatplotlib(name, cursor_color='black', preferred=False):
- colormap = _matplotlib_cm.get_cmap(name)
+
+def _registerColormapFromMatplotlib(
+ name: str,
+ cursor_color: str = "black",
+ preferred: bool = False,
+):
+ if _matplotlib_cm is not None:
+ colormap = _matplotlib_cm.get_cmap(name)
+ else: # matplotlib >= 3.5
+ colormap = _matplotlib_colormaps[name]
lut = colormap(numpy.linspace(0, 1, colormap.N, endpoint=True))
colors = _colormap.array_to_rgba8888(lut)
registerLUT(name, colors, cursor_color, preferred)
-def _getColormap(name):
+def _getColormap(name: str) -> numpy.ndarray:
"""Returns the color LUT corresponding to a colormap name
- :param str name: Name of the colormap to load
+ :param name: Name of the colormap to load
:returns: Corresponding table of colors
- :rtype: numpy.ndarray
:raise ValueError: If no colormap corresponds to name
"""
name = str(name)
@@ -201,40 +251,65 @@ def _getColormap(name):
return _colormap.get_colormap_lut(name)
except ValueError:
# Colormap is not available, try to load it from matplotlib
- _registerColormapFromMatplotlib(name, 'black', False)
+ _registerColormapFromMatplotlib(name, "black", False)
return _colormap.get_colormap_lut(name)
+class _Colormappable:
+ """Class for objects that can be colormapped by a :class:`Colormap`
+
+ Used by silx.gui.plot.items.core.ColormapMixIn
+ """
+
+ def _getColormapAutoscaleRange(
+ self,
+ colormap: Colormap | None,
+ ) -> tuple[float | None, float | None]:
+ """Returns the autoscale range for given colormap.
+
+ :param colormap:
+ The colormap for which to compute the autoscale range.
+ If None, the default, the colormap of the item is used
+ :return: (vmin, vmax) range
+ """
+ raise NotImplementedError("This method must be implemented in subclass")
+
+ def getColormappedData(copy: bool = False) -> numpy.ndarray | None:
+ """Returns the data used to compute the displayed colors
+
+ :param copy: True to get a copy, False to get internal data (do not modify!).
+ """
+ raise NotImplementedError("This method must be implemented in subclass")
+
+
class Colormap(qt.QObject):
"""Description of a colormap
If no `name` nor `colors` are provided, a default gray LUT is used.
- :param str name: Name of the colormap
- :param tuple colors: optional, custom colormap.
+ :param name: Name of the colormap
+ :param 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 normalization: Normalization: 'linear' (default) or 'log'
+ :param normalization: Normalization: 'linear' (default) or 'log'
:param vmin: Lower bound of the colormap or None for autoscale (default)
- :type vmin: Union[None, float]
:param vmax: Upper bounds of the colormap or None for autoscale (default)
- :type vmax: Union[None, float]
"""
- LINEAR = 'linear'
+ LINEAR = "linear"
"""constant for linear normalization"""
- LOGARITHM = 'log'
+ LOGARITHM = "log"
"""constant for logarithmic normalization"""
- SQRT = 'sqrt'
+ SQRT = "sqrt"
"""constant for square root normalization"""
- GAMMA = 'gamma'
+ GAMMA = "gamma"
"""Constant for gamma correction normalization"""
- ARCSINH = 'arcsinh'
+ ARCSINH = "arcsinh"
"""constant for inverse hyperbolic sine normalization"""
_BASIC_NORMALIZATIONS = {
@@ -242,16 +317,16 @@ class Colormap(qt.QObject):
LOGARITHM: _colormap.LogarithmicNormalization(),
SQRT: _colormap.SqrtNormalization(),
ARCSINH: _colormap.ArcsinhNormalization(),
- }
+ }
"""Normalizations without parameters"""
NORMALIZATIONS = LINEAR, LOGARITHM, SQRT, GAMMA, ARCSINH
"""Tuple of managed normalizations"""
- MINMAX = 'minmax'
+ MINMAX = "minmax"
"""constant for autoscale using min/max data range"""
- STDDEV3 = 'stddev3'
+ STDDEV3 = "stddev3"
"""constant for autoscale using mean +/- 3*std(data)
with a clamp on min/max of the data"""
@@ -263,7 +338,15 @@ class Colormap(qt.QObject):
_DEFAULT_NAN_COLOR = 255, 255, 255, 0
- def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None, autoscaleMode=MINMAX):
+ def __init__(
+ self,
+ name: str | None = None,
+ colors: numpy.ndarray | None = None,
+ normalization: str = LINEAR,
+ vmin: float | None = None,
+ vmax: float | None = None,
+ autoscaleMode: str = MINMAX,
+ ):
qt.QObject.__init__(self)
self._editable = True
self.__gamma = 2.0
@@ -276,7 +359,7 @@ class Colormap(qt.QObject):
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 += " Autoscale will be performed."
m = m % (vmin, vmax)
_logger.warning(m)
vmin = None
@@ -286,13 +369,7 @@ class Colormap(qt.QObject):
self._colors = None
if colors is not None and name is not None:
- deprecation.deprecated_warning("Argument",
- name="silx.gui.plot.Colors",
- reason="name and colors can't be used at the same time",
- since_version="0.10.0",
- skip_backtrace_count=1)
-
- colors = None
+ raise ValueError("name and colors arguments can't be set at the same time")
if name is not None:
self.setName(name) # And resets colormap LUT
@@ -309,13 +386,13 @@ class Colormap(qt.QObject):
self.__warnBadVmin = True
self.__warnBadVmax = True
- def setFromColormap(self, other):
+ def setFromColormap(self, other: Colormap):
"""Set this colormap using information from the `other` colormap.
- :param ~silx.gui.colors.Colormap other: Colormap to use as reference.
+ :param other: Colormap to use as reference.
"""
if not self.isEditable():
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
if self == other:
return
with blockSignals(self):
@@ -326,22 +403,19 @@ class Colormap(qt.QObject):
self.setColormapLUT(other.getColormapLUT())
self.setNaNColor(other.getNaNColor())
self.setNormalization(other.getNormalization())
- self.setGammaNormalizationParameter(
- other.getGammaNormalizationParameter())
+ self.setGammaNormalizationParameter(other.getGammaNormalizationParameter())
self.setAutoscaleMode(other.getAutoscaleMode())
self.setVRange(*other.getVRange())
self.setEditable(other.isEditable())
self.sigChanged.emit()
- def getNColors(self, nbColors=None):
+ def getNColors(self, nbColors: int | None = None) -> numpy.ndarray:
"""Returns N colors computed by sampling the colormap regularly.
:param nbColors:
The number of colors in the returned array or None for the default value.
The default value is the size of the colormap LUT.
- :type nbColors: int or None
:return: 2D array of uint8 of shape (nbColors, 4)
- :rtype: numpy.ndarray
"""
# Handle default value for nbColors
if nbColors is None:
@@ -351,20 +425,17 @@ class Colormap(qt.QObject):
colormap = self.copy()
colormap.setNormalization(Colormap.LINEAR)
colormap.setVRange(vmin=0, vmax=nbColors - 1)
- colors = colormap.applyToData(
- numpy.arange(nbColors, dtype=numpy.int32))
+ colors = colormap.applyToData(numpy.arange(nbColors, dtype=numpy.int32))
return colors
- def getName(self):
- """Return the name of the colormap
- :rtype: str
- """
+ def getName(self) -> str | None:
+ """Return the name of the colormap"""
return self._name
- def setName(self, name):
+ def setName(self, name: str):
"""Set the name of the colormap to use.
- :param str name: The name of the colormap.
+ :param name: The name of the colormap.
At least the following names are supported: 'gray',
'reversed gray', 'temperature', 'red', 'green', 'blue', 'jet',
'viridis', 'magma', 'inferno', 'plasma'.
@@ -373,44 +444,45 @@ class Colormap(qt.QObject):
if self._name == name:
return
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
if name not in self.getSupportedColormaps():
raise ValueError("Colormap name '%s' is not supported" % name)
self._name = name
self._colors = _getColormap(self._name)
self.sigChanged.emit()
- def getColormapLUT(self, copy=True):
+ def getColormapLUT(self, copy: bool = True) -> numpy.ndarray | None:
"""Return the list of colors for the colormap or None if not set.
This returns None if the colormap was set with :meth:`setName`.
Use :meth:`getNColors` to get the colormap LUT for any colormap.
- :param bool copy: If true a copy of the numpy array is provided
+ :param copy: If true a copy of the numpy array is provided
:return: the list of colors for the colormap or None if not set
- :rtype: numpy.ndarray or None
"""
if self._name is None:
return numpy.array(self._colors, copy=copy)
- else:
- return None
+ return None
- def setColormapLUT(self, colors):
+ def setColormapLUT(self, colors: numpy.ndarray):
"""Set the colors of the colormap.
- :param numpy.ndarray colors: the colors of the LUT.
+ :param colors: the colors of the LUT.
If float, it is converted from [0, 1] to uint8 range.
Otherwise it is casted to uint8.
.. warning: this will set the value of name to None
"""
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
assert colors is not None
colors = numpy.array(colors, copy=False)
if colors.shape == ():
- raise TypeError("An array is expected for 'colors' argument. '%s' was found." % type(colors))
+ raise TypeError(
+ "An array is expected for 'colors' argument. '%s' was found."
+ % type(colors)
+ )
assert len(colors) != 0
assert colors.ndim >= 2
colors.shape = -1, colors.shape[-1]
@@ -418,44 +490,39 @@ class Colormap(qt.QObject):
self._name = None
self.sigChanged.emit()
- def getNaNColor(self):
- """Returns the color to use for Not-A-Number floating point value.
-
- :rtype: QColor
- """
+ def getNaNColor(self) -> qt.QColor:
+ """Returns the color to use for Not-A-Number floating point value."""
return qt.QColor(*self.__nanColor)
- def setNaNColor(self, color):
+ def setNaNColor(self, color: ColorType):
"""Set the color to use for Not-A-Number floating point value.
:param color: RGB(A) color to use for NaN values
- :type color: QColor, str, tuple of uint8 or float in [0., 1.]
"""
color = (numpy.array(rgba(color)) * 255).astype(numpy.uint8)
if not numpy.array_equal(self.__nanColor, color):
self.__nanColor = color
self.sigChanged.emit()
- def getNormalization(self):
+ def getNormalization(self) -> str:
"""Return the normalization of the colormap.
See :meth:`setNormalization` for returned values.
:return: the normalization of the colormap
- :rtype: str
"""
return self._normalization
- def setNormalization(self, norm):
+ def setNormalization(self, norm: str):
"""Set the colormap normalization.
Accepted normalizations: 'log', 'linear', 'sqrt'
- :param str norm: the norm to set
+ :param norm: the norm to set
"""
assert norm in self.NORMALIZATIONS
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
norm = str(norm)
if norm != self._normalization:
self._normalization = norm
@@ -463,71 +530,63 @@ class Colormap(qt.QObject):
self.__warnBadVmax = True
self.sigChanged.emit()
- def setGammaNormalizationParameter(self, gamma: float) -> None:
+ def setGammaNormalizationParameter(self, gamma: float):
"""Set the gamma correction parameter.
Only used for gamma correction normalization.
- :param float gamma:
:raise ValueError: If gamma is not valid
"""
- if gamma < 0. or not numpy.isfinite(gamma):
+ if gamma < 0.0 or not numpy.isfinite(gamma):
raise ValueError("Gamma value not supported")
if gamma != self.__gamma:
self.__gamma = gamma
self.sigChanged.emit()
def getGammaNormalizationParameter(self) -> float:
- """Returns the gamma correction parameter value.
-
- :rtype: float
- """
+ """Returns the gamma correction parameter value."""
return self.__gamma
- def getAutoscaleMode(self):
- """Return the autoscale mode of the colormap ('minmax' or 'stddev3')
-
- :rtype: str
- """
+ def getAutoscaleMode(self) -> str:
+ """Return the autoscale mode of the colormap ('minmax' or 'stddev3')"""
return self._autoscaleMode
- def setAutoscaleMode(self, mode):
+ def setAutoscaleMode(self, mode: str):
"""Set the autoscale mode: either 'minmax' or 'stddev3'
- :param str mode: the mode to set
+ :param mode: the mode to set
"""
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
assert mode in self.AUTOSCALE_MODES
if mode != self._autoscaleMode:
self._autoscaleMode = mode
self.sigChanged.emit()
- def isAutoscale(self):
+ def isAutoscale(self) -> bool:
"""Return True if both min and max are in autoscale mode"""
return self._vmin is None and self._vmax is None
- def getVMin(self):
+ def getVMin(self) -> float | None:
"""Return the lower bound of the colormap
- :return: the lower bound of the colormap
- :rtype: float or None
- """
+ :return: the lower bound of the colormap
+ """
return self._vmin
- def setVMin(self, vmin):
+ def setVMin(self, vmin: float | None):
"""Set the minimal value of the colormap
- :param float vmin: Lower bound of the colormap or None for autoscale
- (default)
- value)
+ :param vmin: Lower bound of the colormap or None for autoscale (initial value)
"""
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
if vmin is not None:
if self._vmax is not None and vmin > self._vmax:
- err = "Can't set vmin because vmin >= vmax. " \
- "vmin = %s, vmax = %s" % (vmin, self._vmax)
+ err = "Can't set vmin because vmin >= vmax. " "vmin = %s, vmax = %s" % (
+ vmin,
+ self._vmax,
+ )
raise ValueError(err)
if vmin != self._vmin:
@@ -535,26 +594,26 @@ class Colormap(qt.QObject):
self.__warnBadVmin = True
self.sigChanged.emit()
- def getVMax(self):
+ def getVMax(self) -> float | None:
"""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):
+ def setVMax(self, vmax: float | None):
"""Set the maximal value of the colormap
- :param float vmax: Upper bounds of the colormap or None for autoscale
- (default)
+ :param vmax: Upper bounds of the colormap or None for autoscale (initial value)
"""
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
if vmax is not None:
if self._vmin is not None and vmax < self._vmin:
- err = "Can't set vmax because vmax <= vmin. " \
- "vmin = %s, vmax = %s" % (self._vmin, vmax)
+ err = "Can't set vmax because vmax <= vmin. " "vmin = %s, vmax = %s" % (
+ self._vmin,
+ vmax,
+ )
raise ValueError(err)
if vmax != self._vmax:
@@ -562,25 +621,24 @@ class Colormap(qt.QObject):
self.__warnBadVmax = True
self.sigChanged.emit()
- def isEditable(self):
- """ Return if the colormap is editable or not
+ def isEditable(self) -> bool:
+ """Return if the colormap is editable or not
:return: editable state of the colormap
- :rtype: bool
"""
return self._editable
- def setEditable(self, editable):
+ def setEditable(self, editable: bool):
"""
Set the editable state of the colormap
- :param bool editable: is the colormap editable
+ :param editable: is the colormap editable
"""
assert type(editable) is bool
self._editable = editable
self.sigChanged.emit()
- def _getNormalizer(self):
+ def _getNormalizer(self): # TODO
"""Returns normalizer object"""
normalization = self.getNormalization()
if normalization == self.GAMMA:
@@ -588,26 +646,28 @@ class Colormap(qt.QObject):
else:
return self._BASIC_NORMALIZATIONS[normalization]
- def _computeAutoscaleRange(self, data):
+ def _computeAutoscaleRange(self, data: numpy.ndarray):
"""Compute the data range which will be used in autoscale mode.
- :param numpy.ndarray data: The data for which to compute the range
+ :param data: The data for which to compute the range
:return: (vmin, vmax) range
"""
- return self._getNormalizer().autoscale(
- data, mode=self.getAutoscaleMode())
+ return self._getNormalizer().autoscale(data, mode=self.getAutoscaleMode())
- def getColormapRange(self, data=None):
+ def getColormapRange(
+ self,
+ data: numpy.ndarray | _Colormappable | None = None,
+ ) -> tuple[float, float]:
"""Return (vmin, vmax) the range of the colormap for the given data or item.
- :param Union[numpy.ndarray,~silx.gui.plot.items.ColormapMixIn] data:
- The data or item to use for autoscale bounds.
+ :param data: The data or item to use for autoscale bounds.
:return: (vmin, vmax) corresponding to the colormap applied to data if provided.
- :rtype: tuple
"""
vmin = self._vmin
vmax = self._vmax
- assert vmin is None or vmax is None or vmin <= vmax # TODO handle this in setters
+ assert (
+ vmin is None or vmax is None or vmin <= vmax
+ ) # TODO handle this in setters
normalizer = self._getNormalizer()
@@ -615,26 +675,22 @@ class Colormap(qt.QObject):
if vmin is not None and not normalizer.is_valid(vmin):
if self.__warnBadVmin:
self.__warnBadVmin = False
- _logger.info(
- 'Invalid vmin, switching to autoscale for lower bound')
+ _logger.info("Invalid vmin, switching to autoscale for lower bound")
vmin = None
if vmax is not None and not normalizer.is_valid(vmax):
if self.__warnBadVmax:
self.__warnBadVmax = False
- _logger.info(
- 'Invalid vmax, switching to autoscale for upper bound')
+ _logger.info("Invalid vmax, switching to autoscale for upper bound")
vmax = None
if vmin is None or vmax is None: # Handle autoscale
- from .plot.items.core import ColormapMixIn # avoid cyclic import
- if isinstance(data, ColormapMixIn):
+ if isinstance(data, _Colormappable):
min_, max_ = data._getColormapAutoscaleRange(self)
# Make sure min_, max_ are not None
min_ = normalizer.DEFAULT_RANGE[0] if min_ is None else min_
max_ = normalizer.DEFAULT_RANGE[1] if max_ is None else max_
else:
- min_, max_ = normalizer.autoscale(
- data, mode=self.getAutoscaleMode())
+ min_, max_ = normalizer.autoscale(data, mode=self.getAutoscaleMode())
if vmin is None: # Set vmin respecting provided vmax
vmin = min_ if vmax is None else min(min_, vmax)
@@ -644,16 +700,15 @@ class Colormap(qt.QObject):
return vmin, vmax
- def getVRange(self):
+ def getVRange(self) -> tuple[float | None, float | None]:
"""Get the bounds of the colormap
- :rtype: Tuple(Union[float,None],Union[float,None])
:returns: A tuple of 2 values for min and max. Or None instead of float
for autoscale
"""
return self.getVMin(), self.getVMax()
- def setVRange(self, vmin, vmax):
+ def setVRange(self, vmin: float | None, vmax: float | None):
"""Set the bounds of the colormap
:param vmin: Lower bound of the colormap or None for autoscale
@@ -662,11 +717,23 @@ class Colormap(qt.QObject):
(default)
"""
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
+
+ if (vmin is not None and not numpy.isfinite(vmin)) or (
+ vmax is not None and not numpy.isfinite(vmax)
+ ):
+ err = (
+ "Can't set vmin and vmax because vmin or vmax are not finite "
+ "vmin = %s, vmax = %s" % (vmin, vmax)
+ )
+ raise ValueError(err)
+
if vmin is not None and vmax is not None:
if vmin > vmax:
- err = "Can't set vmin and vmax because vmin >= vmax " \
- "vmin = %s, vmax = %s" % (vmin, vmax)
+ err = (
+ "Can't set vmin and vmax because vmin >= vmax "
+ "vmin = %s, vmax = %s" % (vmin, vmax)
+ )
raise ValueError(err)
if self._vmin == vmin and self._vmax == vmax:
@@ -680,79 +747,78 @@ class Colormap(qt.QObject):
self._vmax = vmax
self.sigChanged.emit()
- def __getitem__(self, item):
- if item == 'autoscale':
+ def __getitem__(self, item: str):
+ if item == "autoscale":
return self.isAutoscale()
- elif item == 'name':
+ elif item == "name":
return self.getName()
- elif item == 'normalization':
+ elif item == "normalization":
return self.getNormalization()
- elif item == 'vmin':
+ elif item == "vmin":
return self.getVMin()
- elif item == 'vmax':
+ elif item == "vmax":
return self.getVMax()
- elif item == 'colors':
+ elif item == "colors":
return self.getColormapLUT()
- elif item == 'autoscaleMode':
+ elif item == "autoscaleMode":
return self.getAutoscaleMode()
else:
raise KeyError(item)
- def _toDict(self):
+ def _toDict(self) -> dict:
"""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': self.getColormapLUT(),
- 'vmin': self._vmin,
- 'vmax': self._vmax,
- 'autoscale': self.isAutoscale(),
- 'normalization': self.getNormalization(),
- 'autoscaleMode': self.getAutoscaleMode(),
- }
-
- def _setFromDict(self, dic):
+ "name": self._name,
+ "colors": self.getColormapLUT(),
+ "vmin": self._vmin,
+ "vmax": self._vmax,
+ "autoscale": self.isAutoscale(),
+ "normalization": self.getNormalization(),
+ "autoscaleMode": self.getAutoscaleMode(),
+ }
+
+ def _setFromDict(self, dic: dict):
"""Set values to the colormap from a dictionary
- :param dict dic: the colormap as a dictionary
+ :param dic: the colormap as a dictionary
"""
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- name = dic['name'] if 'name' in dic else None
- colors = dic['colors'] if 'colors' in dic else None
+ raise NotEditableError("Colormap is not editable")
+ name = dic["name"] if "name" in dic else None
+ colors = dic["colors"] if "colors" in dic else None
if name is not None and colors is not None:
if isinstance(colors, int):
# Filter out argument which was supported but never used
_logger.info("Unused 'colors' from colormap dictionary filterer.")
colors = 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']
+ 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
+ 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'
+ 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 recognized (%s)' % normalization
+ err = "Given normalization is not recognized (%s)" % normalization
raise ValueError(err)
- autoscaleMode = dic.get('autoscaleMode', Colormap.MINMAX)
+ autoscaleMode = dic.get("autoscaleMode", Colormap.MINMAX)
if autoscaleMode not in Colormap.AUTOSCALE_MODES:
- err = 'Given autoscale mode is not recognized (%s)' % autoscaleMode
+ err = "Given autoscale mode is not recognized (%s)" % autoscaleMode
raise ValueError(err)
# If autoscale, then set boundaries to None
- if dic.get('autoscale', False):
+ if dic.get("autoscale", False):
vmin, vmax = None, None
if name is not None:
@@ -770,61 +836,57 @@ class Colormap(qt.QObject):
self.sigChanged.emit()
@staticmethod
- def _fromDict(dic):
+ def _fromDict(dic: dict):
colormap = Colormap()
colormap._setFromDict(dic)
return colormap
- def copy(self):
- """Return a copy of the Colormap.
-
- :rtype: silx.gui.colors.Colormap
- """
- colormap = Colormap(name=self._name,
- colors=self.getColormapLUT(),
- vmin=self._vmin,
- vmax=self._vmax,
- normalization=self.getNormalization(),
- autoscaleMode=self.getAutoscaleMode())
+ def copy(self) -> Colormap:
+ """Return a copy of the Colormap."""
+ colormap = Colormap(
+ name=self._name,
+ colors=self.getColormapLUT(),
+ vmin=self._vmin,
+ vmax=self._vmax,
+ normalization=self.getNormalization(),
+ autoscaleMode=self.getAutoscaleMode(),
+ )
colormap.setNaNColor(self.getNaNColor())
- colormap.setGammaNormalizationParameter(
- self.getGammaNormalizationParameter())
+ colormap.setGammaNormalizationParameter(self.getGammaNormalizationParameter())
colormap.setEditable(self.isEditable())
return colormap
- def applyToData(self, data, reference=None):
+ def applyToData(
+ self,
+ data: numpy.ndarray | _Colormappable,
+ reference: numpy.ndarray | _Colormappable | None = None,
+ ) -> numpy.ndarray:
"""Apply the colormap to the data
- :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn] data:
+ :param data:
The data to convert or the item for which to apply the colormap.
- :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn,None] reference:
+ :param reference:
The data or item to use as reference to compute autoscale
"""
if reference is None:
reference = data
vmin, vmax = self.getColormapRange(reference)
- if hasattr(data, "getColormappedData"): # Use item's data
+ if isinstance(data, _Colormappable): # Use item's data
data = data.getColormappedData(copy=False)
return _colormap.cmap(
- data,
- self._colors,
- vmin,
- vmax,
- self._getNormalizer(),
- self.__nanColor)
+ data, self._colors, vmin, vmax, self._getNormalizer(), self.__nanColor
+ )
@staticmethod
- def getSupportedColormaps():
+ def getSupportedColormaps() -> tuple[str, ...]:
"""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',
'viridis', 'magma', 'inferno', 'plasma')
-
- :rtype: tuple
"""
registered_colormaps = _colormap.get_registered_colormaps()
colormaps = set(registered_colormaps)
@@ -832,14 +894,15 @@ class Colormap(qt.QObject):
colormaps.update(_matplotlib_colormaps())
# Put registered_colormaps first
- colormaps = tuple(cmap for cmap in sorted(colormaps)
- if cmap not in registered_colormaps)
+ colormaps = tuple(
+ cmap for cmap in sorted(colormaps) if cmap not in registered_colormaps
+ )
return registered_colormaps + colormaps
- def __str__(self):
+ def __str__(self) -> str:
return str(self._toDict())
- def __eq__(self, other):
+ def __eq__(self, other: Any):
"""Compare colormap values and not pointers"""
if other is None:
return False
@@ -848,28 +911,31 @@ class Colormap(qt.QObject):
if self.getNormalization() != other.getNormalization():
return False
if self.getNormalization() == self.GAMMA:
- delta = self.getGammaNormalizationParameter() - other.getGammaNormalizationParameter()
+ delta = (
+ self.getGammaNormalizationParameter()
+ - other.getGammaNormalizationParameter()
+ )
if abs(delta) > 0.001:
return False
- return (self.getName() == other.getName() and
- self.getAutoscaleMode() == other.getAutoscaleMode() and
- self.getVMin() == other.getVMin() and
- self.getVMax() == other.getVMax() and
- numpy.array_equal(self.getColormapLUT(), other.getColormapLUT())
- )
+ return (
+ self.getName() == other.getName()
+ and self.getAutoscaleMode() == other.getAutoscaleMode()
+ and self.getVMin() == other.getVMin()
+ and self.getVMax() == other.getVMax()
+ and numpy.array_equal(self.getColormapLUT(), other.getColormapLUT())
+ )
_SERIAL_VERSION = 3
- def restoreState(self, byteArray):
+ def restoreState(self, byteArray: qt.QByteArray) -> bool:
"""
Read the colormap state from a QByteArray.
- :param qt.QByteArray byteArray: Stream containing the state
+ :param byteArray: Stream containing the state
:return: True if the restoration sussseed
- :rtype: bool
"""
if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
+ raise NotEditableError("Colormap is not editable")
stream = qt.QDataStream(byteArray, qt.QIODevice.ReadOnly)
className = stream.readQString()
@@ -878,7 +944,7 @@ class Colormap(qt.QObject):
return False
version = stream.readUInt32()
- if version not in numpy.arange(1, self._SERIAL_VERSION+1):
+ if version not in numpy.arange(1, self._SERIAL_VERSION + 1):
_logger.warning("Serial version mismatch. Found %d." % version)
return False
@@ -908,7 +974,12 @@ class Colormap(qt.QObject):
if version <= 2:
nanColor = self._DEFAULT_NAN_COLOR
else:
- nanColor = stream.readInt32(), stream.readInt32(), stream.readInt32(), stream.readInt32()
+ nanColor = (
+ stream.readInt32(),
+ stream.readInt32(),
+ stream.readInt32(),
+ stream.readInt32(),
+ )
# emit change event only once
old = self.blockSignals(True)
@@ -925,12 +996,8 @@ class Colormap(qt.QObject):
self.sigChanged.emit()
return True
- def saveState(self):
- """
- Save state of the colomap into a QDataStream.
-
- :rtype: qt.QByteArray
- """
+ def saveState(self) -> qt.QByteArray:
+ """Save state of the colomap into a QDataStream."""
data = qt.QByteArray()
stream = qt.QDataStream(data, qt.QIODevice.WriteOnly)
@@ -962,20 +1029,27 @@ Tuple of preferred colormap names accessed with :meth:`preferredColormaps`.
"""
_DEFAULT_PREFERRED_COLORMAPS = (
- 'gray', 'reversed gray', 'red', 'green', 'blue',
- 'viridis', 'cividis', 'magma', 'inferno', 'plasma',
- 'temperature',
- 'jet', 'hsv'
+ "gray",
+ "reversed gray",
+ "red",
+ "green",
+ "blue",
+ "viridis",
+ "cividis",
+ "magma",
+ "inferno",
+ "plasma",
+ "temperature",
+ "jet",
+ "hsv",
)
-def preferredColormaps():
+def preferredColormaps() -> tuple[str, ...]:
"""Returns the name of the preferred colormaps.
This list is used by widgets allowing to change the colormap
like the :class:`ColormapDialog` as a subset of colormap choices.
-
- :rtype: tuple of str
"""
global _PREFERRED_COLORMAPS
if _PREFERRED_COLORMAPS is None:
@@ -984,14 +1058,13 @@ def preferredColormaps():
return tuple(_PREFERRED_COLORMAPS)
-def setPreferredColormaps(colormaps):
+def setPreferredColormaps(colormaps: Iterable[str]):
"""Set the list of preferred colormap names.
Warning: If a colormap name is not available
it will be removed from the list.
:param colormaps: Not empty list of colormap names
- :type colormaps: iterable of str
:raise ValueError: if the list of available preferred colormaps is empty.
"""
supportedColormaps = Colormap.getSupportedColormaps()
@@ -1003,18 +1076,23 @@ def setPreferredColormaps(colormaps):
_PREFERRED_COLORMAPS = colormaps
-def registerLUT(name, colors, cursor_color='black', preferred=True):
+def registerLUT(
+ name: str,
+ colors: numpy.ndarray,
+ cursor_color: str = "black",
+ preferred: bool = True,
+):
"""Register a custom LUT to be used with `Colormap` objects.
It can override existing LUT names.
- :param str name: Name of the LUT as defined to configure colormaps
- :param numpy.ndarray colors: The custom LUT to register.
+ :param name: Name of the LUT as defined to configure colormaps
+ :param colors: The custom LUT to register.
Nx3 or Nx4 numpy array of RGB(A) colors,
either uint8 or float in [0, 1].
- :param bool preferred: If true, this LUT will be displayed as part of the
+ :param preferred: If true, this LUT will be displayed as part of the
preferred colormaps in dialogs.
- :param str cursor_color: Color used to display overlay over images using
+ :param cursor_color: Color used to display overlay over images using
colormap with this LUT.
"""
_colormap.register_colormap(name, colors, cursor_color)
@@ -1032,5 +1110,5 @@ def registerLUT(name, colors, cursor_color='black', preferred=True):
# Load some colormaps from matplotlib by default
if _matplotlib_cm is not None:
- _registerColormapFromMatplotlib('jet', cursor_color='pink', preferred=True)
- _registerColormapFromMatplotlib('hsv', cursor_color='black', preferred=True)
+ _registerColormapFromMatplotlib("jet", cursor_color="pink", preferred=True)
+ _registerColormapFromMatplotlib("hsv", cursor_color="black", preferred=True)
diff --git a/src/silx/gui/conftest.py b/src/silx/gui/conftest.py
index 74b5c19..2e9cf0d 100644
--- a/src/silx/gui/conftest.py
+++ b/src/silx/gui/conftest.py
@@ -1,5 +1,47 @@
import pytest
+from silx.gui import qt
+from silx.gui.qt.inspect import isValid
+
+
@pytest.fixture(autouse=True)
def auto_qapp(qapp):
pass
+
+
+@pytest.fixture
+def qWidgetFactory(qapp, qapp_utils):
+ """QWidget factory as fixture
+
+ This fixture provides a function taking a QWidget subclass as argument
+ which returns an instance of this QWidget making sure it is shown first
+ and destroyed once the test is done.
+ """
+ widgets = []
+
+ def createWidget(cls, *args, **kwargs):
+ widget = cls(*args, **kwargs)
+ widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ widget.show()
+ qapp_utils.qWaitForWindowExposed(widget)
+ widgets.append(widget)
+
+ return widget
+
+ yield createWidget
+
+ qapp.processEvents()
+
+ for widget in widgets:
+ if isValid(widget):
+ widget.close()
+ qapp.processEvents()
+
+ # Wait some time for all widgets to be deleted
+ for _ in range(10):
+ validWidgets = [widget for widget in widgets if isValid(widget)]
+ if validWidgets:
+ qapp_utils.qWait(10)
+
+ validWidgets = [widget for widget in widgets if isValid(widget)]
+ assert not validWidgets, f"Some widgets were not destroyed: {validWidgets}"
diff --git a/src/silx/gui/console.py b/src/silx/gui/console.py
index 953b6a1..df0e36c 100644
--- a/src/silx/gui/console.py
+++ b/src/silx/gui/console.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2022 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
@@ -88,15 +87,16 @@ else:
raise ImportError(msg)
try:
- from qtconsole.rich_jupyter_widget import RichJupyterWidget as \
- _RichJupyterWidget
+ from qtconsole.rich_jupyter_widget import RichJupyterWidget as _RichJupyterWidget
except ImportError:
try:
- from qtconsole.rich_ipython_widget import RichJupyterWidget as \
- _RichJupyterWidget
+ from qtconsole.rich_ipython_widget import (
+ RichJupyterWidget as _RichJupyterWidget,
+ )
except ImportError:
- from qtconsole.rich_ipython_widget import RichIPythonWidget as \
- _RichJupyterWidget
+ from qtconsole.rich_ipython_widget import (
+ RichIPythonWidget as _RichJupyterWidget,
+ )
from qtconsole.inprocess import QtInProcessKernelManager
@@ -127,11 +127,15 @@ class IPythonWidget(_RichJupyterWidget):
# Monkey-patch to workaround issue:
# https://github.com/ipython/ipykernel/issues/370
- if (_ipykernel_version_info is not None and
- _ipykernel_version_info[0] > 4 and
- _ipykernel_version_info[:3] <= (5, 1, 0)):
+ if (
+ _ipykernel_version_info is not None
+ and _ipykernel_version_info[0] > 4
+ and _ipykernel_version_info[:3] <= (5, 1, 0)
+ ):
+
def _abort_queues(*args, **kwargs):
pass
+
kernel_manager.kernel._abort_queues = _abort_queues
self.kernel_client = kernel_client = self._kernel_manager.client()
@@ -140,6 +144,7 @@ class IPythonWidget(_RichJupyterWidget):
def stop():
kernel_client.stop_channels()
kernel_manager.shutdown_kernel()
+
self.exit_requested.connect(stop)
def sizeHint(self):
@@ -147,7 +152,7 @@ class IPythonWidget(_RichJupyterWidget):
return qt.QSize(500, 300)
def pushVariables(self, variable_dict):
- """ Given a dictionary containing name / value pairs, push those
+ """Given a dictionary containing name / value pairs, push those
variables to the IPython console widget.
:param variable_dict: Dictionary of variables to be pushed to the
@@ -170,8 +175,10 @@ class IPythonDockWidget(qt.QDockWidget):
:param parent: Parent :class:`qt.QMainWindow` containing this
:class:`qt.QDockWidget`
"""
- def __init__(self, parent=None, available_vars=None, custom_banner=None,
- title="Console"):
+
+ def __init__(
+ self, parent=None, available_vars=None, custom_banner=None, title="Console"
+ ):
super(IPythonDockWidget, self).__init__(title, parent)
self.ipyconsole = IPythonWidget(custom_banner=custom_banner)
@@ -182,13 +189,6 @@ class IPythonDockWidget(qt.QDockWidget):
if available_vars is not None:
self.ipyconsole.pushVariables(available_vars)
- def showEvent(self, event):
- """Make sure this widget is raised when it is shown
- (when it is first created as a tab in PlotWindow or when it is shown
- again after hiding).
- """
- self.raise_()
-
def main():
"""Run a Qt app with an IPython console"""
@@ -198,5 +198,5 @@ def main():
app.exec()
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/src/silx/gui/fit/setup.py b/src/silx/gui/constants.py
index 6672363..cc8b45e 100644
--- a/src/silx/gui/fit/setup.py
+++ b/src/silx/gui/constants.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,22 +21,7 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "21/07/2016"
+"""Constants related to silx GUI"""
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('fit', parent_package, top_path)
- config.add_subpackage('test')
-
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
-
- setup(configuration=configuration)
+SILX_URI_MIMETYPE = "application/x-silx-uri"
+"""Used by silx to share data URL between application"""
diff --git a/src/silx/gui/data/ArrayTableModel.py b/src/silx/gui/data/ArrayTableModel.py
index 23b0bb2..2de0f05 100644
--- a/src/silx/gui/data/ArrayTableModel.py
+++ b/src/silx/gui/data/ArrayTableModel.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +25,6 @@
This module defines a data model for displaying and editing arrays of any
number of dimensions in a table view.
"""
-from __future__ import division
import numpy
import logging
from silx.gui import qt
@@ -34,7 +32,7 @@ from silx.gui.data.TextFormatter import TextFormatter
__authors__ = ["V.A. Sole"]
__license__ = "MIT"
-__date__ = "27/09/2017"
+__date__ = "18/01/2022"
_logger = logging.getLogger(__name__)
@@ -75,7 +73,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
of :meth:`setPerspective`.
"""
- MAX_NUMBER_OF_SECTIONS = 10e6
+ MAX_NUMBER_OF_SECTIONS = 10000000
"""Maximum number of displayed rows and columns"""
def __init__(self, parent=None, data=None, perspective=None):
@@ -194,8 +192,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
dim = self._getRowDim()
else:
dim = self._getColumnDim()
- return (dim is not None and
- self._array.shape[dim] > self.MAX_NUMBER_OF_SECTIONS)
+ return dim is not None and self._array.shape[dim] > self.MAX_NUMBER_OF_SECTIONS
def __isClippedIndex(self, index) -> bool:
"""Returns whether or not index's cell represents clipped data."""
@@ -226,17 +223,23 @@ class ArrayTableModel(qt.QAbstractTableModel):
row, column = index.row(), index.column()
# When clipped, display last data of the array in last column of the table
- if (self.__isClipped(qt.Qt.Vertical) and
- row == self.MAX_NUMBER_OF_SECTIONS - 1):
+ if (
+ self.__isClipped(qt.Qt.Vertical)
+ and row == self.MAX_NUMBER_OF_SECTIONS - 1
+ ):
row = self._array.shape[self._getRowDim()] - 1
- if (self.__isClipped(qt.Qt.Horizontal) and
- column == self.MAX_NUMBER_OF_SECTIONS - 1):
+ if (
+ self.__isClipped(qt.Qt.Horizontal)
+ and column == self.MAX_NUMBER_OF_SECTIONS - 1
+ ):
column = self._array.shape[self._getColumnDim()] - 1
selection = self._getIndexTuple(row, column)
- if role == qt.Qt.DisplayRole:
- return self._formatter.toString(self._array[selection], self._array.dtype)
+ if role == qt.Qt.DisplayRole or role == qt.Qt.EditRole:
+ 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]
@@ -307,8 +310,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
except ValueError:
return False
- selection = self._getIndexTuple(index.row(),
- index.column())
+ selection = self._getIndexTuple(index.row(), index.column())
self._array[selection] = v
self.dataChanged.emit(index, index)
return True
@@ -316,8 +318,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
return False
# Public methods
- def setArrayData(self, data, copy=True,
- perspective=None, editable=False):
+ def setArrayData(self, data, copy=True, perspective=None, editable=False):
"""Set the data array and the viewing perspective.
You can set ``copy=False`` if you need more performances, when dealing
@@ -354,9 +355,11 @@ class ArrayTableModel(qt.QAbstractTableModel):
# 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" +
- " (this will cause the data to be copied!)")
+ raise TypeError(
+ "data is not a proper array. Try setting"
+ + " copy=True to convert it into a numpy array"
+ + " (this will cause the data to be copied!)"
+ )
# # copy not requested, but necessary
# _logger.warning(
# "data is not an array-like object. " +
@@ -368,8 +371,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
self._array = data
# reset colors to None if new data shape is inconsistent
- valid_color_shapes = (self._array.shape + (3,),
- self._array.shape + (4,))
+ valid_color_shapes = (self._array.shape + (3,), self._array.shape + (4,))
if self._bgcolors is not None:
if self._bgcolors.shape not in valid_color_shapes:
self._bgcolors = None
@@ -380,8 +382,11 @@ class ArrayTableModel(qt.QAbstractTableModel):
self.setEditable(editable)
self._index = [0 for _i in range((len(self._array.shape) - 2))]
- self._perspective = tuple(perspective) if perspective is not None else\
- tuple(range(0, len(self._array.shape) - 2))
+ self._perspective = (
+ tuple(perspective)
+ if perspective is not None
+ else tuple(range(0, len(self._array.shape) - 2))
+ )
self.endResetModel()
@@ -444,8 +449,9 @@ class ArrayTableModel(qt.QAbstractTableModel):
if hasattr(self._array.file, "mode"):
if editable and self._array.file.mode == "r":
_logger.warning(
- "Data is a HDF5 dataset open in read-only " +
- "mode. Editing must be disabled.")
+ "Data is a HDF5 dataset open in read-only "
+ + "mode. Editing must be disabled."
+ )
self._editable = False
return False
return True
@@ -491,14 +497,17 @@ class ArrayTableModel(qt.QAbstractTableModel):
else:
self._index = index
if not 0 <= self._index[0] < len_:
- raise ValueError("Index must be a positive integer " +
- "lower than %d" % len_)
+ raise ValueError(
+ "Index must be a positive integer " + "lower than %d" % len_
+ )
else:
# general n-D case
for i_, idx in enumerate(index):
if not 0 <= idx < shape[self._perspective[i_]]:
- raise IndexError("Invalid index %d " % idx +
- "not in range 0-%d" % (shape[i_] - 1))
+ raise IndexError(
+ "Invalid index %d " % idx
+ + "not in range 0-%d" % (shape[i_] - 1)
+ )
self._index = index
self.endResetModel()
@@ -530,8 +539,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
return self._formatter
def __formatChanged(self):
- """Called when the format changed.
- """
+ """Called when the format changed."""
self.reset()
def setPerspective(self, perspective):
@@ -564,8 +572,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
"""
n_dimensions = len(self._array.shape)
if n_dimensions < 3:
- _logger.warning(
- "perspective is not relevant for 1D and 2D arrays")
+ _logger.warning("perspective is not relevant for 1D and 2D arrays")
return
if not hasattr(perspective, "__len__"):
@@ -578,12 +585,18 @@ class ArrayTableModel(qt.QAbstractTableModel):
# ensure unicity of dimensions in perspective
perspective = tuple(set(perspective))
- if len(perspective) != n_dimensions - 2 or\
- min(perspective) < 0 or max(perspective) >= n_dimensions:
+ if (
+ len(perspective) != n_dimensions - 2
+ or min(perspective) < 0
+ or max(perspective) >= n_dimensions
+ ):
raise IndexError(
- "Invalid perspective " + str(perspective) +
- " for %d-D array " % n_dimensions +
- "with shape " + str(self._array.shape))
+ "Invalid perspective "
+ + str(perspective)
+ + " for %d-D array " % n_dimensions
+ + "with shape "
+ + str(self._array.shape)
+ )
self.beginResetModel()
@@ -608,24 +621,31 @@ class ArrayTableModel(qt.QAbstractTableModel):
:raise: IndexError if axes are invalid
"""
if row_axis > col_axis:
- _logger.warning("The dimension of the row axis must be lower " +
- "than the dimension of the column axis. Swapping.")
+ _logger.warning(
+ "The dimension of the row axis must be lower "
+ + "than the dimension of the column axis. Swapping."
+ )
row_axis, col_axis = min(row_axis, col_axis), max(row_axis, col_axis)
n_dimensions = len(self._array.shape)
if n_dimensions < 3:
- _logger.warning(
- "Frame axes cannot be changed for 1D and 2D arrays")
+ _logger.warning("Frame axes cannot be changed for 1D and 2D arrays")
return
perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis})
- if len(perspective) != n_dimensions - 2 or\
- min(perspective) < 0 or max(perspective) >= n_dimensions:
+ if (
+ len(perspective) != n_dimensions - 2
+ or min(perspective) < 0
+ or max(perspective) >= n_dimensions
+ ):
raise IndexError(
- "Invalid perspective " + str(perspective) +
- " for %d-D array " % n_dimensions +
- "with shape " + str(self._array.shape))
+ "Invalid perspective "
+ + str(perspective)
+ + " for %d-D array " % n_dimensions
+ + "with shape "
+ + str(self._array.shape)
+ )
self.beginResetModel()
diff --git a/src/silx/gui/data/ArrayTableWidget.py b/src/silx/gui/data/ArrayTableWidget.py
index baef5f4..882c730 100644
--- a/src/silx/gui/data/ArrayTableWidget.py
+++ b/src/silx/gui/data/ArrayTableWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -30,7 +29,6 @@ sliders.
The widget uses a TableView that relies on a custom abstract item
model: :class:`silx.gui.data.ArrayTableModel`.
"""
-from __future__ import division
import sys
from silx.gui import qt
@@ -56,6 +54,7 @@ class AxesSelector(qt.QWidget):
The two axes can be used to select the row axis and the column axis t
display a slice of the array data in a table view.
"""
+
sigDimensionsChanged = qt.Signal(int, int)
"""Signal emitted whenever one of the comboboxes is changed.
The signal carries the two selected dimensions."""
@@ -128,7 +127,9 @@ class AxesSelector(qt.QWidget):
if not (0 <= row_dim < self.n - 1):
raise IndexError("Row dimension must be between 0 and %d" % (self.n - 2))
if not (row_dim < col_dim <= self.n - 1):
- raise IndexError("Col dimension must be between %d and %d" % (row_dim + 1, self.n - 1))
+ raise IndexError(
+ "Col dimension must be between %d and %d" % (row_dim + 1, self.n - 1)
+ )
# set the rows dimension; this triggers an update of columnsCB
self.rowsCB.setCurrentIndex(row_dim)
@@ -149,8 +150,7 @@ class AxesSelector(qt.QWidget):
self.columnsCB.clear()
def _getRowDim(self):
- """Get rows dimension, selected in :attr:`rowsCB`
- """
+ """Get rows dimension, selected in :attr:`rowsCB`"""
# rows combobox contains elements "0", ..."n-2",
# so the selected dim is always equal to the index
return self.rowsCB.currentIndex()
@@ -233,6 +233,7 @@ class ArrayTableWidget(qt.QWidget):
.. image:: img/ArrayTableWidget.png
"""
+
def __init__(self, parent=None):
"""
@@ -470,6 +471,7 @@ class ArrayTableWidget(qt.QWidget):
def main():
import numpy
+
a = qt.QApplication([])
d = numpy.random.normal(0, 1, (4, 5, 1000, 1000))
for j in range(4):
@@ -488,5 +490,6 @@ def main():
w.show()
a.exec()
+
if __name__ == "__main__":
main()
diff --git a/src/silx/gui/data/DataViewer.py b/src/silx/gui/data/DataViewer.py
index 2e51439..aa522ec 100644
--- a/src/silx/gui/data/DataViewer.py
+++ b/src/silx/gui/data/DataViewer.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2022 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 +24,6 @@
"""This module defines a widget designed to display data using the most adapted
view from the ones provided by silx.
"""
-from __future__ import division
import logging
import os.path
@@ -45,9 +43,9 @@ __date__ = "12/02/2019"
_logger = logging.getLogger(__name__)
-DataSelection = collections.namedtuple("DataSelection",
- ["filename", "datapath",
- "slice", "permutation"])
+DataSelection = collections.namedtuple(
+ "DataSelection", ["filename", "datapath", "slice", "permutation"]
+)
class DataViewer(qt.QFrame):
@@ -83,6 +81,14 @@ class DataViewer(qt.QFrame):
dataChanged = qt.Signal()
"""Emitted when the data changes"""
+ selectionChanged = qt.Signal(object, object)
+ """Emitted when the data selection changes.
+
+ It provides:
+ - the slicing as a tuple of slice or None.
+ - the permutation as a tuple of int or None.
+ """
+
currentAvailableViewsChanged = qt.Signal()
"""Emitted when the current available views (which support the current
data) change"""
@@ -118,6 +124,7 @@ class DataViewer(qt.QFrame):
self.__useAxisSelection = False
self.__userSelectedView = None
self.__hooks = None
+ self.__previousSelection = DataSelection(None, None, None, None)
self.__views = []
self.__index = {}
@@ -165,7 +172,9 @@ class DataViewer(qt.QFrame):
view = viewClass(parent)
views.append(view)
except Exception:
- _logger.warning("%s instantiation failed. View is ignored" % viewClass.__name__)
+ _logger.warning(
+ "%s instantiation failed. View is ignored" % viewClass.__name__
+ )
_logger.debug("Backtrace", exc_info=True)
return views
@@ -215,19 +224,25 @@ class DataViewer(qt.QFrame):
info = self._getInfo()
axisNames = self.__currentView.axesNames(self.__data, info)
- if (info.isArray and info.size != 0 and
- self.__data is not None and axisNames is not None):
+ 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())
+ self.__currentView.customAxisNames()
+ )
data = self.normalizeData(self.__data)
self.__numpySelection.setData(data)
# Try to restore previous permutation and selection
try:
self.__numpySelection.setSelection(
- previousSelection, previousPermutation)
+ previousSelection, previousPermutation
+ )
except ValueError as e:
_logger.info("Not restoring selection because: %s", e)
@@ -270,8 +285,10 @@ class DataViewer(qt.QFrame):
except:
datapath = None
- # FIXME: maybe use DataUrl, with added support of permutation
- self.__displayedSelection = DataSelection(filename, datapath, slicing, permutation)
+ # FIXME: maybe use DataUrl, with added support of permutation
+ self.__displayedSelection = DataSelection(
+ filename, datapath, slicing, permutation
+ )
# TODO: would be good to avoid that, it should be synchonous
qt.QTimer.singleShot(10, self.__setDataInView)
@@ -280,6 +297,20 @@ class DataViewer(qt.QFrame):
self.__currentView.setData(self.__displayedData)
self.__currentView.setDataSelection(self.__displayedSelection)
+ if self.__displayedSelection is None:
+ return
+
+ # Emit signal only when selection has changed
+ if (
+ self.__previousSelection.slice != self.__displayedSelection.slice
+ or self.__previousSelection.permutation
+ != self.__displayedSelection.permutation
+ ):
+ self.selectionChanged.emit(
+ self.__displayedSelection.slice, self.__displayedSelection.permutation
+ )
+ self.__previousSelection = self.__displayedSelection
+
def setDisplayedView(self, view):
"""Set the displayed view.
diff --git a/src/silx/gui/data/DataViewerFrame.py b/src/silx/gui/data/DataViewerFrame.py
index 9bfb95b..912ca1c 100644
--- a/src/silx/gui/data/DataViewerFrame.py
+++ b/src/silx/gui/data/DataViewerFrame.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2022 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
@@ -60,6 +59,14 @@ class DataViewerFrame(qt.QWidget):
dataChanged = qt.Signal()
"""Emitted when the data changes"""
+ selectionChanged = qt.Signal(object, object)
+ """Emitted when the data selection changes.
+
+ It provides:
+ - the slicing as a tuple of slice or None.
+ - the permutation as a tuple of int or None.
+ """
+
def __init__(self, parent=None):
"""
Constructor
@@ -104,6 +111,7 @@ class DataViewerFrame(qt.QWidget):
self.__dataViewer.dataChanged.connect(self.__dataChanged)
self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged)
+ self.__dataViewer.selectionChanged.connect(self.__selectionChanged)
def __dataChanged(self):
"""Called when the data is changed"""
@@ -113,6 +121,10 @@ class DataViewerFrame(qt.QWidget):
"""Called when the displayed view changes"""
self.displayedViewChanged.emit(view)
+ def __selectionChanged(self, slices, permutation):
+ """Called when the data selection has changed"""
+ self.selectionChanged.emit(slices, permutation)
+
def setGlobalHooks(self, hooks):
"""Set a data view hooks for all the views
diff --git a/src/silx/gui/data/DataViewerSelector.py b/src/silx/gui/data/DataViewerSelector.py
index a1e9947..61a4077 100644
--- a/src/silx/gui/data/DataViewerSelector.py
+++ b/src/silx/gui/data/DataViewerSelector.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
@@ -25,7 +24,6 @@
"""This module defines a widget to be able to select the available view
of the DataViewer.
"""
-from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
@@ -120,7 +118,9 @@ class DataViewerSelector(qt.QWidget):
return
if self.__dataViewer is not None:
self.__dataViewer.dataChanged.disconnect(self.__updateButtonsVisibility)
- self.__dataViewer.displayedViewChanged.disconnect(self.__displayedViewChanged)
+ self.__dataViewer.displayedViewChanged.disconnect(
+ self.__displayedViewChanged
+ )
self.__dataViewer = dataViewer
if self.__dataViewer is not None:
self.__dataViewer.dataChanged.connect(self.__updateButtonsVisibility)
diff --git a/src/silx/gui/data/DataViews.py b/src/silx/gui/data/DataViews.py
index b18a813..ed688b8 100644
--- a/src/silx/gui/data/DataViews.py
+++ b/src/silx/gui/data/DataViews.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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,14 +24,12 @@
"""This module defines a views used by :class:`silx.gui.data.DataViewer`.
"""
-from collections import OrderedDict
import logging
import numbers
import numpy
import os
import silx.io
-from silx.utils import deprecation
from silx.gui import qt, icons
from silx.gui.data.TextFormatter import TextFormatter
from silx.io import nxdata
@@ -130,9 +127,17 @@ class DataInfo(object):
if nxd is not None:
self.hasNXdata = True
# can we plot it?
- is_scalar = nxd.signal_is_0d or nxd.interpretation in ["scalar", "scaler"]
- if not (is_scalar or nxd.is_curve or nxd.is_x_y_value_scatter or
- nxd.is_image or nxd.is_stack):
+ is_scalar = nxd.signal_is_0d or nxd.interpretation in [
+ "scalar",
+ "scaler",
+ ]
+ if not (
+ is_scalar
+ or nxd.is_curve
+ or nxd.is_x_y_value_scatter
+ or nxd.is_image
+ or nxd.is_stack
+ ):
# invalid: cannot be plotted by any widget
self.isInvalidNXdata = True
elif nx_class == "NXdata":
@@ -175,8 +180,7 @@ class DataInfo(object):
self.isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating)
self.isBoolean = numpy.issubdtype(data.dtype, numpy.bool_)
elif self.hasNXdata:
- self.isNumeric = numpy.issubdtype(nxd.signal.dtype,
- numpy.number)
+ self.isNumeric = numpy.issubdtype(nxd.signal.dtype, numpy.number)
self.isComplex = numpy.issubdtype(nxd.signal.dtype, numpy.complexfloating)
self.isBoolean = numpy.issubdtype(nxd.signal.dtype, numpy.bool_)
else:
@@ -236,6 +240,7 @@ class DataViewHooks(object):
"""Called when the widget of the view was created"""
return
+
class DataView(object):
"""Holder for the data view."""
@@ -336,13 +341,11 @@ class DataView(object):
pass
def isWidgetInitialized(self):
- """Returns true if the widget is already initialized.
- """
+ """Returns true if the widget is already initialized."""
return self.__widget is not None
def select(self):
- """Called when the view is selected to display the data.
- """
+ """Called when the view is selected to display the data."""
return
def getWidget(self):
@@ -385,20 +388,25 @@ class DataView(object):
:rtype: str
"""
if indices is None:
- return ''
+ return ""
def formatSlice(slice_):
start, stop, step = slice_.start, slice_.stop, slice_.step
- string = ('' if start is None else str(start)) + ':'
+ string = ("" if start is None else str(start)) + ":"
if stop is not None:
string += str(stop)
if step not in (None, 1):
- string += ':' + step
+ string += ":" + step
return string
- return '[' + ', '.join(
- formatSlice(index) if isinstance(index, slice) else str(index)
- for index in indices) + ']'
+ return (
+ "["
+ + ", ".join(
+ formatSlice(index) if isinstance(index, slice) else str(index)
+ for index in indices
+ )
+ + "]"
+ )
def titleForSelection(self, selection):
"""Build title from given selection information.
@@ -414,9 +422,9 @@ class DataView(object):
slicing = self.__formatSlices(selection.slice)
except Exception:
_logger.debug("Error while formatting slices", exc_info=True)
- slicing = '[sliced]'
+ slicing = "[sliced]"
- permuted = '(permuted)' if selection.permutation is not None else ''
+ permuted = "(permuted)" if selection.permutation is not None else ""
try:
title = self.TITLE_PATTERN.format(
@@ -424,7 +432,8 @@ class DataView(object):
filename=filename,
datapath=selection.datapath,
slicing=slicing,
- permuted=permuted)
+ permuted=permuted,
+ )
except Exception:
_logger.debug("Error while formatting title", exc_info=True)
title = selection.datapath + slicing
@@ -531,10 +540,6 @@ class _CompositeDataView(DataView):
"""
raise NotImplementedError()
- @deprecation.deprecated(replacement="getReachableViews", since_version="0.10")
- def availableViews(self):
- return self.getViews()
-
def isSupportedData(self, data, info):
"""If true, the composite view allow sub views to access to this data.
Else this this data is considered as not supported by any of sub views
@@ -557,7 +562,7 @@ class SelectOneDataView(_CompositeDataView):
:param qt.QWidget parent: Parent of the hold widget
"""
super(SelectOneDataView, self).__init__(parent, modeId, icon, label)
- self.__views = OrderedDict()
+ self.__views = {}
self.__currentView = None
def setHooks(self, hooks):
@@ -712,9 +717,10 @@ class SelectOneDataView(_CompositeDataView):
return False
# replace oldView with new view in dict
- self.__views = OrderedDict(
- (newView, None) if view is oldView else (view, idx) for
- view, idx in self.__views.items())
+ self.__views = dict(
+ (newView, None) if view is oldView else (view, idx)
+ for view, idx in self.__views.items()
+ )
return True
@@ -734,7 +740,9 @@ class SelectManyDataView(_CompositeDataView):
:param qt.QWidget parent: Parent of the hold widget
"""
- super(SelectManyDataView, self).__init__(parent, modeId=None, icon=None, label=None)
+ super(SelectManyDataView, self).__init__(
+ parent, modeId=None, icon=None, label=None
+ )
if views is None:
views = []
self.__views = views
@@ -777,7 +785,11 @@ class SelectManyDataView(_CompositeDataView):
"""
if not self.isSupportedData(data, info):
return []
- views = [v for v in self.__views if v.getCachedDataPriority(data, info) != DataView.UNSUPPORTED]
+ views = [
+ v
+ for v in self.__views
+ if v.getCachedDataPriority(data, info) != DataView.UNSUPPORTED
+ ]
return views
def customAxisNames(self):
@@ -871,12 +883,16 @@ class _Plot1dView(DataView):
parent=parent,
modeId=PLOT1D_MODE,
label="Curve",
- icon=icons.getQIcon("view-1d"))
+ icon=icons.getQIcon("view-1d"),
+ )
self.__resetZoomNextTime = True
def createWidget(self, parent):
from silx.gui import plot
- return plot.Plot1D(parent=parent)
+
+ widget = plot.Plot1D(parent=parent)
+ widget.setGraphGrid(True)
+ return widget
def clear(self):
self.getWidget().clear()
@@ -891,10 +907,12 @@ class _Plot1dView(DataView):
data = self.normalizeData(data)
plotWidget = self.getWidget()
legend = "data"
- plotWidget.addCurve(legend=legend,
- x=range(len(data)),
- y=data,
- resetzoom=self.__resetZoomNextTime)
+ plotWidget.addCurve(
+ legend=legend,
+ x=range(len(data)),
+ y=data,
+ resetzoom=self.__resetZoomNextTime,
+ )
plotWidget.setActiveCurve(legend)
self.__resetZoomNextTime = True
@@ -927,7 +945,8 @@ class _Plot2dRecordView(DataView):
parent=parent,
modeId=RECORD_PLOT_MODE,
label="Curve",
- icon=icons.getQIcon("view-1d"))
+ icon=icons.getQIcon("view-1d"),
+ )
self.__resetZoomNextTime = True
self._data = None
self._xAxisDropDown = None
@@ -936,6 +955,7 @@ class _Plot2dRecordView(DataView):
def createWidget(self, parent):
from ._RecordPlot import RecordPlot
+
return RecordPlot(parent=parent)
def clear(self):
@@ -951,7 +971,9 @@ class _Plot2dRecordView(DataView):
self._data = self.normalizeData(data)
all_fields = sorted(self._data.dtype.fields.items(), key=lambda e: e[1][1])
- numeric_fields = [f[0] for f in all_fields if numpy.issubdtype(f[1][0], numpy.number)]
+ numeric_fields = [
+ f[0] for f in all_fields if numpy.issubdtype(f[1][0], numpy.number)
+ ]
if numeric_fields == self.__fields: # Reuse previously selected fields
fieldNameX = self.getWidget().getXAxisFieldName()
fieldNameY = self.getWidget().getYAxisFieldName()
@@ -973,8 +995,12 @@ class _Plot2dRecordView(DataView):
self._plotData(fieldNameX, fieldNameY)
if not self._xAxisDropDown:
- self._xAxisDropDown = self.getWidget().getAxesSelectionToolBar().getXAxisDropDown()
- self._yAxisDropDown = self.getWidget().getAxesSelectionToolBar().getYAxisDropDown()
+ self._xAxisDropDown = (
+ self.getWidget().getAxesSelectionToolBar().getXAxisDropDown()
+ )
+ self._yAxisDropDown = (
+ self.getWidget().getAxesSelectionToolBar().getYAxisDropDown()
+ )
self._xAxisDropDown.activated.connect(self._onAxesSelectionChaned)
self._yAxisDropDown.activated.connect(self._onAxesSelectionChaned)
@@ -992,10 +1018,9 @@ class _Plot2dRecordView(DataView):
xdata = numpy.arange(len(ydata))
else:
xdata = self._data[fieldNameX]
- self.getWidget().addCurve(legend="data",
- x=xdata,
- y=ydata,
- resetzoom=self.__resetZoomNextTime)
+ self.getWidget().addCurve(
+ legend="data", x=xdata, y=ydata, resetzoom=self.__resetZoomNextTime
+ )
self.getWidget().setXAxisFieldName(fieldNameX)
self.getWidget().setYAxisFieldName(fieldNameY)
self.__resetZoomNextTime = True
@@ -1030,18 +1055,20 @@ class _Plot2dView(DataView):
parent=parent,
modeId=PLOT2D_MODE,
label="Image",
- icon=icons.getQIcon("view-2d"))
+ icon=icons.getQIcon("view-2d"),
+ )
self.__resetZoomNextTime = True
def createWidget(self, parent):
from silx.gui import plot
+
widget = plot.Plot2D(parent=parent)
widget.setDefaultColormap(self.defaultColormap())
- widget.getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getColormapAction().setColormapDialog(self.defaultColorDialog())
widget.getIntensityHistogramAction().setVisible(True)
widget.setKeepDataAspectRatio(True)
- widget.getXAxis().setLabel('X')
- widget.getYAxis().setLabel('Y')
+ widget.getXAxis().setLabel("X")
+ widget.getYAxis().setLabel("Y")
maskToolsWidget = widget.getMaskToolsDockWidget().widget()
maskToolsWidget.setItemMaskUpdated(True)
return widget
@@ -1057,9 +1084,9 @@ class _Plot2dView(DataView):
def setData(self, data):
data = self.normalizeData(data)
- self.getWidget().addImage(legend="data",
- data=data,
- resetzoom=self.__resetZoomNextTime)
+ self.getWidget().addImage(
+ legend="data", data=data, resetzoom=self.__resetZoomNextTime
+ )
self.__resetZoomNextTime = False
def setDataSelection(self, selection):
@@ -1071,9 +1098,7 @@ class _Plot2dView(DataView):
def getDataPriority(self, data, info):
if info.size <= 0:
return DataView.UNSUPPORTED
- if (data is None or
- not info.isArray or
- not (info.isNumeric or info.isBoolean)):
+ 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
@@ -1093,7 +1118,8 @@ class _Plot3dView(DataView):
parent=parent,
modeId=PLOT3D_MODE,
label="Cube",
- icon=icons.getQIcon("view-3d"))
+ icon=icons.getQIcon("view-3d"),
+ )
try:
from ._VolumeWindow import VolumeWindow # noqa
except ImportError:
@@ -1144,20 +1170,32 @@ class _ComplexImageView(DataView):
parent=parent,
modeId=COMPLEX_IMAGE_MODE,
label="Complex Image",
- icon=icons.getQIcon("view-2d"))
+ icon=icons.getQIcon("view-2d"),
+ )
def createWidget(self, parent):
from silx.gui.plot.ComplexImageView import ComplexImageView
+
widget = ComplexImageView(parent=parent)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.ABSOLUTE)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.SQUARE_AMPLITUDE)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.REAL)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.IMAGINARY)
- widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.setColormap(
+ self.defaultColormap(), mode=ComplexImageView.ComplexMode.ABSOLUTE
+ )
+ widget.setColormap(
+ self.defaultColormap(), mode=ComplexImageView.ComplexMode.SQUARE_AMPLITUDE
+ )
+ widget.setColormap(
+ self.defaultColormap(), mode=ComplexImageView.ComplexMode.REAL
+ )
+ widget.setColormap(
+ self.defaultColormap(), mode=ComplexImageView.ComplexMode.IMAGINARY
+ )
+ widget.getPlot().getColormapAction().setColormapDialog(
+ self.defaultColorDialog()
+ )
widget.getPlot().getIntensityHistogramAction().setVisible(True)
widget.getPlot().setKeepDataAspectRatio(True)
- widget.getXAxis().setLabel('X')
- widget.getYAxis().setLabel('Y')
+ widget.getXAxis().setLabel("X")
+ widget.getYAxis().setLabel("Y")
maskToolsWidget = widget.getPlot().getMaskToolsDockWidget().widget()
maskToolsWidget.setItemMaskUpdated(True)
return widget
@@ -1174,8 +1212,7 @@ class _ComplexImageView(DataView):
self.getWidget().setData(data)
def setDataSelection(self, selection):
- self.getWidget().getPlot().setGraphTitle(
- self.titleForSelection(selection))
+ self.getWidget().getPlot().setGraphTitle(self.titleForSelection(selection))
def axesNames(self, data, info):
return ["y", "x"]
@@ -1203,6 +1240,7 @@ class _ArrayView(DataView):
def createWidget(self, parent):
from silx.gui.data.ArrayTableWidget import ArrayTableWidget
+
widget = ArrayTableWidget(parent)
widget.displayAxesSelector(False)
return widget
@@ -1237,7 +1275,8 @@ class _StackView(DataView):
parent=parent,
modeId=STACK_MODE,
label="Image stack",
- icon=icons.getQIcon("view-2d-stack"))
+ icon=icons.getQIcon("view-2d-stack"),
+ )
self.__resetZoomNextTime = True
def customAxisNames(self):
@@ -1251,9 +1290,12 @@ class _StackView(DataView):
def createWidget(self, parent):
from silx.gui import plot
+
widget = plot.StackView(parent=parent)
widget.setColormap(self.defaultColormap())
- widget.getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getPlotWidget().getColormapAction().setColormapDialog(
+ self.defaultColorDialog()
+ )
widget.setKeepDataAspectRatio(True)
widget.setLabels(self.axesNames(None, None))
# hide default option panel
@@ -1280,8 +1322,7 @@ class _StackView(DataView):
def setDataSelection(self, selection):
title = self.titleForSelection(selection)
- self.getWidget().setTitleCallback(
- lambda idx: "%s z=%d" % (title, idx))
+ self.getWidget().setTitleCallback(lambda idx: "%s z=%d" % (title, idx))
def axesNames(self, data, info):
return ["depth", "y", "x"]
@@ -1349,6 +1390,7 @@ class _RecordView(DataView):
def createWidget(self, parent):
from .RecordTableView import RecordTableView
+
widget = RecordTableView(parent)
widget.setWordWrap(False)
return widget
@@ -1393,6 +1435,7 @@ class _HexaView(DataView):
def createWidget(self, parent):
from .HexaTableView import HexaTableView
+
widget = HexaTableView(parent)
return widget
@@ -1423,10 +1466,12 @@ class _Hdf5View(DataView):
parent=parent,
modeId=HDF5_MODE,
label="HDF5",
- icon=icons.getQIcon("view-hdf5"))
+ icon=icons.getQIcon("view-hdf5"),
+ )
def createWidget(self, parent):
from .Hdf5TableView import Hdf5TableView
+
widget = Hdf5TableView(parent)
return widget
@@ -1458,10 +1503,8 @@ class _RawView(CompositeDataView):
def __init__(self, parent):
super(_RawView, self).__init__(
- parent=parent,
- modeId=RAW_MODE,
- label="Raw",
- icon=icons.getQIcon("view-raw"))
+ parent=parent, modeId=RAW_MODE, label="Raw", icon=icons.getQIcon("view-raw")
+ )
self.addView(_HexaView(parent))
self.addView(_ScalarView(parent))
self.addView(_ArrayView(parent))
@@ -1479,7 +1522,8 @@ class _ImageView(CompositeDataView):
parent=parent,
modeId=IMAGE_MODE,
label="Image",
- icon=icons.getQIcon("view-2d"))
+ icon=icons.getQIcon("view-2d"),
+ )
self.addView(_ComplexImageView(parent))
self.addView(_Plot2dView(parent))
@@ -1488,9 +1532,9 @@ class _InvalidNXdataView(DataView):
"""DataView showing a simple label with an error message
to inform that a group with @NX_class=NXdata cannot be
interpreted by any NXDataview."""
+
def __init__(self, parent):
- DataView.__init__(self, parent,
- modeId=NXDATA_INVALID_MODE)
+ DataView.__init__(self, parent, modeId=NXDATA_INVALID_MODE)
self._msg = ""
def createWidget(self, parent):
@@ -1531,8 +1575,10 @@ class _InvalidNXdataView(DataView):
self._msg += "@default attribute, "
if default_nxdata_name not in default_entry:
self._msg += " but no corresponding NXdata group exists."
- elif get_attr_as_unicode(default_entry[default_nxdata_name],
- "NX_class") != "NXdata":
+ elif (
+ get_attr_as_unicode(default_entry[default_nxdata_name], "NX_class")
+ != "NXdata"
+ ):
self._msg += " but the corresponding item is not a "
self._msg += "NXdata group."
else:
@@ -1543,7 +1589,10 @@ class _InvalidNXdataView(DataView):
default_nxdata_name = data.attrs["default"]
if default_nxdata_name not in data:
self._msg += " but no corresponding NXdata group exists."
- elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata":
+ elif (
+ get_attr_as_unicode(data[default_nxdata_name], "NX_class")
+ != "NXdata"
+ ):
self._msg += " but the corresponding item is not a "
self._msg += "NXdata group."
else:
@@ -1563,18 +1612,20 @@ class _NXdataBaseDataView(DataView):
cmap_norm = nxdata.plot_style.signal_scale_type
if cmap_norm is not None:
self.defaultColormap().setNormalization(
- 'log' if cmap_norm == 'log' else 'linear')
+ "log" if cmap_norm == "log" else "linear"
+ )
class _NXdataScalarView(_NXdataBaseDataView):
"""DataView using a table view for displaying NXdata scalars:
0-D signal or n-D signal with *@interpretation=scalar*"""
+
def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_SCALAR_MODE)
+ _NXdataBaseDataView.__init__(self, parent, modeId=NXDATA_SCALAR_MODE)
def createWidget(self, parent):
from silx.gui.data.ArrayTableWidget import ArrayTableWidget
+
widget = ArrayTableWidget(parent)
# widget.displayAxesSelector(False)
return widget
@@ -1583,16 +1634,14 @@ class _NXdataScalarView(_NXdataBaseDataView):
return ["col", "row"]
def clear(self):
- self.getWidget().setArrayData(numpy.array([[]]),
- labels=True)
+ self.getWidget().setArrayData(numpy.array([[]]), labels=True)
def setData(self, data):
data = self.normalizeData(data)
# data could be a NXdata or an NXentry
nxd = nxdata.get_default(data, validate=False)
signal = nxd.signal
- self.getWidget().setArrayData(signal,
- labels=True)
+ self.getWidget().setArrayData(signal, labels=True)
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -1611,12 +1660,13 @@ class _NXdataCurveView(_NXdataBaseDataView):
It also handles basic scatter plots:
a 1-D signal with one axis whose values are not monotonically increasing.
"""
+
def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_CURVE_MODE)
+ _NXdataBaseDataView.__init__(self, parent, modeId=NXDATA_CURVE_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayCurvePlot
+
widget = ArrayCurvePlot(parent)
return widget
@@ -1636,24 +1686,17 @@ class _NXdataCurveView(_NXdataBaseDataView):
else:
x_errors = None
- # this fix is necessary until the next release of PyMca (5.2.3 or 5.3.0)
- # see https://github.com/vasole/pymca/issues/144 and https://github.com/vasole/pymca/pull/145
- if not hasattr(self.getWidget(), "setCurvesData") and \
- hasattr(self.getWidget(), "setCurveData"):
- _logger.warning("Using deprecated ArrayCurvePlot API, "
- "without support of auxiliary signals")
- self.getWidget().setCurveData(nxd.signal, nxd.axes[-1],
- yerror=nxd.errors, xerror=x_errors,
- ylabel=nxd.signal_name, xlabel=nxd.axes_names[-1],
- title=nxd.title or nxd.signal_name)
- return
-
- self.getWidget().setCurvesData([nxd.signal] + nxd.auxiliary_signals, nxd.axes[-1],
- yerror=nxd.errors, xerror=x_errors,
- ylabels=signals_names, xlabel=nxd.axes_names[-1],
- title=nxd.title or signals_names[0],
- xscale=nxd.plot_style.axes_scale_types[-1],
- yscale=nxd.plot_style.signal_scale_type)
+ self.getWidget().setCurvesData(
+ [nxd.signal] + nxd.auxiliary_signals,
+ nxd.axes[-1],
+ yerror=nxd.errors,
+ xerror=x_errors,
+ ylabels=signals_names,
+ xlabel=nxd.axes_names[-1],
+ title=nxd.title or signals_names[0],
+ xscale=nxd.plot_style.axes_scale_types[-1],
+ yscale=nxd.plot_style.signal_scale_type,
+ )
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -1666,16 +1709,18 @@ class _NXdataCurveView(_NXdataBaseDataView):
class _NXdataXYVScatterView(_NXdataBaseDataView):
"""DataView using a Plot1D for displaying NXdata 3D scatters as
a scatter of coloured points (1-D signal with 2 axes)"""
+
def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_XYVSCATTER_MODE)
+ _NXdataBaseDataView.__init__(self, parent, modeId=NXDATA_XYVSCATTER_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import XYVScatterPlot
+
widget = XYVScatterPlot(parent)
widget.getScatterView().setColormap(self.defaultColormap())
- widget.getScatterView().getScatterToolBar().getColormapAction().setColorDialog(
- self.defaultColorDialog())
+ widget.getScatterView().getScatterToolBar().getColormapAction().setColormapDialog(
+ self.defaultColorDialog()
+ )
return widget
def axesNames(self, data, info):
@@ -1708,13 +1753,19 @@ class _NXdataXYVScatterView(_NXdataBaseDataView):
self._updateColormap(nxd)
- self.getWidget().setScattersData(y_axis, x_axis, values=[nxd.signal] + nxd.auxiliary_signals,
- yerror=y_errors, xerror=x_errors,
- ylabel=y_label, xlabel=x_label,
- title=nxd.title,
- scatter_titles=[nxd.signal_name] + nxd.auxiliary_signals_names,
- xscale=nxd.plot_style.axes_scale_types[-2],
- yscale=nxd.plot_style.axes_scale_types[-1])
+ self.getWidget().setScattersData(
+ y_axis,
+ x_axis,
+ values=[nxd.signal] + nxd.auxiliary_signals,
+ yerror=y_errors,
+ xerror=x_errors,
+ ylabel=y_label,
+ xlabel=x_label,
+ title=nxd.title,
+ scatter_titles=[nxd.signal_name] + nxd.auxiliary_signals_names,
+ xscale=nxd.plot_style.axes_scale_types[-2],
+ yscale=nxd.plot_style.axes_scale_types[-1],
+ )
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -1729,15 +1780,18 @@ class _NXdataXYVScatterView(_NXdataBaseDataView):
class _NXdataImageView(_NXdataBaseDataView):
"""DataView using a Plot2D for displaying NXdata images:
2-D signal or n-D signals with *@interpretation=image*."""
+
def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_IMAGE_MODE)
+ _NXdataBaseDataView.__init__(self, parent, modeId=NXDATA_IMAGE_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayImagePlot
+
widget = ArrayImagePlot(parent)
widget.getPlot().setDefaultColormap(self.defaultColormap())
- widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getPlot().getColormapAction().setColormapDialog(
+ self.defaultColorDialog()
+ )
return widget
def axesNames(self, data, info):
@@ -1759,14 +1813,22 @@ class _NXdataImageView(_NXdataBaseDataView):
y_axis, x_axis = nxd.axes[img_slicing]
y_label, x_label = nxd.axes_names[img_slicing]
y_scale, x_scale = nxd.plot_style.axes_scale_types[img_slicing]
+ x_units = get_attr_as_unicode(x_axis, "units") if x_axis else None
+ y_units = get_attr_as_unicode(y_axis, "units") if y_axis else None
self.getWidget().setImageData(
[nxd.signal] + nxd.auxiliary_signals,
- x_axis=x_axis, y_axis=y_axis,
+ x_axis=x_axis,
+ y_axis=y_axis,
signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
- xlabel=x_label, ylabel=y_label,
- title=nxd.title, isRgba=isRgba,
- xscale=x_scale, yscale=y_scale)
+ xlabel=x_label,
+ ylabel=y_label,
+ title=nxd.title,
+ isRgba=isRgba,
+ xscale=x_scale,
+ yscale=y_scale,
+ keep_ratio=(x_units == y_units),
+ )
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -1781,14 +1843,17 @@ class _NXdataImageView(_NXdataBaseDataView):
class _NXdataComplexImageView(_NXdataBaseDataView):
"""DataView using a ComplexImageView for displaying NXdata complex images:
2-D signal or n-D signals with *@interpretation=image*."""
+
def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_IMAGE_MODE)
+ _NXdataBaseDataView.__init__(self, parent, modeId=NXDATA_IMAGE_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
+
widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap())
- widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getPlot().getColormapAction().setColormapDialog(
+ self.defaultColorDialog()
+ )
return widget
def clear(self):
@@ -1804,13 +1869,19 @@ class _NXdataComplexImageView(_NXdataBaseDataView):
img_slicing = slice(-2, None)
y_axis, x_axis = nxd.axes[img_slicing]
y_label, x_label = nxd.axes_names[img_slicing]
+ x_units = get_attr_as_unicode(x_axis, "units") if x_axis else None
+ y_units = get_attr_as_unicode(y_axis, "units") if y_axis else None
self.getWidget().setImageData(
[nxd.signal] + nxd.auxiliary_signals,
- x_axis=x_axis, y_axis=y_axis,
+ x_axis=x_axis,
+ y_axis=y_axis,
signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
- xlabel=x_label, ylabel=y_label,
- title=nxd.title)
+ xlabel=x_label,
+ ylabel=y_label,
+ title=nxd.title,
+ keep_ratio=(x_units == y_units),
+ )
def axesNames(self, data, info):
# disabled (used by default axis selector widget in Hdf5Viewer)
@@ -1829,14 +1900,16 @@ class _NXdataComplexImageView(_NXdataBaseDataView):
class _NXdataStackView(_NXdataBaseDataView):
def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_STACK_MODE)
+ _NXdataBaseDataView.__init__(self, parent, modeId=NXDATA_STACK_MODE)
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayStackPlot
+
widget = ArrayStackPlot(parent)
widget.getStackView().setColormap(self.defaultColormap())
- widget.getStackView().getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getStackView().getPlotWidget().getColormapAction().setColormapDialog(
+ self.defaultColorDialog()
+ )
return widget
def axesNames(self, data, info):
@@ -1858,10 +1931,16 @@ class _NXdataStackView(_NXdataBaseDataView):
widget = self.getWidget()
widget.setStackData(
- nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
- signal_name=signal_name,
- xlabel=x_label, ylabel=y_label, zlabel=z_label,
- title=title)
+ nxd.signal,
+ x_axis=x_axis,
+ y_axis=y_axis,
+ z_axis=z_axis,
+ signal_name=signal_name,
+ xlabel=x_label,
+ ylabel=y_label,
+ zlabel=z_label,
+ title=title,
+ )
# Override the colormap, while setStack overwrite it
widget.getStackView().setColormap(self.defaultColormap())
@@ -1877,10 +1956,12 @@ class _NXdataStackView(_NXdataBaseDataView):
class _NXdataVolumeView(_NXdataBaseDataView):
def __init__(self, parent):
_NXdataBaseDataView.__init__(
- self, parent,
+ self,
+ parent,
label="NXdata (3D)",
icon=icons.getQIcon("view-nexus"),
- modeId=NXDATA_VOLUME_MODE)
+ modeId=NXDATA_VOLUME_MODE,
+ )
try:
import silx.gui.plot3d # noqa
except ImportError:
@@ -1895,6 +1976,7 @@ class _NXdataVolumeView(_NXdataBaseDataView):
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayVolumePlot
+
widget = ArrayVolumePlot(parent)
return widget
@@ -1915,10 +1997,16 @@ class _NXdataVolumeView(_NXdataBaseDataView):
widget = self.getWidget()
widget.setData(
- nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
+ nxd.signal,
+ x_axis=x_axis,
+ y_axis=y_axis,
+ z_axis=z_axis,
signal_name=signal_name,
- xlabel=x_label, ylabel=y_label, zlabel=z_label,
- title=title)
+ xlabel=x_label,
+ ylabel=y_label,
+ zlabel=z_label,
+ title=title,
+ )
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -1932,16 +2020,21 @@ class _NXdataVolumeView(_NXdataBaseDataView):
class _NXdataVolumeAsStackView(_NXdataBaseDataView):
def __init__(self, parent):
_NXdataBaseDataView.__init__(
- self, parent,
+ self,
+ parent,
label="NXdata (2D)",
icon=icons.getQIcon("view-nexus"),
- modeId=NXDATA_VOLUME_AS_STACK_MODE)
+ modeId=NXDATA_VOLUME_AS_STACK_MODE,
+ )
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayStackPlot
+
widget = ArrayStackPlot(parent)
widget.getStackView().setColormap(self.defaultColormap())
- widget.getStackView().getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getStackView().getPlotWidget().getColormapAction().setColormapDialog(
+ self.defaultColorDialog()
+ )
return widget
def axesNames(self, data, info):
@@ -1963,10 +2056,16 @@ class _NXdataVolumeAsStackView(_NXdataBaseDataView):
widget = self.getWidget()
widget.setStackData(
- nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
- signal_name=signal_name,
- xlabel=x_label, ylabel=y_label, zlabel=z_label,
- title=title)
+ nxd.signal,
+ x_axis=x_axis,
+ y_axis=y_axis,
+ z_axis=z_axis,
+ signal_name=signal_name,
+ xlabel=x_label,
+ ylabel=y_label,
+ zlabel=z_label,
+ title=title,
+ )
# Override the colormap, while setStack overwrite it
widget.getStackView().setColormap(self.defaultColormap())
@@ -1980,19 +2079,25 @@ class _NXdataVolumeAsStackView(_NXdataBaseDataView):
return DataView.UNSUPPORTED
+
class _NXdataComplexVolumeAsStackView(_NXdataBaseDataView):
def __init__(self, parent):
_NXdataBaseDataView.__init__(
- self, parent,
+ self,
+ parent,
label="NXdata (2D)",
icon=icons.getQIcon("view-nexus"),
- modeId=NXDATA_VOLUME_AS_STACK_MODE)
+ modeId=NXDATA_VOLUME_AS_STACK_MODE,
+ )
self._is_complex_data = False
def createWidget(self, parent):
from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
+
widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap())
- widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ widget.getPlot().getColormapAction().setColormapDialog(
+ self.defaultColorDialog()
+ )
return widget
def axesNames(self, data, info):
@@ -2014,9 +2119,13 @@ class _NXdataComplexVolumeAsStackView(_NXdataBaseDataView):
self.getWidget().setImageData(
[nxd.signal] + nxd.auxiliary_signals,
- x_axis=x_axis, y_axis=y_axis,
+ x_axis=x_axis,
+ y_axis=y_axis,
signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
- xlabel=x_label, ylabel=y_label, title=nxd.title)
+ xlabel=x_label,
+ ylabel=y_label,
+ title=nxd.title,
+ )
def getDataPriority(self, data, info):
data = self.normalizeData(data)
@@ -2032,12 +2141,14 @@ class _NXdataComplexVolumeAsStackView(_NXdataBaseDataView):
class _NXdataView(CompositeDataView):
"""Composite view displaying NXdata groups using the most adequate
widget depending on the dimensionality."""
+
def __init__(self, parent):
super(_NXdataView, self).__init__(
parent=parent,
label="NXdata",
modeId=NXDATA_MODE,
- icon=icons.getQIcon("view-nexus"))
+ icon=icons.getQIcon("view-nexus"),
+ )
self.addView(_InvalidNXdataView(parent))
self.addView(_NXdataScalarView(parent))
diff --git a/src/silx/gui/data/Hdf5TableView.py b/src/silx/gui/data/Hdf5TableView.py
index 9d65a84..bb14768 100644
--- a/src/silx/gui/data/Hdf5TableView.py
+++ b/src/silx/gui/data/Hdf5TableView.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,13 +25,11 @@
This module define model and widget to display 1D slices from numpy
array using compound data types or hdf5 databases.
"""
-from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "12/02/2019"
-import collections
import functools
import os.path
import logging
@@ -41,6 +38,7 @@ import numpy
from silx.gui import qt
import silx.io
+from silx.io import h5link_utils
from .TextFormatter import TextFormatter
import silx.gui.hdf5
from silx.gui.widgets import HierarchicalTableView
@@ -52,8 +50,8 @@ _logger = logging.getLogger(__name__)
class _CellData(object):
- """Store a table item
- """
+ """Store a table item"""
+
def __init__(self, value=None, isHeader=False, span=None, tooltip=None):
"""
Constructor
@@ -75,8 +73,7 @@ class _CellData(object):
return self.__isHeader
def value(self):
- """Returns the value of the item.
- """
+ """Returns the value of the item."""
return self.__value
def span(self):
@@ -189,10 +186,18 @@ class _CellFilterAvailableData(_CellData):
_states = {
True: ("Available", qt.QColor(0x000000), None, None),
- False: ("Not available", qt.QColor(0xFFFFFF), qt.QColor(0xFF0000),
- "You have to install this filter on your system to be able to read this dataset"),
- "na": ("n.a.", qt.QColor(0x000000), None,
- "This version of h5py/hdf5 is not able to display the information"),
+ False: (
+ "Not available",
+ qt.QColor(0xFFFFFF),
+ qt.QColor(0xFF0000),
+ "You have to install this filter on your system to be able to read this dataset",
+ ),
+ "na": (
+ "n.a.",
+ qt.QColor(0x000000),
+ None,
+ "This version of h5py/hdf5 is not able to display the information",
+ ),
}
def __init__(self, filterId):
@@ -311,7 +316,9 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
if h5pyObject is None or self.isSupportedObject(h5pyObject):
self.__obj = h5pyObject
else:
- _logger.warning("Object class %s unsupported. Object ignored.", type(h5pyObject))
+ _logger.warning(
+ "Object class %s unsupported. Object ignored.", type(h5pyObject)
+ )
self.__initProperties()
self.endResetModel()
@@ -321,10 +328,12 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
return self.__hdf5Formatter.humanReadableHdf5Type(dataset)
def __attributeTooltip(self, attribute):
- attributeDict = collections.OrderedDict()
+ attributeDict = {}
if hasattr(attribute, "shape"):
attributeDict["Shape"] = self.__hdf5Formatter.humanReadableShape(attribute)
- attributeDict["Data type"] = self.__hdf5Formatter.humanReadableType(attribute, full=True)
+ attributeDict["Data type"] = self.__hdf5Formatter.humanReadableType(
+ attribute, full=True
+ )
html = htmlFromDict(attributeDict, title="HDF5 Attribute")
return html
@@ -338,7 +347,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
return self.__hdf5Formatter.humanReadableShape(dataset)
size = dataset.size
shape = self.__hdf5Formatter.humanReadableShape(dataset)
- return u"%s = %s" % (shape, size)
+ return "%s = %s" % (shape, size)
def __formatChunks(self, dataset):
"""Format the shape"""
@@ -346,7 +355,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
if chunks is None:
return ""
shape = " \u00D7 ".join([str(i) for i in chunks])
- sizes = numpy.product(chunks)
+ sizes = numpy.prod(chunks)
text = "%s = %s" % (shape, sizes)
return text
@@ -385,7 +394,9 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.__data.addHeaderValueRow("Local", local)
else:
# it's a real H5py object
- self.__data.addHeaderValueRow("Basename", lambda x: os.path.basename(x.name))
+ self.__data.addHeaderValueRow(
+ "Basename", lambda x: os.path.basename(x.name)
+ )
self.__data.addHeaderValueRow("Name", lambda x: x.name)
if obj.file is not None:
self.__data.addHeaderValueRow("File", lambda x: x.file.filename)
@@ -401,25 +412,10 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
showPhysicalLocation = False
# External data (nothing to do with external links)
- nExtSources = 0
- firstExtSource = None
- extType = None
- if silx.io.is_dataset(hdf5obj):
- if hasattr(hdf5obj, "is_virtual"):
- if hdf5obj.is_virtual:
- extSources = hdf5obj.virtual_sources()
- if extSources:
- firstExtSource = extSources[0].file_name + SEPARATOR + extSources[0].dset_name
- extType = "Virtual"
- nExtSources = len(extSources)
- if hasattr(hdf5obj, "external"):
- extSources = hdf5obj.external
- if extSources:
- firstExtSource = extSources[0][0]
- extType = "Raw"
- nExtSources = len(extSources)
+ external_dataset_info = h5link_utils.external_dataset_info(hdf5obj)
if showPhysicalLocation:
+
def _physical_location(x):
if isinstance(obj, silx.gui.hdf5.H5Node):
return x.physical_filename + SEPARATOR + x.physical_name
@@ -433,33 +429,15 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.__data.addHeaderValueRow("Physical", _physical_location)
- if extType:
- def _first_source(x):
- # Absolute path
- if os.path.isabs(firstExtSource):
- return firstExtSource
-
- # Relative path with respect to the file directory
- if isinstance(obj, silx.gui.hdf5.H5Node):
- filename = x.physical_filename
- elif silx.io.is_file(obj):
- filename = x.filename
- elif obj.file is not None:
- filename = x.file.filename
- else:
- return firstExtSource
-
- if firstExtSource[0] == ".":
- firstExtSource.pop(0)
- return os.path.join(os.path.dirname(filename), firstExtSource)
-
+ if external_dataset_info is not None:
self.__data.addHeaderRow(headerLabel="External sources")
- self.__data.addHeaderValueRow("Type", extType)
- self.__data.addHeaderValueRow("Count", str(nExtSources))
- self.__data.addHeaderValueRow("First", _first_source)
+ self.__data.addHeaderValueRow("Type", external_dataset_info.type)
+ self.__data.addHeaderValueRow("Count", external_dataset_info.nfiles)
+ self.__data.addHeaderValueRow(
+ "First", external_dataset_info.first_source_url
+ )
if hasattr(obj, "dtype"):
-
self.__data.addHeaderRow(headerLabel="Data info")
if hasattr(obj, "id") and hasattr(obj.id, "get_type"):
@@ -501,10 +479,14 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.__data.addHeaderRow(headerLabel="Attributes")
for key in sorted(obj.attrs.keys()):
callback = lambda key, x: self.__formatter.toString(x.attrs[key])
- callbackTooltip = lambda key, x: self.__attributeTooltip(x.attrs[key])
- self.__data.addHeaderValueRow(headerLabel=key,
- value=functools.partial(callback, key),
- tooltip=functools.partial(callbackTooltip, key))
+ callbackTooltip = lambda key, x: self.__attributeTooltip(
+ x.attrs[key]
+ )
+ self.__data.addHeaderValueRow(
+ headerLabel=key,
+ value=functools.partial(callback, key),
+ tooltip=functools.partial(callbackTooltip, key),
+ )
def __getFilterInfo(self, dataset, filterIndex):
"""Get a tuple of readable info from dataset filters
@@ -559,8 +541,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
return self.__formatter
def __formatChanged(self):
- """Called when the format changed.
- """
+ """Called when the format changed."""
self.reset()
@@ -629,6 +610,11 @@ class Hdf5TableView(HierarchicalTableView.HierarchicalTableView):
for row in range(model.rowCount()):
for column in range(model.columnCount()):
index = model.index(row, column)
- if (index.isValid() and index.data(
- HierarchicalTableView.HierarchicalTableModel.IsHeaderRole) is False):
+ if (
+ index.isValid()
+ and index.data(
+ HierarchicalTableView.HierarchicalTableModel.IsHeaderRole
+ )
+ is False
+ ):
self.openPersistentEditor(index)
diff --git a/src/silx/gui/data/HexaTableView.py b/src/silx/gui/data/HexaTableView.py
index 9e00a7b..f50bf88 100644
--- a/src/silx/gui/data/HexaTableView.py
+++ b/src/silx/gui/data/HexaTableView.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,9 +25,6 @@
This module defines model and widget to display raw data using an
hexadecimal viewer.
"""
-from __future__ import division
-
-import collections
import numpy
@@ -48,7 +44,7 @@ class _VoidConnector(object):
"""
def __init__(self, data):
- self.__cache = collections.OrderedDict()
+ self.__cache = {}
self.__len = data.itemsize
self.__data = data
@@ -57,10 +53,10 @@ class _VoidConnector(object):
pos = bufferId << 10
data = self.__data
if hasattr(data, "tobytes"):
- data = data.tobytes()[pos:pos + 1024]
+ data = data.tobytes()[pos : pos + 1024]
else:
# Old fashion
- data = data.data[pos:pos + 1024]
+ data = data.data[pos : pos + 1024]
self.__cache[bufferId] = data
if len(self.__cache) > 32:
@@ -100,6 +96,7 @@ class HexaTableModel(qt.QAbstractTableModel):
: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)
@@ -138,7 +135,7 @@ class HexaTableModel(qt.QAbstractTableModel):
if role == qt.Qt.DisplayRole:
if column == 0x10:
- start = (row << 4)
+ start = row << 4
text = ""
for i in range(0x10):
pos = start + i
@@ -238,6 +235,7 @@ class HexaTableView(qt.QTableView):
It customs the column size to provide a better layout.
"""
+
def __init__(self, parent=None):
"""
Constructor
diff --git a/src/silx/gui/data/NXdataWidgets.py b/src/silx/gui/data/NXdataWidgets.py
index 54ea287..a2bab7a 100644
--- a/src/silx/gui/data/NXdataWidgets.py
+++ b/src/silx/gui/data/NXdataWidgets.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -60,6 +59,7 @@ class ArrayCurvePlot(qt.QWidget):
This widget also handles simple 2D or 3D scatter plots (third dimension
displayed as colour of points).
"""
+
def __init__(self, parent=None):
"""
@@ -76,6 +76,7 @@ class ArrayCurvePlot(qt.QWidget):
self.__values = None
self._plot = Plot1D(self)
+ self._plot.setGraphGrid(True)
self._selector = NumpyAxesSelector(self)
self._selector.setNamedAxesSelectorVisibility(False)
@@ -97,10 +98,18 @@ class ArrayCurvePlot(qt.QWidget):
"""
return self._plot
- def setCurvesData(self, ys, x=None,
- yerror=None, xerror=None,
- ylabels=None, xlabel=None, title=None,
- xscale=None, yscale=None):
+ def setCurvesData(
+ self,
+ ys,
+ x=None,
+ yerror=None,
+ xerror=None,
+ ylabels=None,
+ xlabel=None,
+ title=None,
+ xscale=None,
+ yscale=None,
+ ):
"""
:param List[ndarray] ys: List of arrays to be represented by the y (vertical) axis.
@@ -139,11 +148,9 @@ class ArrayCurvePlot(qt.QWidget):
self._plot.setGraphTitle(title or "")
if xscale is not None:
- self._plot.getXAxis().setScale(
- 'log' if xscale == 'log' else 'linear')
+ self._plot.getXAxis().setScale("log" if xscale == "log" else "linear")
if yscale is not None:
- self._plot.getYAxis().setScale(
- 'log' if yscale == 'log' else 'linear')
+ self._plot.getYAxis().setScale("log" if yscale == "log" else "linear")
self._updateCurve()
if not self.__selector_is_connected:
@@ -168,8 +175,10 @@ class ArrayCurvePlot(qt.QWidget):
# Only remove curves that will no longer belong to the plot
# So remaining curves keep their settings
for item in self._plot.getItems():
- if (isinstance(item, items.Curve) and
- item.getName() not in self.__signals_names):
+ if (
+ isinstance(item, items.Curve)
+ and item.getName() not in self.__signals_names
+ ):
self._plot.remove(item)
for i in range(len(self.__signals)):
@@ -179,9 +188,9 @@ class ArrayCurvePlot(qt.QWidget):
y_errors = None
if i == 0 and self.__signal_errors is not None:
y_errors = self.__signal_errors[self._selector.selection()]
- self._plot.addCurve(x, ys[i], legend=legend,
- xerror=self.__x_axis_errors,
- yerror=y_errors)
+ self._plot.addCurve(
+ x, ys[i], legend=legend, xerror=self.__x_axis_errors, yerror=y_errors
+ )
if i == 0:
self._plot.setActiveCurve(legend)
@@ -207,6 +216,7 @@ class XYVScatterPlot(qt.QWidget):
Widget for plotting one or more scatters
(with identical x, y coordinates).
"""
+
def __init__(self, parent=None):
"""
@@ -229,9 +239,11 @@ class XYVScatterPlot(qt.QWidget):
self.__y_axis_errors = None
self._plot = ScatterView(self)
- self._plot.setColormap(Colormap(name="viridis",
- vmin=None, vmax=None,
- normalization=Colormap.LINEAR))
+ self._plot.setColormap(
+ Colormap(
+ name="viridis", vmin=None, vmax=None, normalization=Colormap.LINEAR
+ )
+ )
self._slider = HorizontalSliderWithBrowser(parent=self)
self._slider.setMinimum(0)
@@ -263,11 +275,20 @@ class XYVScatterPlot(qt.QWidget):
"""
return self._plot.getPlotWidget()
- def setScattersData(self, y, x, values,
- yerror=None, xerror=None,
- ylabel=None, xlabel=None,
- title="", scatter_titles=None,
- xscale=None, yscale=None):
+ def setScattersData(
+ self,
+ y,
+ x,
+ values,
+ yerror=None,
+ xerror=None,
+ ylabel=None,
+ xlabel=None,
+ title="",
+ scatter_titles=None,
+ xscale=None,
+ yscale=None,
+ ):
"""
:param ndarray y: 1D array for y (vertical) coordinates.
@@ -306,11 +327,9 @@ class XYVScatterPlot(qt.QWidget):
self._slider.valueChanged[int].connect(self._sliderIdxChanged)
if xscale is not None:
- self._plot.getXAxis().setScale(
- 'log' if xscale == 'log' else 'linear')
+ self._plot.getXAxis().setScale("log" if xscale == "log" else "linear")
if yscale is not None:
- self._plot.getYAxis().setScale(
- 'log' if yscale == 'log' else 'linear')
+ self._plot.getYAxis().setScale("log" if yscale == "log" else "linear")
self._updateScatter()
@@ -324,14 +343,18 @@ class XYVScatterPlot(qt.QWidget):
title = self.__graph_title # main NXdata @title
if len(self.__scatter_titles) > 1:
# Append dataset name only when there is many datasets
- title += '\n' + self.__scatter_titles[idx]
+ title += "\n" + self.__scatter_titles[idx]
else:
title = self.__scatter_titles[idx] # scatter dataset name
self._plot.setGraphTitle(title)
- self._plot.setData(x, y, self.__values[idx],
- xerror=self.__x_axis_errors,
- yerror=self.__y_axis_errors)
+ self._plot.setData(
+ x,
+ y,
+ self.__values[idx],
+ xerror=self.__x_axis_errors,
+ yerror=self.__y_axis_errors,
+ )
self._plot.resetZoom()
self._plot.getXAxis().setLabel(self.__x_axis_name)
self._plot.getYAxis().setLabel(self.__y_axis_name)
@@ -356,6 +379,7 @@ class ArrayImagePlot(qt.QWidget):
If one or both of the axes does not have regularly spaced values, the
the image is plotted as a coloured scatter plot.
"""
+
def __init__(self, parent=None):
"""
@@ -371,9 +395,11 @@ class ArrayImagePlot(qt.QWidget):
self.__y_axis_name = None
self._plot = Plot2D(self)
- self._plot.setDefaultColormap(Colormap(name="viridis",
- vmin=None, vmax=None,
- normalization=Colormap.LINEAR))
+ self._plot.setDefaultColormap(
+ Colormap(
+ name="viridis", vmin=None, vmax=None, normalization=Colormap.LINEAR
+ )
+ )
self._plot.getIntensityHistogramAction().setVisible(True)
self._plot.setKeepDataAspectRatio(True)
maskToolWidget = self._plot.getMaskToolsDockWidget().widget()
@@ -407,12 +433,20 @@ class ArrayImagePlot(qt.QWidget):
"""
return self._plot
- def setImageData(self, signals,
- x_axis=None, y_axis=None,
- signals_names=None,
- xlabel=None, ylabel=None,
- title=None, isRgba=False,
- xscale=None, yscale=None):
+ def setImageData(
+ self,
+ signals,
+ x_axis=None,
+ y_axis=None,
+ signals_names=None,
+ xlabel=None,
+ ylabel=None,
+ title=None,
+ isRgba=False,
+ xscale=None,
+ yscale=None,
+ keep_ratio: bool = True,
+ ):
"""
:param signals: list of n-D datasets, whose last 2 dimensions are used as the
@@ -430,6 +464,7 @@ class ArrayImagePlot(qt.QWidget):
:param isRgba: True if data is a 3D RGBA image
:param str xscale: Scale of X axis in (None, 'linear', 'log')
:param str yscale: Scale of Y axis in (None, 'linear', 'log')
+ :param keep_ratio: Toggle plot keep aspect ratio
"""
self._selector.selectionChanged.disconnect(self._updateImage)
self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged)
@@ -464,12 +499,14 @@ class ArrayImagePlot(qt.QWidget):
self._auxSigSlider.setValue(0)
self._axis_scales = xscale, yscale
- self._updateImage()
- self._plot.resetZoom()
self._selector.selectionChanged.connect(self._updateImage)
self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged)
+ self._updateImage()
+ self._plot.setKeepDataAspectRatio(keep_ratio)
+ self._plot.resetZoom()
+
def _updateImage(self):
selection = self._selector.selection()
auxSigIdx = self._auxSigSlider.value()
@@ -491,7 +528,7 @@ class ArrayImagePlot(qt.QWidget):
x_axis = numpy.arange(image.shape[1])
elif numpy.isscalar(x_axis) or len(x_axis) == 1:
# constant axis
- x_axis = x_axis * numpy.ones((image.shape[1], ))
+ x_axis = x_axis * numpy.ones((image.shape[1],))
elif len(x_axis) == 2:
# linear calibration
x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1]
@@ -499,14 +536,25 @@ class ArrayImagePlot(qt.QWidget):
if y_axis is None:
y_axis = numpy.arange(image.shape[0])
elif numpy.isscalar(y_axis) or len(y_axis) == 1:
- y_axis = y_axis * numpy.ones((image.shape[0], ))
+ y_axis = y_axis * numpy.ones((image.shape[0],))
elif len(y_axis) == 2:
y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1]
- xcalib = ArrayCalibration(x_axis)
- ycalib = ArrayCalibration(y_axis)
-
- self._plot.remove(kind=("scatter", "image",))
+ try:
+ xcalib = ArrayCalibration(x_axis)
+ except ValueError:
+ xcalib = NoCalibration()
+ try:
+ ycalib = ArrayCalibration(y_axis)
+ except ValueError:
+ ycalib = NoCalibration()
+
+ self._plot.remove(
+ kind=(
+ "scatter",
+ "image",
+ )
+ )
if xcalib.is_affine() and ycalib.is_affine():
# regular image
xorigin, xscale = xcalib(0), xcalib.get_slope()
@@ -514,33 +562,42 @@ class ArrayImagePlot(qt.QWidget):
origin = (xorigin, yorigin)
scale = (xscale, yscale)
- self._plot.getXAxis().setScale('linear')
- self._plot.getYAxis().setScale('linear')
- self._plot.addImage(image, legend=legend,
- origin=origin, scale=scale,
- replace=True, resetzoom=False)
+ self._plot.getXAxis().setScale("linear")
+ self._plot.getYAxis().setScale("linear")
+ self._plot.addImage(
+ image,
+ legend=legend,
+ origin=origin,
+ scale=scale,
+ replace=True,
+ resetzoom=False,
+ )
else:
xaxisscale, yaxisscale = self._axis_scales
if xaxisscale is not None:
self._plot.getXAxis().setScale(
- 'log' if xaxisscale == 'log' else 'linear')
+ "log" if xaxisscale == "log" else "linear"
+ )
if yaxisscale is not None:
self._plot.getYAxis().setScale(
- 'log' if yaxisscale == 'log' else 'linear')
+ "log" if yaxisscale == "log" else "linear"
+ )
scatterx, scattery = numpy.meshgrid(x_axis, y_axis)
# fixme: i don't think this can handle "irregular" RGBA images
- self._plot.addScatter(numpy.ravel(scatterx),
- numpy.ravel(scattery),
- numpy.ravel(image),
- legend=legend)
+ self._plot.addScatter(
+ numpy.ravel(scatterx),
+ numpy.ravel(scattery),
+ numpy.ravel(image),
+ legend=legend,
+ )
if self.__title:
title = self.__title
if len(self.__signals_names) > 1:
# Append dataset name only when there is many datasets
- title += '\n' + self.__signals_names[auxSigIdx]
+ title += "\n" + self.__signals_names[auxSigIdx]
else:
title = self.__signals_names[auxSigIdx]
self._plot.setGraphTitle(title)
@@ -570,6 +627,7 @@ class ArrayComplexImagePlot(qt.QWidget):
If one or both of the axes does not have regularly spaced values, the
the image is plotted as a coloured scatter plot.
"""
+
def __init__(self, parent=None, colormap=None):
"""
@@ -586,10 +644,12 @@ class ArrayComplexImagePlot(qt.QWidget):
self._plot = ComplexImageView(self)
if colormap is not None:
- for mode in (ComplexImageView.ComplexMode.ABSOLUTE,
- ComplexImageView.ComplexMode.SQUARE_AMPLITUDE,
- ComplexImageView.ComplexMode.REAL,
- ComplexImageView.ComplexMode.IMAGINARY):
+ for mode in (
+ ComplexImageView.ComplexMode.ABSOLUTE,
+ ComplexImageView.ComplexMode.SQUARE_AMPLITUDE,
+ ComplexImageView.ComplexMode.REAL,
+ ComplexImageView.ComplexMode.IMAGINARY,
+ ):
self._plot.setColormap(colormap, mode)
self._plot.getPlot().getIntensityHistogramAction().setVisible(True)
@@ -625,11 +685,17 @@ class ArrayComplexImagePlot(qt.QWidget):
"""
return self._plot.getPlot()
- def setImageData(self, signals,
- x_axis=None, y_axis=None,
- signals_names=None,
- xlabel=None, ylabel=None,
- title=None):
+ def setImageData(
+ self,
+ signals,
+ x_axis=None,
+ y_axis=None,
+ signals_names=None,
+ xlabel=None,
+ ylabel=None,
+ title=None,
+ keep_ratio: bool = True,
+ ):
"""
:param signals: list of n-D datasets, whose last 2 dimensions are used as the
@@ -644,6 +710,7 @@ class ArrayComplexImagePlot(qt.QWidget):
:param xlabel: Label for X axis
:param ylabel: Label for Y axis
:param title: Graph title
+ :param keep_ratio: Toggle plot keep aspect ratio
"""
self._selector.selectionChanged.disconnect(self._updateImage)
self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged)
@@ -673,6 +740,7 @@ class ArrayComplexImagePlot(qt.QWidget):
self._auxSigSlider.setValue(0)
self._updateImage()
+ self._plot.setKeepDataAspectRatio(keep_ratio)
self._plot.getPlot().resetZoom()
self._selector.selectionChanged.connect(self._updateImage)
@@ -697,7 +765,7 @@ class ArrayComplexImagePlot(qt.QWidget):
x_axis = numpy.arange(image.shape[1])
elif numpy.isscalar(x_axis) or len(x_axis) == 1:
# constant axis
- x_axis = x_axis * numpy.ones((image.shape[1], ))
+ x_axis = x_axis * numpy.ones((image.shape[1],))
elif len(x_axis) == 2:
# linear calibration
x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1]
@@ -705,25 +773,31 @@ class ArrayComplexImagePlot(qt.QWidget):
if y_axis is None:
y_axis = numpy.arange(image.shape[0])
elif numpy.isscalar(y_axis) or len(y_axis) == 1:
- y_axis = y_axis * numpy.ones((image.shape[0], ))
+ y_axis = y_axis * numpy.ones((image.shape[0],))
elif len(y_axis) == 2:
y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1]
- xcalib = ArrayCalibration(x_axis)
- ycalib = ArrayCalibration(y_axis)
+ try:
+ xcalib = ArrayCalibration(x_axis)
+ except ValueError:
+ xcalib = NoCalibration()
+ try:
+ ycalib = ArrayCalibration(y_axis)
+ except ValueError:
+ ycalib = NoCalibration()
self._plot.setData(image)
if xcalib.is_affine():
xorigin, xscale = xcalib(0), xcalib.get_slope()
else:
_logger.warning("Unsupported complex image X axis calibration")
- xorigin, xscale = 0., 1.
+ xorigin, xscale = 0.0, 1.0
if ycalib.is_affine():
yorigin, yscale = ycalib(0), ycalib.get_slope()
else:
_logger.warning("Unsupported complex image Y axis calibration")
- yorigin, yscale = 0., 1.
+ yorigin, yscale = 0.0, 1.0
self._plot.setOrigin((xorigin, yorigin))
self._plot.setScale((xscale, yscale))
@@ -732,7 +806,7 @@ class ArrayComplexImagePlot(qt.QWidget):
title = self.__title
if len(self.__signals_names) > 1:
# Append dataset name only when there is many datasets
- title += '\n' + self.__signals_names[auxSigIdx]
+ title += "\n" + self.__signals_names[auxSigIdx]
else:
title = self.__signals_names[auxSigIdx]
self._plot.setGraphTitle(title)
@@ -759,6 +833,7 @@ class ArrayStackPlot(qt.QWidget):
the signal array, and the plot is updated to load the stack corresponding
to the selection.
"""
+
def __init__(self, parent=None):
"""
@@ -778,7 +853,9 @@ class ArrayStackPlot(qt.QWidget):
self.__x_axis_name = None
self._stack_view = StackView(self)
- maskToolWidget = self._stack_view.getPlotWidget().getMaskToolsDockWidget().widget()
+ maskToolWidget = (
+ self._stack_view.getPlotWidget().getMaskToolsDockWidget().widget()
+ )
maskToolWidget.setItemMaskUpdated(True)
self._hline = qt.QFrame(self)
@@ -804,11 +881,18 @@ class ArrayStackPlot(qt.QWidget):
"""
return self._stack_view
- def setStackData(self, signal,
- x_axis=None, y_axis=None, z_axis=None,
- signal_name=None,
- xlabel=None, ylabel=None, zlabel=None,
- title=None):
+ def setStackData(
+ self,
+ signal,
+ x_axis=None,
+ y_axis=None,
+ z_axis=None,
+ signal_name=None,
+ xlabel=None,
+ ylabel=None,
+ zlabel=None,
+ title=None,
+ ):
"""
:param signal: n-D dataset, whose last 3 dimensions are used as the
@@ -890,13 +974,12 @@ class ArrayStackPlot(qt.QWidget):
calibrations = []
for axis in [z_axis, y_axis, x_axis]:
-
if axis is None:
calibrations.append(NoCalibration())
elif len(axis) == 2:
calibrations.append(
- LinearCalibration(y_intercept=axis[0],
- slope=axis[1]))
+ LinearCalibration(y_intercept=axis[0], slope=axis[1])
+ )
else:
calibrations.append(ArrayCalibration(axis))
@@ -911,9 +994,8 @@ class ArrayStackPlot(qt.QWidget):
self._stack_view.setStack(stk, calibrations=calibrations)
self._stack_view.setLabels(
- labels=[self.__z_axis_name,
- self.__y_axis_name,
- self.__x_axis_name])
+ labels=[self.__z_axis_name, self.__y_axis_name, self.__x_axis_name]
+ )
def clear(self):
old = self._selector.blockSignals(True)
@@ -935,6 +1017,7 @@ class ArrayVolumePlot(qt.QWidget):
the signal array, and the plot is updated to load the stack corresponding
to the selection.
"""
+
def __init__(self, parent=None):
"""
@@ -980,11 +1063,18 @@ class ArrayVolumePlot(qt.QWidget):
"""
return self._view
- def setData(self, signal,
- x_axis=None, y_axis=None, z_axis=None,
- signal_name=None,
- xlabel=None, ylabel=None, zlabel=None,
- title=None):
+ def setData(
+ self,
+ signal,
+ x_axis=None,
+ y_axis=None,
+ z_axis=None,
+ signal_name=None,
+ xlabel=None,
+ ylabel=None,
+ zlabel=None,
+ title=None,
+ ):
"""
:param signal: n-D dataset, whose last 3 dimensions are used as the
@@ -1050,14 +1140,13 @@ class ArrayVolumePlot(qt.QWidget):
if axis is None:
calibration = NoCalibration()
elif len(axis) == 2:
- calibration = LinearCalibration(
- y_intercept=axis[0], slope=axis[1])
+ calibration = LinearCalibration(y_intercept=axis[0], slope=axis[1])
else:
calibration = ArrayCalibration(axis)
if not calibration.is_affine():
_logger.warning("Axis has not linear values, ignored")
- offset.append(0.)
- scale.append(1.)
+ offset.append(0.0)
+ scale.append(1.0)
else:
offset.append(calibration(0))
scale.append(calibration.get_slope())
@@ -1077,7 +1166,8 @@ class ArrayVolumePlot(qt.QWidget):
volumeView = self.getVolumeView()
volumeView.setData(data, offset=offset, scale=scale)
volumeView.setAxesLabels(
- self.__x_axis_name, self.__y_axis_name, self.__z_axis_name)
+ self.__x_axis_name, self.__y_axis_name, self.__z_axis_name
+ )
def clear(self):
old = self._selector.blockSignals(True)
diff --git a/src/silx/gui/data/NumpyAxesSelector.py b/src/silx/gui/data/NumpyAxesSelector.py
index e6da0d4..9b62c29 100644
--- a/src/silx/gui/data/NumpyAxesSelector.py
+++ b/src/silx/gui/data/NumpyAxesSelector.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
@@ -25,7 +24,6 @@
"""This module defines a widget able to convert a numpy array from n-dimensions
to a numpy array with less dimensions.
"""
-from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
@@ -270,8 +268,9 @@ class NumpyAxesSelector(qt.QWidget):
:param List[str] axesNames: List of distinct strings identifying axis names
"""
self.__axisNames = list(axesNames)
- assert len(set(self.__axisNames)) == len(self.__axisNames),\
+ assert len(set(self.__axisNames)) == len(self.__axisNames), (
"Non-unique axes names: %s" % self.__axisNames
+ )
delta = len(self.__axis) - len(self.__axisNames)
if delta < 0:
@@ -320,10 +319,14 @@ class NumpyAxesSelector(qt.QWidget):
if index >= delta and index - delta < len(self.__axisNames):
axis.setAxisName(self.__axisNames[index - delta])
# this weak method was expected to be able to delete sub widget
- callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisValueChanged), axis)
+ callback = functools.partial(
+ silx.utils.weakref.WeakMethodProxy(self.__axisValueChanged), axis
+ )
axis.valueChanged.connect(callback)
# this weak method was expected to be able to delete sub widget
- callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisNameChanged), axis)
+ callback = functools.partial(
+ silx.utils.weakref.WeakMethodProxy(self.__axisNameChanged), axis
+ )
axis.axisNameChanged.connect(callback)
axis.setNamedAxisSelectorVisibility(self.__namedAxesVisibility)
self.layout().addWidget(axis)
@@ -337,8 +340,12 @@ class NumpyAxesSelector(qt.QWidget):
"""Update axes geometry to align all axes components together."""
if len(self.__axis) <= 0:
return
- lineEditWidth = max([a.slider().lineEdit().minimumSize().width() for a in self.__axis])
- limitWidth = max([a.slider().limitWidget().minimumSizeHint().width() for a in self.__axis])
+ lineEditWidth = max(
+ [a.slider().lineEdit().minimumSize().width() for a in self.__axis]
+ )
+ limitWidth = max(
+ [a.slider().limitWidget().minimumSizeHint().width() for a in self.__axis]
+ )
for a in self.__axis:
a.slider().lineEdit().setFixedWidth(lineEditWidth)
a.slider().limitWidget().setFixedWidth(limitWidth)
@@ -420,7 +427,9 @@ class NumpyAxesSelector(qt.QWidget):
# get a view with few fixed dimensions
# with a h5py dataset, it create a copy
# TODO we can reuse the same memory in case of a copy
- self.__selectedData = numpy.transpose(self.__data[self.selection()], permutation)
+ self.__selectedData = numpy.transpose(
+ self.__data[self.selection()], permutation
+ )
self.selectionChanged.emit()
def data(self):
@@ -479,8 +488,12 @@ class NumpyAxesSelector(qt.QWidget):
if self.__data is None:
return tuple()
else:
- return tuple([axis.value() if axis.axisName() == "" else slice(None)
- for axis in self.__axis])
+ return tuple(
+ [
+ axis.value() if axis.axisName() == "" else slice(None)
+ for axis in self.__axis
+ ]
+ )
def setSelection(self, selection, permutation=None):
"""Set the selection along each dimension.
@@ -503,8 +516,9 @@ class NumpyAxesSelector(qt.QWidget):
# Check selection
if len(selection) != len(data_shape):
raise ValueError(
- "Selection length (%d) and data ndim (%d) mismatch" %
- (len(selection), len(data_shape)))
+ "Selection length (%d) and data ndim (%d) mismatch"
+ % (len(selection), len(data_shape))
+ )
# Check selection type
selectedDataNDim = 0
@@ -512,8 +526,9 @@ class NumpyAxesSelector(qt.QWidget):
if isinstance(element, int):
if not 0 <= element < size:
raise ValueError(
- "Selected index (%d) outside data dimension range [0-%d]" %
- (element, size))
+ "Selected index (%d) outside data dimension range [0-%d]"
+ % (element, size)
+ )
elif element is None or element == slice(None):
selectedDataNDim += 1
else:
@@ -522,8 +537,9 @@ class NumpyAxesSelector(qt.QWidget):
ndim = len(self.__axisNames)
if selectedDataNDim != ndim:
raise ValueError(
- "Selection dimensions (%d) and number of axes (%d) mismatch" %
- (selectedDataNDim, ndim))
+ "Selection dimensions (%d) and number of axes (%d) mismatch"
+ % (selectedDataNDim, ndim)
+ )
# check permutation
if permutation is None:
@@ -532,7 +548,8 @@ class NumpyAxesSelector(qt.QWidget):
if set(permutation) != set(range(ndim)):
raise ValueError(
"Error in provided permutation: "
- "Wrong size, elements out of range or duplicates")
+ "Wrong size, elements out of range or duplicates"
+ )
inversePermutation = numpy.argsort(permutation)
diff --git a/src/silx/gui/data/RecordTableView.py b/src/silx/gui/data/RecordTableView.py
index ea73c62..8bf1683 100644
--- a/src/silx/gui/data/RecordTableView.py
+++ b/src/silx/gui/data/RecordTableView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -26,7 +25,6 @@
This module define model and widget to display 1D slices from numpy
array using compound data types or hdf5 databases.
"""
-from __future__ import division
import itertools
import numpy
@@ -55,8 +53,9 @@ class _MultiLineItem(qt.QItemDelegate):
"""
qt.QItemDelegate.__init__(self, parent)
self.__textOptions = qt.QTextOption()
- self.__textOptions.setFlags(qt.QTextOption.IncludeTrailingSpaces |
- qt.QTextOption.ShowTabsAndSpaces)
+ self.__textOptions.setFlags(
+ qt.QTextOption.IncludeTrailingSpaces | qt.QTextOption.ShowTabsAndSpaces
+ )
self.__textOptions.setWrapMode(qt.QTextOption.NoWrap)
self.__textOptions.setAlignment(qt.Qt.AlignTop | qt.Qt.AlignLeft)
@@ -150,7 +149,7 @@ class RecordTableModel(qt.QAbstractTableModel):
:param numpy.ndarray data: A numpy array or a h5py dataset
"""
- MAX_NUMBER_OF_ROWS = 10e6
+ MAX_NUMBER_OF_ROWS = int(10e6)
"""Maximum number of display values of the dataset"""
def __init__(self, parent=None, data=None):
@@ -244,9 +243,11 @@ class RecordTableModel(qt.QAbstractTableModel):
return None
# Handle clipping of huge tables
- if (self.__isClipped() and
- orientation == qt.Qt.Vertical and
- section == self.rowCount() - 2):
+ if (
+ self.__isClipped()
+ and orientation == qt.Qt.Vertical
+ and section == self.rowCount() - 2
+ ):
return self.__clippedData(role)
if role == qt.Qt.DisplayRole:
@@ -278,7 +279,11 @@ class RecordTableModel(qt.QAbstractTableModel):
def __isClipped(self) -> bool:
"""Returns whether the displayed array is clipped or not"""
- return self.__data is not None and self.__is_array and len(self.__data) > self.MAX_NUMBER_OF_ROWS
+ return (
+ self.__data is not None
+ and self.__is_array
+ and len(self.__data) > self.MAX_NUMBER_OF_ROWS
+ )
def setArrayData(self, data):
"""Set the data array and the viewing perspective.
@@ -361,8 +366,7 @@ class RecordTableModel(qt.QAbstractTableModel):
return self.__formatter
def __formatChanged(self):
- """Called when the format changed.
- """
+ """Called when the format changed."""
self.__editFormatter = TextFormatter(self, self.getFormatter())
self.__editFormatter.setUseQuoteForText(False)
self.reset()
@@ -400,8 +404,8 @@ class _ShowEditorProxyModel(qt.QIdentityProxyModel):
class RecordTableView(qt.QTableView):
- """TableView using DatabaseTableModel as default model.
- """
+ """TableView using DatabaseTableModel as default model."""
+
def __init__(self, parent=None):
"""
Constructor
diff --git a/src/silx/gui/data/TextFormatter.py b/src/silx/gui/data/TextFormatter.py
index b6baca4..aee2427 100644
--- a/src/silx/gui/data/TextFormatter.py
+++ b/src/silx/gui/data/TextFormatter.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -84,8 +83,8 @@ class TextFormatter(qt.QObject):
self.__integerFormat = "%d"
self.__floatFormat = "%g"
self.__useQuoteForText = True
- self.__imaginaryUnit = u"j"
- self.__enumFormat = u"%(name)s(%(value)d)"
+ self.__imaginaryUnit = "j"
+ self.__enumFormat = "%(name)s(%(value)d)"
def integerFormat(self):
"""Returns the format string controlling how the integer data
@@ -196,7 +195,7 @@ class TextFormatter(qt.QObject):
def __formatText(self, text):
if self.__useQuoteForText:
- text = "\"%s\"" % text.replace("\\", "\\\\").replace("\"", "\\\"")
+ text = '"%s"' % text.replace("\\", "\\\\").replace('"', '\\"')
return text
def __formatBinary(self, data):
@@ -210,7 +209,7 @@ class TextFormatter(qt.QObject):
pass
data = ["\\x%02X" % d for d in data]
if self.__useQuoteForText:
- return "b\"%s\"" % "".join(data)
+ return 'b"%s"' % "".join(data)
else:
return "".join(data)
@@ -218,7 +217,7 @@ class TextFormatter(qt.QObject):
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)
+ return 'b"%s"' % "".join(data)
else:
return "".join(data)
@@ -234,6 +233,8 @@ class TextFormatter(qt.QObject):
:param data: A binary string of char expected in ASCII
:rtype: str
"""
+ if isinstance(data, str):
+ return self.__formatText(data)
try:
text = "%s" % data.decode("ascii")
return self.__formatText(text)
@@ -243,7 +244,7 @@ class TextFormatter(qt.QObject):
_logger.error("Invalid ASCII string %s.", data)
if data == b"\xB0":
_logger.error("Fallback using cp1252 encoding")
- return self.__formatText(u"\u00B0")
+ return self.__formatText("\u00B0")
return self.__formatSafeAscii(data)
def __formatH5pyObject(self, data, dtype):
@@ -295,7 +296,7 @@ class TextFormatter(qt.QObject):
else:
text = [self.toString(d, dtype) for d in data]
return "[" + " ".join(text) + "]"
- if dtype is not None and dtype.kind == 'O':
+ if dtype is not None and dtype.kind == "O":
text = self.__formatH5pyObject(data, dtype)
if text is not None:
return text
@@ -305,7 +306,9 @@ class TextFormatter(qt.QObject):
if dtype.fields is not None:
text = []
for index, field in enumerate(dtype.fields.items()):
- text.append(field[0] + ":" + self.toString(data[index], field[1][0]))
+ text.append(
+ field[0] + ":" + self.toString(data[index], field[1][0])
+ )
return "(" + " ".join(text) + ")"
return self.__formatBinary(data)
elif isinstance(data, (numpy.unicode_, str)):
@@ -315,9 +318,9 @@ class TextFormatter(qt.QObject):
dtype = data.dtype
if dtype is not None:
# Maybe a sub item from HDF5
- if dtype.kind == 'S':
+ if dtype.kind == "S":
return self.__formatCharString(data)
- elif dtype.kind == 'O':
+ elif dtype.kind == "O":
text = self.__formatH5pyObject(data, dtype)
if text is not None:
return text
@@ -354,18 +357,28 @@ class TextFormatter(qt.QObject):
text += self.__floatFormat % data.real
if data.real != 0 and data.imag != 0:
if data.imag < 0:
- template = self.__floatFormat + " - " + self.__floatFormat + self.__imaginaryUnit
+ template = (
+ self.__floatFormat
+ + " - "
+ + self.__floatFormat
+ + self.__imaginaryUnit
+ )
params = (data.real, -data.imag)
else:
- template = self.__floatFormat + " + " + self.__floatFormat + self.__imaginaryUnit
+ template = (
+ self.__floatFormat
+ + " + "
+ + self.__floatFormat
+ + self.__imaginaryUnit
+ )
params = (data.real, data.imag)
else:
if data.imag != 0:
template = self.__floatFormat + self.__imaginaryUnit
- params = (data.imag)
+ params = data.imag
else:
template = self.__floatFormat
- params = (data.real)
+ params = data.real
return template % params
elif isinstance(data, h5py.h5r.Reference):
dtype = h5py.special_dtype(ref=h5py.Reference)
diff --git a/src/silx/gui/data/_RecordPlot.py b/src/silx/gui/data/_RecordPlot.py
index 5be792f..b994a6e 100644
--- a/src/silx/gui/data/_RecordPlot.py
+++ b/src/silx/gui/data/_RecordPlot.py
@@ -5,16 +5,28 @@ from .. import qt
class RecordPlot(PlotWindow):
def __init__(self, parent=None, backend=None):
- super(RecordPlot, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=True,
- logScale=True, grid=True,
- curveStyle=True, colormap=False,
- aspectRatio=False, yInverted=False,
- copy=True, save=True, print_=True,
- control=True, position=True,
- roi=True, mask=False, fit=True)
+ super(RecordPlot, self).__init__(
+ parent=parent,
+ backend=backend,
+ resetzoom=True,
+ autoScale=True,
+ logScale=True,
+ grid=True,
+ curveStyle=True,
+ colormap=False,
+ aspectRatio=False,
+ yInverted=False,
+ copy=True,
+ save=True,
+ print_=True,
+ control=True,
+ position=True,
+ roi=True,
+ mask=False,
+ fit=True,
+ )
if parent is None:
- self.setWindowTitle('RecordPlot')
+ self.setWindowTitle("RecordPlot")
self._axesSelectionToolBar = AxesSelectionToolBar(parent=self, plot=self)
self.addToolBar(qt.Qt.BottomToolBarArea, self._axesSelectionToolBar)
@@ -23,7 +35,7 @@ class RecordPlot(PlotWindow):
:param Union[str,None] value:
"""
- label = '' if value is None else value
+ label = "" if value is None else value
index = self._axesSelectionToolBar.getXAxisDropDown().findData(value)
if index >= 0:
@@ -53,7 +65,7 @@ class RecordPlot(PlotWindow):
"""
comboBox = self._axesSelectionToolBar.getXAxisDropDown()
comboBox.clear()
- comboBox.addItem('-', None)
+ comboBox.addItem("-", None)
comboBox.insertSeparator(1)
for name in fieldNames:
comboBox.addItem(name, name)
@@ -65,8 +77,9 @@ class RecordPlot(PlotWindow):
def getAxesSelectionToolBar(self):
return self._axesSelectionToolBar
+
class AxesSelectionToolBar(qt.QToolBar):
- def __init__(self, parent=None, plot=None, title='Plot Axes Selection'):
+ def __init__(self, parent=None, plot=None, title="Plot Axes Selection"):
super(AxesSelectionToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
@@ -89,4 +102,4 @@ class AxesSelectionToolBar(qt.QToolBar):
return self._selectXAxisDropDown
def getYAxisDropDown(self):
- return self._selectYAxisDropDown \ No newline at end of file
+ return self._selectYAxisDropDown
diff --git a/src/silx/gui/data/_VolumeWindow.py b/src/silx/gui/data/_VolumeWindow.py
index 03b6876..49b18d5 100644
--- a/src/silx/gui/data/_VolumeWindow.py
+++ b/src/silx/gui/data/_VolumeWindow.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2019 European Synchrotron Radiation Facility
@@ -57,16 +56,16 @@ class VolumeWindow(SceneWindow):
"""
sceneWidget = self.getSceneWidget()
sceneWidget.getSceneGroup().setAxesLabels(
- 'X' if xlabel is None else xlabel,
- 'Y' if ylabel is None else ylabel,
- 'Z' if zlabel is None else zlabel)
+ "X" if xlabel is None else xlabel,
+ "Y" if ylabel is None else ylabel,
+ "Z" if zlabel is None else zlabel,
+ )
def clear(self):
"""Clear any currently displayed data"""
sceneWidget = self.getSceneWidget()
items = sceneWidget.getItems()
- if (len(items) == 1 and
- isinstance(items[0], (ScalarField3D, ComplexField3D))):
+ if len(items) == 1 and isinstance(items[0], (ScalarField3D, ComplexField3D)):
items[0].setData(None)
else: # Safety net
sceneWidget.clearItems()
@@ -84,7 +83,7 @@ class VolumeWindow(SceneWindow):
else:
return numpy.mean(data) + numpy.std(data)
- def setData(self, data, offset=(0., 0., 0.), scale=(1., 1., 1.)):
+ def setData(self, data, offset=(0.0, 0.0, 0.0), scale=(1.0, 1.0, 1.0)):
"""Set the 3D array data to display.
:param numpy.ndarray data: 3D array of float or complex
@@ -95,9 +94,11 @@ class VolumeWindow(SceneWindow):
dataMaxCoords = numpy.array(list(reversed(data.shape))) - 1
previousItems = sceneWidget.getItems()
- if (len(previousItems) == 1 and
- isinstance(previousItems[0], (ScalarField3D, ComplexField3D)) and
- numpy.iscomplexobj(data) == isinstance(previousItems[0], ComplexField3D)):
+ if (
+ len(previousItems) == 1
+ and isinstance(previousItems[0], (ScalarField3D, ComplexField3D))
+ and numpy.iscomplexobj(data) == isinstance(previousItems[0], ComplexField3D)
+ ):
# Reuse existing volume item
volume = sceneWidget.getItems()[0]
volume.setData(data, copy=False)
@@ -110,13 +111,13 @@ class VolumeWindow(SceneWindow):
# Add a new volume
sceneWidget.clearItems()
volume = sceneWidget.addVolume(data, copy=False)
- volume.setLabel('Volume')
+ volume.setLabel("Volume")
for plane in volume.getCutPlanes():
# Make plane going through the center of the data
plane.setPoint(dataMaxCoords // 2)
plane.setVisible(False)
plane.sigItemChanged.connect(self.__cutPlaneUpdated)
- volume.addIsosurface(self.__computeIsolevel, '#FF0000FF')
+ volume.addIsosurface(self.__computeIsolevel, "#FF0000FF")
# Expand the parameter tree
model = self.getParamTreeView().model()
diff --git a/src/silx/gui/data/__init__.py b/src/silx/gui/data/__init__.py
index 560062d..59d32f1 100644
--- a/src/silx/gui/data/__init__.py
+++ b/src/silx/gui/data/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/data/setup.py b/src/silx/gui/data/setup.py
deleted file mode 100644
index 23ccbdd..0000000
--- a/src/silx/gui/data/setup.py
+++ /dev/null
@@ -1,41 +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.
-#
-# ###########################################################################*/
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "16/01/2017"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('data', parent_package, top_path)
- config.add_subpackage('test')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
diff --git a/src/silx/gui/data/test/__init__.py b/src/silx/gui/data/test/__init__.py
index 7790ee5..1d8207b 100644
--- a/src/silx/gui/data/test/__init__.py
+++ b/src/silx/gui/data/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/data/test/test_arraywidget.py b/src/silx/gui/data/test/test_arraywidget.py
index c84a34f..faca333 100644
--- a/src/silx/gui/data/test/test_arraywidget.py
+++ b/src/silx/gui/data/test/test_arraywidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -28,7 +27,6 @@ __date__ = "05/12/2016"
import os
import tempfile
-import unittest
import numpy
@@ -42,6 +40,7 @@ import h5py
class TestArrayWidget(TestCaseQt):
"""Basic test for ArrayTableWidget with a numpy array"""
+
def setUp(self):
super(TestArrayWidget, self).setUp()
self.aw = ArrayTableWidget.ArrayTableWidget()
@@ -80,16 +79,13 @@ class TestArrayWidget(TestCaseQt):
self.assertEqual(len(self.aw.model._perspective), 0)
def testSetData4D(self):
- a = numpy.reshape(numpy.linspace(0.213, 1.234, 1250),
- (5, 5, 5, 10))
+ a = numpy.reshape(numpy.linspace(0.213, 1.234, 1250), (5, 5, 5, 10))
self.aw.setArrayData(a)
# default perspective (0, 1)
- self.assertEqual(list(self.aw.model._perspective),
- [0, 1])
+ self.assertEqual(list(self.aw.model._perspective), [0, 1])
self.aw.setPerspective((1, 3))
- self.assertEqual(list(self.aw.model._perspective),
- [1, 3])
+ self.assertEqual(list(self.aw.model._perspective), [1, 3])
b = self.aw.getData(copy=True)
self.assertTrue(numpy.array_equal(a, b))
@@ -97,12 +93,10 @@ class TestArrayWidget(TestCaseQt):
# 4D data has a 2-tuple as frame index
self.assertEqual(len(self.aw.model._index), 2)
# default index is (0, 0)
- self.assertEqual(list(self.aw.model._index),
- [0, 0])
+ self.assertEqual(list(self.aw.model._index), [0, 0])
self.aw.setFrameIndex((3, 1))
- self.assertEqual(list(self.aw.model._index),
- [3, 1])
+ self.assertEqual(list(self.aw.model._index), [3, 1])
def testColors(self):
a = numpy.arange(256, dtype=numpy.uint8)
@@ -122,18 +116,20 @@ class TestArrayWidget(TestCaseQt):
for i in range(256):
# all RGB channels for BG equal to data value
self.assertEqual(
- self.aw.model.data(self.aw.model.index(0, i),
- role=qt.Qt.BackgroundRole),
+ self.aw.model.data(
+ self.aw.model.index(0, i), role=qt.Qt.BackgroundRole
+ ),
qt.QColor(i, i, i),
- "Unexpected background color"
+ "Unexpected background color",
)
# all RGB channels for FG equal to XOR(data value, 255)
self.assertEqual(
- self.aw.model.data(self.aw.model.index(0, i),
- role=qt.Qt.ForegroundRole),
+ self.aw.model.data(
+ self.aw.model.index(0, i), role=qt.Qt.ForegroundRole
+ ),
qt.QColor(i ^ 255, i ^ 255, i ^ 255),
- "Unexpected text color"
+ "Unexpected text color",
)
# test colors are reset to None when a new data array is loaded
@@ -143,30 +139,27 @@ class TestArrayWidget(TestCaseQt):
for i in range(300):
# all RGB channels for BG equal to data value
self.assertIsNone(
- self.aw.model.data(self.aw.model.index(0, i),
- role=qt.Qt.BackgroundRole))
+ self.aw.model.data(self.aw.model.index(0, i), role=qt.Qt.BackgroundRole)
+ )
def testDefaultFlagNotEditable(self):
"""editable should be False by default, in setArrayData"""
self.aw.setArrayData([[0]])
idx = self.aw.model.createIndex(0, 0)
# model is editable
- self.assertFalse(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
+ self.assertFalse(self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
def testFlagEditable(self):
self.aw.setArrayData([[0]], editable=True)
idx = self.aw.model.createIndex(0, 0)
# model is editable
- self.assertTrue(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
+ self.assertTrue(self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
def testFlagNotEditable(self):
self.aw.setArrayData([[0]], editable=False)
idx = self.aw.model.createIndex(0, 0)
# model is editable
- self.assertFalse(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
+ self.assertFalse(self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
def testReferenceReturned(self):
"""when setting the data with copy=False and
@@ -174,8 +167,7 @@ class TestArrayWidget(TestCaseQt):
the same original object.
"""
# n-D (n >=2)
- a0 = numpy.reshape(numpy.linspace(0.213, 1.234, 1000),
- (10, 10, 10))
+ a0 = numpy.reshape(numpy.linspace(0.213, 1.234, 1000), (10, 10, 10))
self.aw.setArrayData(a0, copy=False)
a1 = self.aw.getData(copy=False)
@@ -204,15 +196,15 @@ class TestH5pyArrayWidget(TestCaseQt):
"""Basic test for ArrayTableWidget with a dataset.
Test flags, for dataset open in read-only or read-write modes"""
+
def setUp(self):
super(TestH5pyArrayWidget, self).setUp()
self.aw = ArrayTableWidget.ArrayTableWidget()
- self.data = numpy.reshape(numpy.linspace(0.213, 1.234, 1000),
- (10, 10, 10))
+ self.data = numpy.reshape(numpy.linspace(0.213, 1.234, 1000), (10, 10, 10))
# create an h5py file with a dataset
self.tempdir = tempfile.mkdtemp()
self.h5_fname = os.path.join(self.tempdir, "array.h5")
- h5f = h5py.File(self.h5_fname, mode='w')
+ h5f = h5py.File(self.h5_fname, mode="w")
h5f["my_array"] = self.data
h5f["my_scalar"] = 3.14
h5f["my_1D_array"] = numpy.array(numpy.arange(1000))
@@ -237,7 +229,7 @@ class TestH5pyArrayWidget(TestCaseQt):
self.aw.setArrayData(a, copy=False, editable=True)
- self.assertIsInstance(a, h5py.Dataset) # simple sanity check
+ self.assertIsInstance(a, h5py.Dataset) # simple sanity check
# internal representation must be a reference to original data (copy=False)
self.assertIsInstance(self.aw.model._array, h5py.Dataset)
self.assertTrue(self.aw.model._array.file.mode == "r")
@@ -248,12 +240,12 @@ class TestH5pyArrayWidget(TestCaseQt):
# model must have detected read-only dataset and disabled editing
self.assertFalse(self.aw.model._editable)
idx = self.aw.model.createIndex(0, 0)
- self.assertFalse(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
+ self.assertFalse(self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
# force editing read-only datasets raises IOError
- self.assertRaises(IOError, self.aw.model.setData,
- idx, 123.4, role=qt.Qt.EditRole)
+ self.assertRaises(
+ IOError, self.aw.model.setData, idx, 123.4, role=qt.Qt.EditRole
+ )
h5f.close()
def testReadWrite(self):
@@ -267,8 +259,7 @@ class TestH5pyArrayWidget(TestCaseQt):
idx = self.aw.model.createIndex(0, 0)
# model is editable
- self.assertTrue(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
+ self.assertTrue(self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
h5f.close()
def testSetData0D(self):
diff --git a/src/silx/gui/data/test/test_dataviewer.py b/src/silx/gui/data/test/test_dataviewer.py
index 30b76ce..85bbf7a 100644
--- a/src/silx/gui/data/test/test_dataviewer.py
+++ b/src/silx/gui/data/test/test_dataviewer.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -91,7 +90,7 @@ class _TestAbstractDataViewer(TestCaseQt):
self.assertEqual(DataViews.RAW_MODE, widget.displayMode())
def test_plot_1d_data(self):
- data = numpy.arange(3 ** 1)
+ data = numpy.arange(3**1)
data.shape = [3] * 1
widget = self.create_widget()
widget.setData(data)
@@ -100,7 +99,7 @@ class _TestAbstractDataViewer(TestCaseQt):
self.assertIn(DataViews.PLOT1D_MODE, availableModes)
def test_image_data(self):
- data = numpy.arange(3 ** 2)
+ data = numpy.arange(3**2)
data.shape = [3] * 2
widget = self.create_widget()
widget.setData(data)
@@ -118,7 +117,7 @@ class _TestAbstractDataViewer(TestCaseQt):
self.assertIn(DataViews.IMAGE_MODE, availableModes)
def test_image_complex_data(self):
- data = numpy.arange(3 ** 2, dtype=numpy.complex64)
+ data = numpy.arange(3**2, dtype=numpy.complex64)
data.shape = [3] * 2
widget = self.create_widget()
widget.setData(data)
@@ -127,41 +126,42 @@ class _TestAbstractDataViewer(TestCaseQt):
self.assertIn(DataViews.IMAGE_MODE, availableModes)
def test_plot_3d_data(self):
- data = numpy.arange(3 ** 3)
+ data = numpy.arange(3**3)
data.shape = [3] * 3
widget = self.create_widget()
widget.setData(data)
availableModes = set([v.modeId() for v in widget.currentAvailableViews()])
try:
import silx.gui.plot3d # noqa
+
self.assertIn(DataViews.PLOT3D_MODE, availableModes)
except ImportError:
self.assertIn(DataViews.STACK_MODE, availableModes)
self.assertEqual(DataViews.RAW_MODE, widget.displayMode())
def test_array_1d_data(self):
- data = numpy.array(["aaa"] * (3 ** 1))
+ data = numpy.array(["aaa"] * (3**1))
data.shape = [3] * 1
widget = self.create_widget()
widget.setData(data)
self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
def test_array_2d_data(self):
- data = numpy.array(["aaa"] * (3 ** 2))
+ data = numpy.array(["aaa"] * (3**2))
data.shape = [3] * 2
widget = self.create_widget()
widget.setData(data)
self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
def test_array_4d_data(self):
- data = numpy.array(["aaa"] * (3 ** 4))
+ data = numpy.array(["aaa"] * (3**4))
data.shape = [3] * 4
widget = self.create_widget()
widget.setData(data)
self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
def test_record_4d_data(self):
- data = numpy.zeros(3 ** 4, dtype='3int8, float32, (2,3)float64')
+ data = numpy.zeros(3**4, dtype="3int8, float32, (2,3)float64")
data.shape = [3] * 4
widget = self.create_widget()
widget.setData(data)
@@ -192,18 +192,36 @@ class _TestAbstractDataViewer(TestCaseQt):
listener.clear()
def test_change_display_mode(self):
- data = numpy.arange(10 ** 4)
+ listener = SignalListener()
+ data = numpy.arange(10**4)
data.shape = [10] * 4
widget = self.create_widget()
+ widget.selectionChanged.connect(listener)
widget.setData(data)
+
widget.setDisplayMode(DataViews.PLOT1D_MODE)
self.assertEqual(widget.displayedView().modeId(), DataViews.PLOT1D_MODE)
+ self.qWait(200)
+ assert listener.arguments() == [((0, 0, 0, slice(None)), None)]
+ listener.clear()
+
widget.setDisplayMode(DataViews.IMAGE_MODE)
self.assertEqual(widget.displayedView().modeId(), DataViews.IMAGE_MODE)
+ self.qWait(200)
+ assert listener.arguments() == [((0, 0, slice(None), slice(None)), None)]
+ listener.clear()
+
widget.setDisplayMode(DataViews.RAW_MODE)
self.assertEqual(widget.displayedView().modeId(), DataViews.RAW_MODE)
+ self.qWait(200)
+ # Changing from 2D to 2D view: Selection didn't changed
+ assert listener.callCount() == 0
+
widget.setDisplayMode(DataViews.EMPTY_MODE)
self.assertEqual(widget.displayedView().modeId(), DataViews.EMPTY_MODE)
+ self.qWait(200)
+ assert listener.arguments() == [(None, None)]
+ listener.clear()
def test_create_default_views(self):
widget = self.create_widget()
@@ -228,8 +246,7 @@ class _TestAbstractDataViewer(TestCaseQt):
def test_replace_view(self):
widget = self.create_widget()
view = _DataViewMock(widget)
- widget.replaceView(DataViews.RAW_MODE,
- view)
+ widget.replaceView(DataViews.RAW_MODE, view)
self.assertIsNone(widget.getViewFromModeId(DataViews.RAW_MODE))
self.assertTrue(view in widget.availableViews())
self.assertTrue(view in widget.currentAvailableViews())
@@ -238,29 +255,30 @@ class _TestAbstractDataViewer(TestCaseQt):
# replace a view that is a child of a composite view
widget = self.create_widget()
view = _DataViewMock(widget)
- replaced = widget.replaceView(DataViews.NXDATA_INVALID_MODE,
- view)
+ replaced = widget.replaceView(DataViews.NXDATA_INVALID_MODE, view)
self.assertTrue(replaced)
nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE)
- self.assertNotIn(DataViews.NXDATA_INVALID_MODE,
- [v.modeId() for v in nxdata_view.getViews()])
+ self.assertNotIn(
+ DataViews.NXDATA_INVALID_MODE, [v.modeId() for v in nxdata_view.getViews()]
+ )
self.assertTrue(view in nxdata_view.getViews())
class TestDataViewer(_TestAbstractDataViewer):
__test__ = True # because _TestAbstractDataViewer is ignored
+
def create_widget(self):
return DataViewer()
class TestDataViewerFrame(_TestAbstractDataViewer):
__test__ = True # because _TestAbstractDataViewer is ignored
+
def create_widget(self):
return DataViewerFrame()
class TestDataView(TestCaseQt):
-
def createComplexData(self):
line = [1, 2j, 3 + 3j, 4]
image = [line, line, line, line]
diff --git a/src/silx/gui/data/test/test_numpyaxesselector.py b/src/silx/gui/data/test/test_numpyaxesselector.py
index 37b8d3e..450b89d 100644
--- a/src/silx/gui/data/test/test_numpyaxesselector.py
+++ b/src/silx/gui/data/test/test_numpyaxesselector.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
@@ -28,7 +27,6 @@ __date__ = "29/01/2018"
import os
import tempfile
-import unittest
from contextlib import contextmanager
import numpy
@@ -41,7 +39,6 @@ import h5py
class TestNumpyAxesSelector(TestCaseQt):
-
def test_creation(self):
data = numpy.arange(3 * 3 * 3)
data.shape = 3, 3, 3
diff --git a/src/silx/gui/data/test/test_textformatter.py b/src/silx/gui/data/test/test_textformatter.py
index af41def..49b8283 100644
--- a/src/silx/gui/data/test/test_textformatter.py
+++ b/src/silx/gui/data/test/test_textformatter.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +25,6 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "12/12/2017"
-import unittest
import shutil
import tempfile
@@ -35,13 +33,12 @@ import numpy
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.utils.testutils import SignalListener
from ..TextFormatter import TextFormatter
-from silx.io.utils import h5py_read_dataset
import h5py
+import pytest
class TestTextFormatter(TestCaseQt):
-
def test_copy(self):
formatter = TextFormatter()
copy = TextFormatter(formatter=formatter)
@@ -98,11 +95,10 @@ class TestTextFormatter(TestCaseQt):
# degree character in cp1252
formatter = TextFormatter()
result = formatter.toString(numpy.bytes_(b"\xB0"))
- self.assertEqual(result, u'"\u00B0"')
+ self.assertEqual(result, '"\u00B0"')
class TestTextFormatterWithH5py(TestCaseQt):
-
@classmethod
def setUpClass(cls):
super(TestTextFormatterWithH5py, cls).setUpClass()
@@ -132,10 +128,10 @@ class TestTextFormatterWithH5py(TestCaseQt):
self.assertEqual(result, '"abc"')
def testUnicode(self):
- d = self.create_dataset(data=u"i\u2661cookies")
+ d = self.create_dataset(data="i\u2661cookies")
result = self.read_dataset(d)
self.assertEqual(len(result), 11)
- self.assertEqual(result, u'"i\u2661cookies"')
+ self.assertEqual(result, '"i\u2661cookies"')
def testBadAscii(self):
d = self.create_dataset(data=b"\xF0\x9F\x92\x94")
@@ -148,18 +144,18 @@ class TestTextFormatterWithH5py(TestCaseQt):
self.assertEqual(result, 'b"\\x61\\x62\\x63\\xF0"')
def testEnum(self):
- dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42}))
+ 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.read_dataset(d)
- self.assertEqual(result, 'BLUE(42)')
+ self.assertEqual(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.read_dataset(d)
- self.assertEqual(result, 'REF')
+ self.assertEqual(result, "REF")
def testArrayAscii(self):
d = self.create_dataset(data=[b"abc"])
@@ -168,11 +164,11 @@ class TestTextFormatterWithH5py(TestCaseQt):
def testArrayUnicode(self):
dtype = h5py.special_dtype(vlen=str)
- d = numpy.array([u"i\u2661cookies"], dtype=dtype)
+ d = numpy.array(["i\u2661cookies"], dtype=dtype)
d = self.create_dataset(data=d)
result = self.read_dataset(d)
self.assertEqual(len(result), 13)
- self.assertEqual(result, u'["i\u2661cookies"]')
+ self.assertEqual(result, '["i\u2661cookies"]')
def testArrayBadAscii(self):
d = self.create_dataset(data=[b"\xF0\x9F\x92\x94"])
@@ -185,15 +181,32 @@ class TestTextFormatterWithH5py(TestCaseQt):
self.assertEqual(result, '[b"\\x61\\x62\\x63\\xF0"]')
def testArrayEnum(self):
- dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42}))
+ 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.read_dataset(d)
- self.assertEqual(result, '[BLUE(42) GREEN(1) 100]')
+ self.assertEqual(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.read_dataset(d)
- self.assertEqual(result, '[REF NULL_REF]')
+ self.assertEqual(result, "[REF NULL_REF]")
+
+
+@pytest.mark.parametrize(
+ "data, expected",
+ [
+ (b"bytes", '"bytes"'),
+ ("unicode", '"unicode"'),
+ ((b"elem0", b"elem1"), '["elem0" "elem1"]'),
+ (("elem0", "elem1"), '["elem0" "elem1"]'),
+ ],
+)
+def test_formatter_h5py_attr(tmp_h5py_file, data, expected):
+ """Test formatter with h5py attributes"""
+ tmp_h5py_file.attrs["attr"] = data
+ formatter = TextFormatter()
+ result = formatter.toString(tmp_h5py_file.attrs["attr"])
+ assert result == expected
diff --git a/src/silx/gui/dialog/AbstractDataFileDialog.py b/src/silx/gui/dialog/AbstractDataFileDialog.py
index 5272f48..00db275 100644
--- a/src/silx/gui/dialog/AbstractDataFileDialog.py
+++ b/src/silx/gui/dialog/AbstractDataFileDialog.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2022 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
@@ -35,7 +34,6 @@ import sys
import os
import logging
import functools
-from distutils.version import LooseVersion
import numpy
@@ -58,7 +56,6 @@ some version of PyQt."""
class _IconProvider(object):
-
FileDialogToParentDir = qt.QStyle.SP_CustomBase + 1
FileDialogToParentFile = qt.QStyle.SP_CustomBase + 2
@@ -94,7 +91,9 @@ class _IconProvider(object):
pixmap.fill(qt.Qt.transparent)
painter = qt.QPainter(pixmap)
painter.drawPixmap(0, 0, backgroundIcon.pixmap(baseSize, mode=mode))
- painter.drawPixmap(0, size.height() // 3, baseIcon.pixmap(baseSize, mode=mode))
+ painter.drawPixmap(
+ 0, size.height() // 3, baseIcon.pixmap(baseSize, mode=mode)
+ )
painter.end()
icon.addPixmap(pixmap, mode=mode)
@@ -102,12 +101,16 @@ class _IconProvider(object):
def getFileDialogToParentDir(self):
if self.__iconFileDialogToParentDir is None:
- self.__iconFileDialogToParentDir = self._createIconToParent(qt.QStyle.SP_DirIcon)
+ self.__iconFileDialogToParentDir = self._createIconToParent(
+ qt.QStyle.SP_DirIcon
+ )
return self.__iconFileDialogToParentDir
def getFileDialogToParentFile(self):
if self.__iconFileDialogToParentFile is None:
- self.__iconFileDialogToParentFile = self._createIconToParent(qt.QStyle.SP_FileIcon)
+ self.__iconFileDialogToParentFile = self._createIconToParent(
+ qt.QStyle.SP_FileIcon
+ )
return self.__iconFileDialogToParentFile
def icon(self, kind):
@@ -149,13 +152,17 @@ class _SideBar(qt.QListView):
:rtype: List[str]
"""
urls = []
- version = LooseVersion(qt.qVersion())
+ version = tuple(map(int, qt.qVersion().split(".")[:3]))
feed_sidebar = True
if not DEFAULT_SIDEBAR_URL:
_logger.debug("Skip default sidebar URLs (from setted variable)")
feed_sidebar = False
- elif version < LooseVersion("5.11.2") and qt.BINDING == "PyQt5" and sys.platform in ["linux", "linux2"]:
+ elif (
+ version < (5, 11, 2)
+ and qt.BINDING == "PyQt5"
+ and sys.platform in ["linux", "linux2"]
+ ):
# Avoid segfault on PyQt5 + gtk
_logger.debug("Skip default sidebar URLs (avoid PyQt5 segfault)")
feed_sidebar = False
@@ -188,7 +195,9 @@ class _SideBar(qt.QListView):
selectionModel = self.selectionModel()
if selected is not None:
- selectionModel.setCurrentIndex(selected, qt.QItemSelectionModel.ClearAndSelect)
+ selectionModel.setCurrentIndex(
+ selected, qt.QItemSelectionModel.ClearAndSelect
+ )
else:
selectionModel.clear()
@@ -234,11 +243,12 @@ class _SideBar(qt.QListView):
def sizeHint(self):
index = self.model().index(0, 0)
- return self.sizeHintForIndex(index) + qt.QSize(2 * self.frameWidth(), 2 * self.frameWidth())
+ return self.sizeHintForIndex(index) + qt.QSize(
+ 2 * self.frameWidth(), 2 * self.frameWidth()
+ )
class _Browser(qt.QStackedWidget):
-
activated = qt.Signal(qt.QModelIndex)
selected = qt.Signal(qt.QModelIndex)
rootIndexChanged = qt.Signal(qt.QModelIndex)
@@ -304,7 +314,7 @@ class _Browser(qt.QStackedWidget):
elif self.currentIndex() == 1:
return qt.QFileDialog.Detail
else:
- assert(False)
+ assert False
def setViewMode(self, mode):
"""Set the current view mode.
@@ -316,7 +326,7 @@ class _Browser(qt.QStackedWidget):
elif mode == qt.QFileDialog.List:
self.showList()
else:
- assert(False)
+ assert False
def showList(self):
self.__listView.show()
@@ -344,11 +354,10 @@ class _Browser(qt.QStackedWidget):
self.__detailView.setModel(None)
def setRootIndex(self, index, model=None):
- """Sets the root item to the item at the given index.
- """
+ """Sets the root item to the item at the given index."""
rootIndex = self.__listView.rootIndex()
newModel = model or index.model()
- assert(newModel is not None)
+ assert newModel is not None
if rootIndex is None or rootIndex.model() is not newModel:
# update the model
@@ -417,19 +426,23 @@ class _Browser(qt.QStackedWidget):
nameId = stream.readQString()
if nameId != "Browser":
- _logger.warning("Stored state contains an invalid name id. Browser restoration cancelled.")
+ _logger.warning(
+ "Stored state contains an invalid name id. Browser restoration cancelled."
+ )
return False
version = stream.readInt32()
if version != self.__serialVersion:
- _logger.warning("Stored state contains an invalid version. Browser restoration cancelled.")
+ _logger.warning(
+ "Stored state contains an invalid version. Browser restoration cancelled."
+ )
return False
headerData = stream.readQVariant()
self.__detailView.header().restoreState(headerData)
viewMode = stream.readInt32()
- self.setViewMode(viewMode)
+ self.setViewMode(qt.QFileDialog.ViewMode(viewMode))
return True
def saveState(self):
@@ -440,17 +453,19 @@ class _Browser(qt.QStackedWidget):
data = qt.QByteArray()
stream = qt.QDataStream(data, qt.QIODevice.WriteOnly)
- nameId = u"Browser"
+ nameId = "Browser"
stream.writeQString(nameId)
stream.writeInt32(self.__serialVersion)
stream.writeQVariant(self.__detailView.header().saveState())
- stream.writeInt32(self.viewMode())
+ viewMode = self.viewMode()
+ if qt.BINDING in ("PyQt6", "PySide6"): # No auto conversion to int
+ viewMode = viewMode.value
+ stream.writeInt32(viewMode)
return data
class _FabioData(object):
-
def __init__(self, fabioFile):
self.__fabioFile = fabioFile
@@ -491,7 +506,6 @@ class _PathEdit(qt.QLineEdit):
class _CatchResizeEvent(qt.QObject):
-
resized = qt.Signal(qt.QResizeEvent)
def __init__(self, parent, target):
@@ -564,6 +578,7 @@ class AbstractDataFileDialog(qt.QDialog):
_logger.debug("Uses default QFileSystemModel with a SafeFileIconProvider")
self.__fileModel = qt.QFileSystemModel(self)
from .SafeFileIconProvider import SafeFileIconProvider
+
iconProvider = SafeFileIconProvider()
self.__fileModel.setIconProvider(iconProvider)
@@ -676,8 +691,12 @@ class AbstractDataFileDialog(qt.QDialog):
self.__fileTypeCombo = FileTypeComboBox(self)
self.__fileTypeCombo.setObjectName("fileTypeCombo")
self.__fileTypeCombo.setDuplicatesEnabled(False)
- self.__fileTypeCombo.setSizeAdjustPolicy(qt.QComboBox.AdjustToMinimumContentsLengthWithIcon)
- self.__fileTypeCombo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
+ self.__fileTypeCombo.setSizeAdjustPolicy(
+ qt.QComboBox.AdjustToMinimumContentsLengthWithIcon
+ )
+ self.__fileTypeCombo.setSizePolicy(
+ qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed
+ )
self.__fileTypeCombo.activated[int].connect(self.__filterSelected)
self.__fileTypeCombo.setFabioUrlSupproted(self._isFabioFilesSupported())
@@ -707,7 +726,9 @@ class AbstractDataFileDialog(qt.QDialog):
if self.__selectorWidget is not None:
self.__selectorWidget.selectionChanged.connect(self.__selectorWidgetChanged)
- self.__previewToolBar = self._createPreviewToolbar(self, self.__previewWidget, self.__selectorWidget)
+ self.__previewToolBar = self._createPreviewToolbar(
+ self, self.__previewWidget, self.__selectorWidget
+ )
self.__dataIcon = qt.QLabel(self)
self.__dataIcon.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
@@ -766,7 +787,9 @@ class AbstractDataFileDialog(qt.QDialog):
parentFileDirectory = qt.QAction(toolbar)
parentFileDirectory.setText("Parent directory of the file")
parentFileDirectory.setObjectName("toDirectoryAction")
- parentFileDirectory.setIcon(iconProvider.icon(iconProvider.FileDialogToParentDir))
+ parentFileDirectory.setIcon(
+ iconProvider.icon(iconProvider.FileDialogToParentDir)
+ )
parentFileDirectory.triggered.connect(self.__navigateToParentDir)
self.__parentFileDirectoryAction = parentFileDirectory
@@ -817,11 +840,15 @@ class AbstractDataFileDialog(qt.QDialog):
dummyCombo.setFixedHeight(self.__fileTypeCombo.height())
self.__resizeCombo = _CatchResizeEvent(self, self.__fileTypeCombo)
- self.__resizeCombo.resized.connect(lambda e: dummyCombo.setFixedHeight(e.size().height()))
+ self.__resizeCombo.resized.connect(
+ lambda e: dummyCombo.setFixedHeight(e.size().height())
+ )
dummyToolBar.setFixedHeight(self.__browseToolBar.height())
self.__resizeToolbar = _CatchResizeEvent(self, self.__browseToolBar)
- self.__resizeToolbar.resized.connect(lambda e: dummyToolBar.setFixedHeight(e.size().height()))
+ self.__resizeToolbar.resized.connect(
+ lambda e: dummyToolBar.setFixedHeight(e.size().height())
+ )
datasetSelection = qt.QWidget(self)
layoutLeft = qt.QVBoxLayout()
@@ -830,7 +857,9 @@ class AbstractDataFileDialog(qt.QDialog):
layoutLeft.addWidget(self.__browser)
layoutLeft.addWidget(self.__fileTypeCombo)
datasetSelection.setLayout(layoutLeft)
- datasetSelection.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Expanding)
+ datasetSelection.setSizePolicy(
+ qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Expanding
+ )
infoLayout = qt.QHBoxLayout()
infoLayout.setContentsMargins(0, 0, 0, 0)
@@ -857,7 +886,9 @@ class AbstractDataFileDialog(qt.QDialog):
dummyToolbar2.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
dummyToolbar2.setFixedHeight(self.__browseToolBar.height())
self.__resizeToolbar = _CatchResizeEvent(self, self.__browseToolBar)
- self.__resizeToolbar.resized.connect(lambda e: dummyToolbar2.setFixedHeight(e.size().height()))
+ self.__resizeToolbar.resized.connect(
+ lambda e: dummyToolbar2.setFixedHeight(e.size().height())
+ )
dataLayout.addWidget(dummyToolbar2)
dataLayout.addWidget(dataFrame)
@@ -869,7 +900,9 @@ class AbstractDataFileDialog(qt.QDialog):
dummyCombo2.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
dummyCombo2.setFixedHeight(self.__fileTypeCombo.height())
self.__resizeToolbar = _CatchResizeEvent(self, self.__fileTypeCombo)
- self.__resizeToolbar.resized.connect(lambda e: dummyCombo2.setFixedHeight(e.size().height()))
+ self.__resizeToolbar.resized.connect(
+ lambda e: dummyCombo2.setFixedHeight(e.size().height())
+ )
dataLayout.addWidget(dummyCombo2)
dataSelection.setLayout(dataLayout)
@@ -903,7 +936,10 @@ class AbstractDataFileDialog(qt.QDialog):
def __navigateForward(self):
"""Navigate through the history one step forward."""
- if len(self.__currentHistory) > 0 and self.__currentHistoryLocation < len(self.__currentHistory) - 1:
+ if (
+ len(self.__currentHistory) > 0
+ and self.__currentHistoryLocation < len(self.__currentHistory) - 1
+ ):
self.__currentHistoryLocation += 1
url = self.__currentHistory[self.__currentHistoryLocation]
self.selectUrl(url)
@@ -970,7 +1006,7 @@ class AbstractDataFileDialog(qt.QDialog):
self.__listViewAction.setChecked(True)
self.__detailViewAction.setChecked(False)
else:
- assert(False)
+ assert False
def __showAsListView(self):
self.setViewMode(qt.QFileDialog.List)
@@ -1004,7 +1040,7 @@ class AbstractDataFileDialog(qt.QDialog):
if silx.io.is_group(obj):
self.__browser.setRootIndex(index)
else:
- assert(False)
+ assert False
def __browsedItemSelected(self, index):
self.__dataSelected(index)
@@ -1019,7 +1055,7 @@ class AbstractDataFileDialog(qt.QDialog):
:param str path: Path to load
"""
- assert(path is not None)
+ assert path is not None
if path != "" and not os.path.exists(path):
return
if self.hasPendingEvents():
@@ -1101,8 +1137,7 @@ class AbstractDataFileDialog(qt.QDialog):
return True
def __isSilxHavePriority(self, filename):
- """Silx have priority when there is a specific decoder
- """
+ """Silx have priority when there is a specific decoder"""
_, ext = os.path.splitext(filename)
ext = "*%s" % ext
formats = silx.io.supported_extensions(flat_formats=False)
@@ -1165,14 +1200,17 @@ class AbstractDataFileDialog(qt.QDialog):
if os.path.isfile(path):
codec = self.__fileTypeCombo.currentCodec()
is_fabio_decoder = codec.is_fabio_codec()
- is_fabio_have_priority = not codec.is_silx_codec() and not self.__isSilxHavePriority(path)
+ is_fabio_have_priority = (
+ not codec.is_silx_codec()
+ and not self.__isSilxHavePriority(path)
+ )
if is_fabio_decoder or is_fabio_have_priority:
# Then it's flat frame container
self.__openFabioFile(path)
if self.__fabio is not None:
selectedData = _FabioData(self.__fabio)
else:
- assert(False)
+ assert False
self.__setData(selectedData)
@@ -1192,7 +1230,9 @@ class AbstractDataFileDialog(qt.QDialog):
self.__setSelectedData(data)
self.__selectorWidget.hide()
else:
- self.__selectorWidget.setVisible(self.__selectorWidget.hasVisibleSelectors())
+ self.__selectorWidget.setVisible(
+ self.__selectorWidget.hasVisibleSelectors()
+ )
# Needed to fake the fact we have to reset the zoom in preview
self.__selectedData = None
self.__selectorWidget.selectionChanged.emit()
@@ -1265,7 +1305,10 @@ class AbstractDataFileDialog(qt.QDialog):
def __updateDataInfo(self):
if self.__errorWhileLoadingFile is not None:
filename, message = self.__errorWhileLoadingFile
- message = "<b>Error while loading file '%s'</b><hr/>%s" % (filename, message)
+ message = "<b>Error while loading file '%s'</b><hr/>%s" % (
+ filename,
+ message,
+ )
size = self.__dataInfo.height()
icon = self.style().standardIcon(qt.QStyle.SP_MessageBoxCritical)
pixmap = icon.pixmap(size, size)
@@ -1316,7 +1359,11 @@ class AbstractDataFileDialog(qt.QDialog):
filename = ""
dataPath = None
- if useSelectorWidget and self.__selectorWidget is not None and self.__selectorWidget.isUsed():
+ if (
+ useSelectorWidget
+ and self.__selectorWidget is not None
+ and self.__selectorWidget.isUsed()
+ ):
slicing = self.__selectorWidget.slicing()
if slicing == tuple():
slicing = None
@@ -1339,7 +1386,9 @@ class AbstractDataFileDialog(qt.QDialog):
else:
scheme = None
- url = silx.io.url.DataUrl(file_path=filename, data_path=dataPath, data_slice=slicing, scheme=scheme)
+ url = silx.io.url.DataUrl(
+ file_path=filename, data_path=dataPath, data_slice=slicing, scheme=scheme
+ )
return url
def __updatePath(self):
@@ -1361,7 +1410,9 @@ class AbstractDataFileDialog(qt.QDialog):
if currentUrl is None or currentUrl != url.path():
# clean up the forward history
- self.__currentHistory = self.__currentHistory[0:self.__currentHistoryLocation + 1]
+ self.__currentHistory = self.__currentHistory[
+ 0 : self.__currentHistoryLocation + 1
+ ]
self.__currentHistory.append(url.path())
self.__currentHistoryLocation += 1
@@ -1399,15 +1450,16 @@ class AbstractDataFileDialog(qt.QDialog):
selectionModel.selectionChanged.connect(self.__shortcutSelected)
def __updateActionHistory(self):
- self.__forwardAction.setEnabled(len(self.__currentHistory) - 1 > self.__currentHistoryLocation)
+ self.__forwardAction.setEnabled(
+ len(self.__currentHistory) - 1 > self.__currentHistoryLocation
+ )
self.__backwardAction.setEnabled(self.__currentHistoryLocation > 0)
def __textChanged(self, text):
self.__pathChanged()
def _isFabioFilesSupported(self):
- """Returns true fabio files can be loaded.
- """
+ """Returns true fabio files can be loaded."""
return True
def _isLoadableUrl(self, url):
@@ -1478,7 +1530,7 @@ class AbstractDataFileDialog(qt.QDialog):
# data = _FabioData(self.__fabio)
# self.__setData(data)
else:
- assert(False)
+ assert False
else:
self.__browser.setRootIndex(index, model=self.__fileModel)
self.__clearData()
@@ -1614,7 +1666,7 @@ class AbstractDataFileDialog(qt.QDialog):
"""
if len(self.__currentHistory) <= 1:
return []
- history = self.__currentHistory[0:self.__currentHistoryLocation]
+ history = self.__currentHistory[0 : self.__currentHistoryLocation]
return list(history)
def setHistory(self, history):
@@ -1669,12 +1721,18 @@ class AbstractDataFileDialog(qt.QDialog):
qualifiedName = stream.readQString()
if qualifiedName != self.qualifiedName():
- _logger.warning("Stored state contains an invalid qualified name. %s restoration cancelled.", self.__class__.__name__)
+ _logger.warning(
+ "Stored state contains an invalid qualified name. %s restoration cancelled.",
+ self.__class__.__name__,
+ )
return False
version = stream.readInt32()
if version != self.__serialVersion:
- _logger.warning("Stored state contains an invalid version. %s restoration cancelled.", self.__class__.__name__)
+ _logger.warning(
+ "Stored state contains an invalid version. %s restoration cancelled.",
+ self.__class__.__name__,
+ )
return False
result = True
@@ -1695,7 +1753,7 @@ class AbstractDataFileDialog(qt.QDialog):
if workingDirectory is not None:
self.setDirectory(workingDirectory)
result &= self.__browser.restoreState(browserData)
- self.setViewMode(viewMode)
+ self.setViewMode(qt.QFileDialog.ViewMode(viewMode))
colormap = self.colormap()
if colormap is not None:
result &= self.colormap().restoreState(colormapData)
@@ -1712,16 +1770,19 @@ class AbstractDataFileDialog(qt.QDialog):
stream = qt.QDataStream(data, qt.QIODevice.WriteOnly)
s = self.qualifiedName()
- stream.writeQString(u"%s" % s)
+ stream.writeQString("%s" % s)
stream.writeInt32(self.__serialVersion)
stream.writeQVariant(self.__splitter.saveState())
- strings = [u"%s" % s.toString() for s in self.sidebarUrls()]
+ strings = ["%s" % s.toString() for s in self.sidebarUrls()]
stream.writeQStringList(strings)
- strings = [u"%s" % s for s in self.history()]
+ strings = ["%s" % s for s in self.history()]
stream.writeQStringList(strings)
- stream.writeQString(u"%s" % self.directory())
+ stream.writeQString("%s" % self.directory())
stream.writeQVariant(self.__browser.saveState())
- stream.writeInt32(self.viewMode())
+ viewMode = self.viewMode()
+ if qt.BINDING in ("PyQt6", "PySide6"): # No auto conversion to int
+ viewMode = viewMode.value
+ stream.writeInt32(viewMode)
colormap = self.colormap()
if colormap is not None:
stream.writeQVariant(self.colormap().saveState())
diff --git a/src/silx/gui/dialog/ColormapDialog.py b/src/silx/gui/dialog/ColormapDialog.py
index 2506e2a..75ab39e 100644
--- a/src/silx/gui/dialog/ColormapDialog.py
+++ b/src/silx/gui/dialog/ColormapDialog.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -59,6 +58,8 @@ The updates of the colormap description are also available through the signal:
:attr:`ColormapDialog.sigColormapChanged`.
""" # noqa
+from __future__ import annotations
+
__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
__date__ = "08/12/2020"
@@ -81,11 +82,11 @@ from silx.gui.plot import items
from silx.gui import icons
from silx.gui.qt import inspect as qtinspect
from silx.gui.widgets.ColormapNameComboBox import ColormapNameComboBox
-from silx.gui.widgets.WaitingPushButton import WaitingPushButton
+from silx.gui.widgets.FormGridLayout import FormGridLayout
from silx.math.histogram import Histogramnd
-from silx.utils import deprecation
from silx.gui.plot.items.roi import RectangleROI
from silx.gui.plot.tools.roi import RegionOfInterestManager
+from silx.utils.enum import Enum as _Enum
_logger = logging.getLogger(__name__)
@@ -128,8 +129,22 @@ class _BoundaryWidget(qt.QWidget):
self.setLayout(qt.QHBoxLayout())
self.layout().setContentsMargins(0, 0, 0, 0)
self._numVal = FloatEdit(parent=self, value=value)
+
+ self._iconAuto = icons.getQIcon("scale-auto")
+ self._iconFixed = icons.getQIcon("scale-fixed")
+
+ self._autoToggleAction = qt.QAction(self)
+ self._autoToggleAction.setText("Auto scale")
+ self._autoToggleAction.setToolTip("Toggle auto scale")
+ self._autoToggleAction.setCheckable(True)
+ self._autoToggleAction.setIcon(self._iconFixed)
+ self._autoToggleAction.setChecked(False)
+ self._autoToggleAction.toggled.connect(self._autoToggled)
+
+ self._numVal.addAction(self._autoToggleAction, qt.QLineEdit.LeadingPosition)
+
self.layout().addWidget(self._numVal)
- self._autoCB = qt.QCheckBox('auto', parent=self)
+ self._autoCB = qt.QCheckBox("auto", parent=self)
self.layout().addWidget(self._autoCB)
self._autoCB.setChecked(False)
self._autoCB.setVisible(False)
@@ -174,7 +189,7 @@ class _BoundaryWidget(qt.QWidget):
return self._numVal.value()
def _autoToggled(self, enabled):
- self._numVal.setEnabled(not enabled)
+ self._updateAutoScaleState(enabled)
self._updateDisplayedText()
self.sigAutoScaleChanged.emit(enabled)
@@ -198,14 +213,26 @@ class _BoundaryWidget(qt.QWidget):
if not self.__textWasEdited:
self._numVal.setValue(value)
self.__realValue = value
- self._numVal.setEnabled(not isAuto)
+ self._updateAutoScaleState(isAuto)
+
+ def _updateAutoScaleState(self, isAutoScale):
+ self._numVal.setReadOnly(isAutoScale)
+ palette = qt.QPalette()
+ if isAutoScale:
+ color = palette.color(qt.QPalette.Disabled, qt.QPalette.Base)
+ icon = self._iconAuto
+ else:
+ color = palette.color(qt.QPalette.Active, qt.QPalette.Base)
+ icon = self._iconFixed
+ palette.setColor(qt.QPalette.Base, color)
+ self._numVal.setPalette(palette)
+ self._autoToggleAction.setIcon(icon)
class _AutoscaleModeComboBox(qt.QComboBox):
-
DATA = {
Colormap.MINMAX: ("Min/max", "Use the data min/max"),
- Colormap.STDDEV3: ("Mean ± 3 × stddev", "Use the data mean ± 3 × standard deviation"),
+ Colormap.STDDEV3: ("Mean±3std", "Use the data mean ± 3 × standard deviation"),
}
def __init__(self, parent: qt.QWidget):
@@ -248,92 +275,44 @@ class _AutoscaleModeComboBox(qt.QComboBox):
self.setCurrentIndex(self.count() - 1)
-class _AutoScaleButtons(qt.QWidget):
-
+class _AutoScaleButton(qt.QPushButton):
autoRangeChanged = qt.Signal(object)
def __init__(self, parent=None):
- qt.QWidget.__init__(self, parent=parent)
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- self.setFocusPolicy(qt.Qt.NoFocus)
-
- self._bothAuto = qt.QPushButton(self)
- self._bothAuto.setText("Autoscale")
- self._bothAuto.setToolTip("Enable/disable the autoscale for both min and max")
- self._bothAuto.setCheckable(True)
- self._bothAuto.toggled[bool].connect(self.__bothToggled)
- self._bothAuto.setFocusPolicy(qt.Qt.TabFocus)
-
- self._minAuto = qt.QCheckBox(self)
- self._minAuto.setText("")
- self._minAuto.setToolTip("Enable/disable the autoscale for min")
- self._minAuto.toggled[bool].connect(self.__minToggled)
- self._minAuto.setFocusPolicy(qt.Qt.TabFocus)
-
- self._maxAuto = qt.QCheckBox(self)
- self._maxAuto.setText("")
- self._maxAuto.setToolTip("Enable/disable the autoscale for max")
- self._maxAuto.toggled[bool].connect(self.__maxToggled)
- self._maxAuto.setFocusPolicy(qt.Qt.TabFocus)
-
- layout.addStretch(1)
- layout.addWidget(self._minAuto)
- layout.addSpacing(20)
- layout.addWidget(self._bothAuto)
- layout.addSpacing(20)
- layout.addWidget(self._maxAuto)
- layout.addStretch(1)
-
- def __bothToggled(self, checked):
+ qt.QPushButton.__init__(self, parent=parent)
+ self.setText("Autoscale")
+ self.setToolTip("Enable/disable the autoscale for both min and max")
+ self.setCheckable(True)
+ self.toggled[bool].connect(self.__toggled)
+ self.setFocusPolicy(qt.Qt.TabFocus)
+
+ def __toggled(self, checked):
autoRange = checked, checked
self.setAutoRange(autoRange)
self.autoRangeChanged.emit(autoRange)
- def __minToggled(self, checked):
- autoRange = self.getAutoRange()
- self.setAutoRange(autoRange)
- self.autoRangeChanged.emit(autoRange)
-
- def __maxToggled(self, checked):
- autoRange = self.getAutoRange()
- self.setAutoRange(autoRange)
- self.autoRangeChanged.emit(autoRange)
-
def setAutoRangeFromColormap(self, colormap):
vRange = colormap.getVRange()
autoRange = vRange[0] is None, vRange[1] is None
self.setAutoRange(autoRange)
def setAutoRange(self, autoRange):
- if autoRange[0] == autoRange[1]:
- with utils.blockSignals(self._bothAuto):
- self._bothAuto.setChecked(autoRange[0])
- else:
- with utils.blockSignals(self._bothAuto):
- self._bothAuto.setChecked(False)
- with utils.blockSignals(self._minAuto):
- self._minAuto.setChecked(autoRange[0])
- with utils.blockSignals(self._maxAuto):
- self._maxAuto.setChecked(autoRange[1])
-
- def getAutoRange(self):
- return self._minAuto.isChecked(), self._maxAuto.isChecked()
+ with utils.blockSignals(self):
+ self.setChecked(autoRange[0] if autoRange[0] == autoRange[1] else False)
@enum.unique
-class _DataInPlotMode(enum.Enum):
+class DisplayMode(_Enum):
"""Enum for each mode of display of the data in the plot."""
- RANGE = 'range'
- HISTOGRAM = 'histogram'
+
+ RANGE = "range"
+ HISTOGRAM = "histogram"
class _ColormapHistogram(qt.QWidget):
"""Display the colormap and the data as a plot."""
- sigRangeMoving = qt.Signal(object, object)
+ sigRangeMoving = qt.Signal(object, object, object)
"""Emitted when a mouse interaction moves the location
of the colormap range in the plot.
@@ -341,27 +320,29 @@ class _ColormapHistogram(qt.QWidget):
- vmin: A float value if this range was moved, else None
- vmax: A float value if this range was moved, else None
+ - gammaPos: A float value if this range was moved, else None
"""
- sigRangeMoved = qt.Signal(object, object)
+ sigRangeMoved = qt.Signal(object, object, object)
"""Emitted when a mouse interaction stop.
This signal contains 2 elements:
- vmin: A float value if this range was moved, else None
- vmax: A float value if this range was moved, else None
+ - gammaPos: A float value if this range was moved, else None
"""
def __init__(self, parent):
qt.QWidget.__init__(self, parent=parent)
- self._dataInPlotMode = _DataInPlotMode.RANGE
+ self._displayMode = DisplayMode.RANGE
self._finiteRange = None, None
self._initPlot()
self._histogramData = {}
"""Histogram displayed in the plot"""
- self._dragging = False, False
+ self._dragging = False, False, False
"""True, if the min or the max handle is dragging"""
self._dataRange = {}
@@ -371,7 +352,7 @@ class _ColormapHistogram(qt.QWidget):
def paintEvent(self, event):
if self._invalidated:
- self._updateDataInPlot()
+ self._updateDisplayMode()
self._invalidated = False
self._updateMarkerPosition()
return super(_ColormapHistogram, self).paintEvent(event)
@@ -440,7 +421,9 @@ class _ColormapHistogram(qt.QWidget):
dataRange = self._getNormalizedDataRange()
if dataRange[0] is None or dataRange[1] is None:
return None, None
- counts, edges = self.parent().computeHistogram(data, scale=norm, dataRange=dataRange)
+ counts, edges = self.parent().computeHistogram(
+ data, scale=norm, dataRange=dataRange
+ )
return counts, edges
def _getNormalizedDataRange(self):
@@ -528,25 +511,26 @@ class _ColormapHistogram(qt.QWidget):
def _initPlot(self):
"""Init the plot to display the range and the values"""
self._plot = PlotWidget(self)
- self._plot.setDataMargins(0.125, 0.125, 0.125, 0.125)
+ self._plot.setAxesDisplayed(False)
+ self._plot.setDataMargins(0.125, 0.125, 0.01, 0.01)
self._plot.getXAxis().setLabel("Data Values")
self._plot.getYAxis().setLabel("")
- self._plot.setInteractiveMode('select', zoomOnWheel=False)
+ self._plot.setInteractiveMode("select", zoomOnWheel=False)
self._plot.setActiveCurveHandling(False)
self._plot.setMinimumSize(qt.QSize(250, 200))
self._plot.sigPlotSignal.connect(self._plotEventReceived)
palette = self.palette()
- color = palette.color(qt.QPalette.Normal, qt.QPalette.Window)
+ color = palette.color(qt.QPalette.Active, qt.QPalette.Window)
self._plot.setBackgroundColor(color)
self._plot.setDataBackgroundColor("white")
lut = numpy.arange(256)
lut.shape = 1, -1
- self._plot.addImage(lut, legend='lut')
+ self._plot.addImage(lut, legend="lut")
self._lutItem = self._plot._getItem("image", "lut")
self._lutItem.setVisible(False)
- self._plot.addScatter(x=[], y=[], value=[], legend='lut2')
+ self._plot.addScatter(x=[], y=[], value=[], legend="lut2")
self._lutItem2 = self._plot._getItem("scatter", "lut2")
self._lutItem2.setVisible(False)
self.__lutY = numpy.array([-0.05] * 256)
@@ -566,24 +550,32 @@ class _ColormapHistogram(qt.QWidget):
group = qt.QActionGroup(self._plotToolbar)
group.setExclusive(True)
-
+ # data range mode
action = qt.QAction("Data range", self)
- action.setToolTip("Display the data range within the colormap range. A fast data processing have to be done.")
- action.setIcon(icons.getQIcon('colormap-range'))
+ action.setToolTip(
+ "Display the data range within the colormap range. A fast data processing have to be done."
+ )
+ action.setIcon(icons.getQIcon("colormap-range"))
action.setCheckable(True)
- action.setData(_DataInPlotMode.RANGE)
- action.setChecked(action.data() == self._dataInPlotMode)
+ action.setData(DisplayMode.RANGE)
+ action.setChecked(action.data() == self._displayMode)
self._plotToolbar.addAction(action)
group.addAction(action)
+ self._dataRangeAction = action
+ # histogram mode
action = qt.QAction("Histogram", self)
- action.setToolTip("Display the data histogram within the colormap range. A slow data processing have to be done. ")
- action.setIcon(icons.getQIcon('colormap-histogram'))
+ action.setToolTip(
+ "Display the data histogram within the colormap range. A slow data processing have to be done. "
+ )
+ action.setIcon(icons.getQIcon("colormap-histogram"))
action.setCheckable(True)
- action.setData(_DataInPlotMode.HISTOGRAM)
- action.setChecked(action.data() == self._dataInPlotMode)
+ action.setData(DisplayMode.HISTOGRAM)
+ action.setChecked(action.data() == self._displayMode)
self._plotToolbar.addAction(action)
group.addAction(action)
- group.triggered.connect(self._displayDataInPlotModeChanged)
+ self._dataHistogramAction = action
+ group.setExclusive(True)
+ group.triggered.connect(self._displayModeChanged)
plotBoxLayout = qt.QHBoxLayout()
plotBoxLayout.setContentsMargins(0, 0, 0, 0)
@@ -595,25 +587,31 @@ class _ColormapHistogram(qt.QWidget):
def _plotEventReceived(self, event):
"""Handle events from the plot"""
- kind = event['event']
+ kind = event["event"]
- if kind == 'markerMoving':
- value = event['xdata']
- if event['label'] == 'Min':
- self._dragging = True, False
+ if kind == "markerMoving":
+ value = event["xdata"]
+ if event["label"] == "Min":
+ self._dragging = True, False, False
self._finiteRange = value, self._finiteRange[1]
- self._last = value, None
+ self._last = value, None, None
+ self._updateGammaPosition()
self.sigRangeMoving.emit(*self._last)
- elif event['label'] == 'Max':
- self._dragging = False, True
+ elif event["label"] == "Max":
+ self._dragging = False, True, False
self._finiteRange = self._finiteRange[0], value
- self._last = None, value
+ self._last = None, value, None
+ self._updateGammaPosition()
+ self.sigRangeMoving.emit(*self._last)
+ elif event["label"] == "Gamma":
+ self._dragging = False, False, True
+ self._last = None, None, value
self.sigRangeMoving.emit(*self._last)
self._updateLutItem(self._finiteRange)
- elif kind == 'markerMoved':
+ elif kind == "markerMoved":
self.sigRangeMoved.emit(*self._last)
self._plot.resetZoom()
- self._dragging = False, False
+ self._dragging = False, False, False
else:
pass
@@ -630,23 +628,62 @@ class _ColormapHistogram(qt.QWidget):
if posMin is not None and not self._dragging[0]:
self._plot.addXMarker(
posMin,
- legend='Min',
- text='Min',
+ legend="Min",
+ text="Min",
draggable=isDraggable,
color="blue",
- constraint=self._plotMinMarkerConstraint)
+ constraint=self._plotMinMarkerConstraint,
+ )
+ self._updateGammaPosition()
if posMax is not None and not self._dragging[1]:
self._plot.addXMarker(
posMax,
- legend='Max',
- text='Max',
+ legend="Max",
+ text="\n\nMax",
draggable=isDraggable,
color="blue",
- constraint=self._plotMaxMarkerConstraint)
+ constraint=self._plotMaxMarkerConstraint,
+ )
self._updateLutItem((posMin, posMax))
self._plot.resetZoom()
+ def _updateGammaPosition(self):
+ colormap = self.getColormap()
+ posMin, posMax = self._getDisplayableRange()
+
+ if colormap is None:
+ gamma = None
+ else:
+ if colormap.getNormalization() == Colormap.GAMMA:
+ gamma = colormap.getGammaNormalizationParameter()
+ else:
+ gamma = None
+
+ if gamma is not None:
+ if not self._dragging[2]:
+ posRange = posMax - posMin
+ if posRange > 0:
+ gammaPos = posMin + posRange * 0.5 ** (1 / gamma)
+ else:
+ gammaPos = posMin
+ marker = self._plot._getMarker(
+ self._plot.addXMarker(
+ gammaPos,
+ legend="Gamma",
+ text="\nGamma",
+ draggable=True,
+ color="blue",
+ constraint=self._plotGammaMarkerConstraint,
+ )
+ )
+ marker.setZValue(2)
+ else:
+ try:
+ self._plot.removeMarker("Gamma")
+ except Exception:
+ pass
+
def _updateLutItem(self, vRange):
colormap = self.getColormap()
if colormap is None:
@@ -680,10 +717,9 @@ class _ColormapHistogram(qt.QWidget):
self._lutItem2.setVisible(False)
self._lutItem2.setColormap(normColormap)
xx = numpy.geomspace(posMin, posMax, 256)
- self._lutItem2.setData(x=xx,
- y=self.__lutY,
- value=self.__lutV,
- copy=False)
+ self._lutItem2.setData(
+ x=xx, y=self.__lutY, value=self.__lutV, copy=False
+ )
self._lutItem2.setSymbol("|")
self._lutItem2.setVisible(True)
self._lutItem.setVisible(False)
@@ -694,10 +730,8 @@ class _ColormapHistogram(qt.QWidget):
self._lutItem2.setColormap(normColormap)
xx = numpy.linspace(posMin, posMax, 256, endpoint=True)
self._lutItem2.setData(
- x=xx,
- y=self.__lutY,
- value=self.__lutV,
- copy=False)
+ x=xx, y=self.__lutY, value=self.__lutV, copy=False
+ )
self._lutItem2.setSymbol("|")
self._lutItem2.setVisible(True)
self._lutItem.setVisible(False)
@@ -718,15 +752,38 @@ class _ColormapHistogram(qt.QWidget):
return x, y
return max(x, vmin), y
- def _setDataInPlotMode(self, mode):
- if self._dataInPlotMode == mode:
+ def _plotGammaMarkerConstraint(self, x, y):
+ """Constraint of the gamma marker"""
+ vmin, vmax = self.getFiniteRange()
+ if vmin is not None:
+ x = max(x, vmin)
+ if vmax is not None:
+ x = min(x, vmax)
+ return x, y
+
+ def setDisplayMode(self, mode: str | DisplayMode):
+ mode = DisplayMode.from_value(mode)
+ if mode is DisplayMode.HISTOGRAM:
+ action = self._dataHistogramAction
+ elif mode is DisplayMode.RANGE:
+ action = self._dataRangeAction
+ else:
+ raise ValueError("Mode not supported")
+ action.setChecked(True)
+ self._displayModeChanged(action)
+
+ def _setDisplayMode(self, mode):
+ if self._displayMode == mode:
return
- self._dataInPlotMode = mode
- self._updateDataInPlot()
+ self._displayMode = mode
+ self._updateDisplayMode()
- def _displayDataInPlotModeChanged(self, action):
+ def getDsiplayMode(self) -> DisplayMode:
+ return self._displayMode
+
+ def _displayModeChanged(self, action):
mode = action.data()
- self._setDataInPlotMode(mode)
+ self._setDisplayMode(mode)
def invalidateData(self):
self._histogramData = {}
@@ -734,8 +791,8 @@ class _ColormapHistogram(qt.QWidget):
self._invalidated = True
self.update()
- def _updateDataInPlot(self):
- mode = self._dataInPlotMode
+ def _updateDisplayMode(self):
+ mode = self._displayMode
norm = self._getNorm()
if norm == Colormap.LINEAR:
@@ -748,38 +805,42 @@ class _ColormapHistogram(qt.QWidget):
axis = self._plot.getXAxis()
axis.setScale(scale)
- if mode == _DataInPlotMode.RANGE:
+ if mode == DisplayMode.RANGE:
dataRange = self._getNormalizedDataRange()
xmin, xmax = dataRange
if xmax is None or xmin is None:
- self._plot.remove(legend='Data', kind='histogram')
+ self._plot.remove(legend="Data", kind="histogram")
else:
histogram = numpy.array([1])
bin_edges = numpy.array([xmin, xmax])
- self._plot.addHistogram(histogram,
- bin_edges,
- legend="Data",
- color='gray',
- align='center',
- fill=True,
- z=1)
-
- elif mode == _DataInPlotMode.HISTOGRAM:
+ self._plot.addHistogram(
+ histogram,
+ bin_edges,
+ legend="Data",
+ color="gray",
+ align="center",
+ fill=True,
+ z=1,
+ )
+
+ elif mode == DisplayMode.HISTOGRAM:
histogram, bin_edges = self._getNormalizedHistogram()
if histogram is None or bin_edges is None:
- self._plot.remove(legend='Data', kind='histogram')
+ self._plot.remove(legend="Data", kind="histogram")
else:
histogram = numpy.array(histogram, copy=True)
bin_edges = numpy.array(bin_edges, copy=True)
- with numpy.errstate(invalid='ignore'):
+ with numpy.errstate(invalid="ignore"):
norm_histogram = histogram / numpy.nanmax(histogram)
- self._plot.addHistogram(norm_histogram,
- bin_edges,
- legend="Data",
- color='gray',
- align='center',
- fill=True,
- z=1)
+ self._plot.addHistogram(
+ norm_histogram,
+ bin_edges,
+ legend="Data",
+ color="gray",
+ align="center",
+ fill=True,
+ z=1,
+ )
else:
_logger.error("Mode unsupported")
@@ -798,7 +859,7 @@ class _ColormapHistogram(qt.QWidget):
return norm
def updateNormalization(self):
- self._updateDataInPlot()
+ self._updateDisplayMode()
self.update()
@@ -829,6 +890,9 @@ class ColormapDialog(qt.QDialog):
self._item = None
"""Weak ref to an external item"""
+ self._colormapped = None
+ """Weak ref to reduce data update"""
+
self._colormapChange = utils.LockReentrant()
"""Used as a semaphore to avoid editing the colormap object when we are
only attempt to display it.
@@ -852,16 +916,19 @@ class ColormapDialog(qt.QDialog):
# Colormap row
self._comboBoxColormap = ColormapNameComboBox(parent=self)
- self._comboBoxColormap.currentIndexChanged[int].connect(self._comboBoxColormapUpdated)
+ self._comboBoxColormap.currentIndexChanged[int].connect(
+ self._comboBoxColormapUpdated
+ )
# Normalization row
self._comboBoxNormalization = qt.QComboBox(parent=self)
normalizations = [
- ('Linear', Colormap.LINEAR),
- ('Gamma correction', Colormap.GAMMA),
- ('Arcsinh', Colormap.ARCSINH),
- ('Logarithmic', Colormap.LOGARITHM),
- ('Square root', Colormap.SQRT)]
+ ("Linear", Colormap.LINEAR),
+ ("Gamma correction", Colormap.GAMMA),
+ ("Arcsinh", Colormap.ARCSINH),
+ ("Logarithmic", Colormap.LOGARITHM),
+ ("Square root", Colormap.SQRT),
+ ]
for name, userData in normalizations:
try:
icon = icons.getQIcon("colormap-norm-%s" % userData)
@@ -869,11 +936,12 @@ class ColormapDialog(qt.QDialog):
icon = qt.QIcon()
self._comboBoxNormalization.addItem(icon, name, userData)
self._comboBoxNormalization.currentIndexChanged[int].connect(
- self._normalizationUpdated)
+ self._normalizationUpdated
+ )
self._gammaSpinBox = qt.QDoubleSpinBox(parent=self)
self._gammaSpinBox.setEnabled(False)
- self._gammaSpinBox.setRange(0., 1000.)
+ self._gammaSpinBox.setRange(0.01, 100.0)
self._gammaSpinBox.setDecimals(4)
if hasattr(qt.QDoubleSpinBox, "setStepType"):
# Introduced in Qt 5.12
@@ -881,7 +949,7 @@ class ColormapDialog(qt.QDialog):
else:
self._gammaSpinBox.setSingleStep(0.1)
self._gammaSpinBox.valueChanged.connect(self._gammaUpdated)
- self._gammaSpinBox.setValue(2.)
+ self._gammaSpinBox.setValue(2.0)
autoScaleCombo = _AutoscaleModeComboBox(self)
autoScaleCombo.currentIndexChanged.connect(self._autoscaleModeUpdated)
@@ -891,13 +959,15 @@ class ColormapDialog(qt.QDialog):
self._minValue = _BoundaryWidget(parent=self, value=1.0)
self._minValue.sigAutoScaleChanged.connect(self._minAutoscaleUpdated)
self._minValue.sigValueChanged.connect(self._minValueUpdated)
+ self._minValue.setMinimumWidth(140)
# Max row
self._maxValue = _BoundaryWidget(parent=self, value=10.0)
self._maxValue.sigAutoScaleChanged.connect(self._maxAutoscaleUpdated)
self._maxValue.sigValueChanged.connect(self._maxValueUpdated)
+ self._maxValue.setMinimumWidth(140)
- self._autoButtons = _AutoScaleButtons(self)
+ self._autoButtons = _AutoScaleButton(self)
self._autoButtons.autoRangeChanged.connect(self._autoRangeButtonsUpdated)
rangeLayout = qt.QGridLayout()
@@ -909,36 +979,43 @@ class ColormapDialog(qt.QDialog):
labelMax = qt.QLabel("Max", self)
labelMax.setAlignment(qt.Qt.AlignHCenter)
labelMax.setFont(miniFont)
- rangeLayout.addWidget(labelMin, 0, 0)
- rangeLayout.addWidget(labelMax, 0, 1)
- rangeLayout.addWidget(self._minValue, 1, 0)
- rangeLayout.addWidget(self._maxValue, 1, 1)
- rangeLayout.addWidget(self._autoButtons, 2, 0, 1, -1, qt.Qt.AlignCenter)
+ rangeLayout.addWidget(labelMin, 0, 1)
+ rangeLayout.addWidget(labelMax, 0, 3)
+ rangeLayout.addWidget(self._minValue, 1, 1)
+ rangeLayout.addWidget(self._maxValue, 1, 3)
+ rangeLayout.setColumnStretch(0, 1)
+ rangeLayout.setColumnStretch(1, 2)
+ rangeLayout.setColumnStretch(2, 1)
+ rangeLayout.setColumnStretch(3, 2)
+ rangeLayout.setColumnStretch(4, 1)
self._histoWidget = _ColormapHistogram(self)
self._histoWidget.sigRangeMoving.connect(self._histogramRangeMoving)
self._histoWidget.sigRangeMoved.connect(self._histogramRangeMoved)
+ self._histoWidget.setSizePolicy(
+ qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding
+ )
# Scale to buttons
self._visibleAreaButton = qt.QPushButton(self)
self._visibleAreaButton.setEnabled(False)
self._visibleAreaButton.setText("Visible Area")
self._visibleAreaButton.clicked.connect(
- self._handleScaleToVisibleAreaClicked,
- type=qt.Qt.QueuedConnection)
+ self._handleScaleToVisibleAreaClicked, type=qt.Qt.QueuedConnection
+ )
# Place-holder for selected area ROI manager
self._roiForColormapManager = None
- self._selectedAreaButton = WaitingPushButton(self)
+ self._selectedAreaButton = qt.QPushButton(self)
+ self._selectedAreaButton.setCheckable(True)
self._selectedAreaButton.setEnabled(False)
self._selectedAreaButton.setText("Selection")
self._selectedAreaButton.setIcon(icons.getQIcon("add-shape-rectangle"))
self._selectedAreaButton.setCheckable(True)
- self._selectedAreaButton.setDisabledWhenWaiting(False)
self._selectedAreaButton.toggled.connect(
- self._handleScaleToSelectionToggled,
- type=qt.Qt.QueuedConnection)
+ self._handleScaleToSelectionToggled, type=qt.Qt.QueuedConnection
+ )
# define modal buttons
types = qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel
@@ -961,36 +1038,48 @@ class ColormapDialog(qt.QDialog):
self._buttonsNonModal.setFocus(qt.Qt.OtherFocusReason)
# Set the colormap to default values
- self.setColormap(Colormap(name='gray', normalization='linear',
- vmin=None, vmax=None))
+ self.setColormap(
+ Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
+ )
self.setModal(self.isModal())
- formLayout = qt.QFormLayout(self)
- formLayout.setContentsMargins(10, 10, 10, 10)
- formLayout.addRow('Colormap:', self._comboBoxColormap)
- formLayout.addRow('Normalization:', self._comboBoxNormalization)
- formLayout.addRow('Gamma:', self._gammaSpinBox)
- formLayout.addRow(self._histoWidget)
- formLayout.addRow(rangeLayout)
- label = qt.QLabel('Mode:', self)
- self._autoscaleModeLabel = label
- label.setToolTip("Mode for autoscale. Algorithm used to find range in auto scale.")
- formLayout.addItem(qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed))
- formLayout.addRow(label, autoScaleCombo)
-
layout = qt.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._visibleAreaButton)
layout.addWidget(self._selectedAreaButton)
- self._scaleToAreaGroup = qt.QGroupBox('Scale to:', self)
+ layout.addStretch()
+ self._scaleToAreaGroup = qt.QWidget(self)
self._scaleToAreaGroup.setLayout(layout)
self._scaleToAreaGroup.setVisible(False)
- formLayout.addRow(self._scaleToAreaGroup)
+ layoutScale = qt.QHBoxLayout()
+ layoutScale.setContentsMargins(0, 0, 0, 0)
+ layoutScale.addWidget(self._autoButtons)
+ layoutScale.addWidget(self._autoScaleCombo)
+ layoutScale.addStretch()
+
+ formLayout = FormGridLayout(self)
+ formLayout.setContentsMargins(10, 10, 10, 10)
+
+ formLayout.addRow("Colormap:", self._comboBoxColormap)
+ formLayout.addRow("Normalization:", self._comboBoxNormalization)
+ formLayout.addRow("Gamma:", self._gammaSpinBox)
+
+ formLayout.addItem(
+ qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
+ )
+ formLayout.addRow(self._histoWidget)
+ formLayout.setRowStretch(formLayout.rowCount() - 1, 1)
+ formLayout.addRow(rangeLayout)
+ formLayout.addItem(
+ qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
+ )
+ formLayout.addRow("Scale:", layoutScale)
+ formLayout.addRow("Fixed scale on:", self._scaleToAreaGroup)
formLayout.addRow(self._buttonsModal)
formLayout.addRow(self._buttonsNonModal)
- formLayout.setSizeConstraint(qt.QLayout.SetMinimumSize)
+ formLayout.setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
self.setTabOrder(self._comboBoxColormap, self._comboBoxNormalization)
self.setTabOrder(self._comboBoxNormalization, self._gammaSpinBox)
@@ -1003,9 +1092,11 @@ class ColormapDialog(qt.QDialog):
self.setTabOrder(self._selectedAreaButton, self._buttonsModal)
self.setTabOrder(self._buttonsModal, self._buttonsNonModal)
- self.setFixedSize(self.sizeHint())
self._applyColormap()
+ def getHistogramWidget(self):
+ return self._histoWidget
+
def _invalidateColormap(self):
if self.isVisible():
self._applyColormap()
@@ -1041,10 +1132,14 @@ class ColormapDialog(qt.QDialog):
super(ColormapDialog, self).closeEvent(event)
def hideEvent(self, event):
+ if self._selectedAreaButton.isChecked():
+ self._selectedAreaButton.setChecked(False)
self.visibleChanged.emit(False)
super(ColormapDialog, self).hideEvent(event)
def close(self):
+ if self._selectedAreaButton.isChecked():
+ self._selectedAreaButton.setChecked(False)
self.accept()
qt.QDialog.close(self)
@@ -1104,9 +1199,9 @@ class ColormapDialog(qt.QDialog):
if dataRange is None or len(dataRange) != 3:
qt.QMessageBox.warning(
- None, "No Data",
- "Image data does not contain any real value")
- dataRange = 1., 1., 10.
+ None, "No Data", "Image data does not contain any real value"
+ )
+ dataRange = 1.0, 1.0, 10.0
return dataRange
@@ -1130,11 +1225,11 @@ class ColormapDialog(qt.QDialog):
return None, None
if data.ndim == 3: # RGB(A) images
- _logger.info('Converting current image from RGB(A) to grayscale\
- in order to compute the intensity distribution')
- data = (data[:,:, 0] * 0.299 +
- data[:,:, 1] * 0.587 +
- data[:,:, 2] * 0.114)
+ _logger.info(
+ "Converting current image from RGB(A) to grayscale\
+ in order to compute the intensity distribution"
+ )
+ data = data[:, :, 0] * 0.299 + data[:, :, 1] * 0.587 + data[:, :, 2] * 0.114
# bad hack: get 256 continuous bins in the case we have a B&W
normalizeData = True
@@ -1148,7 +1243,7 @@ class ColormapDialog(qt.QDialog):
if normalizeData:
if scale == Colormap.LOGARITHM:
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
data = numpy.log10(data)
if dataRange is not None:
@@ -1179,7 +1274,7 @@ class ColormapDialog(qt.QDialog):
bins = histogram.edges[0]
if normalizeData:
if scale == Colormap.LOGARITHM:
- bins = 10 ** bins
+ bins = 10**bins
return histogram.histo, bins
def _getItem(self):
@@ -1196,12 +1291,22 @@ class ColormapDialog(qt.QDialog):
the data range or the histogram of the data using :meth:`setDataRange`
and :meth:`setHistogram`
"""
- # While event from items are not supported, we can't ignore dup items
- # old = self._getItem()
- # if old is item:
- # return
- self._data = None
- self._itemHolder = None
+ old = self._getItem()
+ if old is item:
+ # While event from items are not supported, we can't ignore dup items
+ if item is not None:
+ array = item.getColormappedData(copy=False)
+ else:
+ array = None
+ colormapped = self._colormapped
+ if colormapped is not None:
+ oldArray = colormapped()
+ else:
+ oldArray = None
+ if oldArray is array:
+ return
+
+ self.__resetItem()
try:
if item is None:
self._item = None
@@ -1209,6 +1314,7 @@ class ColormapDialog(qt.QDialog):
if not isinstance(item, items.ColormapMixIn):
self._item = None
raise ValueError("Item %s is not supported" % item)
+ item.sigItemChanged.connect(self.__itemChanged)
self._item = weakref.ref(item, self._itemAboutToFinalize)
finally:
self._syncScaleToButtonsEnabled()
@@ -1216,6 +1322,20 @@ class ColormapDialog(qt.QDialog):
self._histogramData = None
self._invalidateData()
+ def __resetItem(self):
+ """Reset item and data used by the dialog"""
+ self._data = None
+ self._itemHolder = None
+ if self._item is not None:
+ item = self._item()
+ self._item = None
+ if item is not None:
+ item.sigItemChanged.disconnect(self.__itemChanged)
+
+ def __itemChanged(self, event):
+ if event == items.ItemChangedType.DATA:
+ self._invalidateData()
+
def _getData(self):
if self._data is None:
return None
@@ -1232,12 +1352,9 @@ class ColormapDialog(qt.QDialog):
if oldData is data:
return
- self._item = None
+ self.__resetItem()
self._syncScaleToButtonsEnabled()
- if data is None:
- self._data = None
- self._itemHolder = None
- else:
+ if data is not None:
self._data = weakref.ref(data, self._dataAboutToFinalize)
self._itemHolder = _DataRefHolder(self._data)
@@ -1252,7 +1369,12 @@ class ColormapDialog(qt.QDialog):
return data
item = self._getItem()
if item is not None:
- return item.getColormappedData(copy=False)
+ colormapped = item.getColormappedData(copy=False)
+ if colormapped is not None:
+ self._colormapped = weakref.ref(colormapped)
+ else:
+ self._colormapped = None
+ return colormapped
return None
def _colormapAboutToFinalize(self, weakrefColormap):
@@ -1270,14 +1392,6 @@ class ColormapDialog(qt.QDialog):
if self._item is weakref and qtinspect.isValid(self):
self.setItem(None)
- @deprecation.deprecated(reason="It is private data", since_version="0.13")
- def getHistogram(self):
- histo = self._getHistogram()
- if histo is None:
- return None
- counts, bin_edges = histo
- return numpy.array(counts, copy=True), numpy.array(bin_edges, copy=True)
-
def _getHistogram(self):
"""Returns the histogram defined by the dialog as metadata
to describe the data in order to speed up the dialog.
@@ -1361,7 +1475,7 @@ class ColormapDialog(qt.QDialog):
(xmin, xmax, ymin, ymax) Rectangular region in data space
"""
if bounds is None:
- return None # no-op
+ return # no-op
colormap = self.getColormap()
if colormap is None:
@@ -1369,13 +1483,15 @@ class ColormapDialog(qt.QDialog):
item = self._getItem()
if not isinstance(item, items.ColormapMixIn):
- return None # no-op
+ return # no-op
data = item.getColormappedData(copy=False)
-
xmin, xmax, ymin, ymax = bounds
if isinstance(item, items.ImageBase):
+ if data.ndim != 2:
+ return # no-op
+
ox, oy = item.getOrigin()
sx, sy = item.getScale()
@@ -1392,7 +1508,9 @@ class ColormapDialog(qt.QDialog):
subset = data[
numpy.logical_and(
numpy.logical_and(xmin <= x, x <= xmax),
- numpy.logical_and(ymin <= y, y <= ymax))]
+ numpy.logical_and(ymin <= y, y <= ymax),
+ )
+ ]
if subset.size == 0:
return # no-op
@@ -1418,7 +1536,6 @@ class ColormapDialog(qt.QDialog):
self._histoWidget.setFiniteRange((xmin, xmax))
with utils.blockSignals(self._autoButtons):
self._autoButtons.setAutoRange((autoMin, autoMax))
- self._autoscaleModeLabel.setEnabled(autoMin or autoMax)
def accept(self):
self.storeCurrentState()
@@ -1487,7 +1604,6 @@ class ColormapDialog(qt.QDialog):
self._minValue.setEnabled(False)
self._maxValue.setEnabled(False)
self._autoButtons.setEnabled(False)
- self._autoscaleModeLabel.setEnabled(False)
self._histoWidget.setVisible(False)
self._histoWidget.setFiniteRange((None, None))
else:
@@ -1497,19 +1613,21 @@ class ColormapDialog(qt.QDialog):
self._comboBoxColormap.setEnabled(colormap.isEditable())
with utils.blockSignals(self._comboBoxNormalization):
index = self._comboBoxNormalization.findData(
- colormap.getNormalization())
+ colormap.getNormalization()
+ )
if index < 0:
- _logger.error('Unsupported normalization: %s' %
- colormap.getNormalization())
+ _logger.error(
+ "Unsupported normalization: %s" % colormap.getNormalization()
+ )
else:
self._comboBoxNormalization.setCurrentIndex(index)
self._comboBoxNormalization.setEnabled(colormap.isEditable())
with utils.blockSignals(self._gammaSpinBox):
- self._gammaSpinBox.setValue(
- colormap.getGammaNormalizationParameter())
+ self._gammaSpinBox.setValue(colormap.getGammaNormalizationParameter())
self._gammaSpinBox.setEnabled(
- colormap.getNormalization() == 'gamma' and
- colormap.isEditable())
+ colormap.getNormalization() == Colormap.GAMMA
+ and colormap.isEditable()
+ )
with utils.blockSignals(self._autoScaleCombo):
self._autoScaleCombo.setCurrentMode(colormap.getAutoscaleMode())
self._autoScaleCombo.setEnabled(colormap.isEditable())
@@ -1530,7 +1648,6 @@ class ColormapDialog(qt.QDialog):
with utils.blockSignals(self._maxValue):
self._maxValue.setValue(vmax or dataRange[1], isAuto=vmax is None)
self._maxValue.setEnabled(colormap.isEditable())
- self._autoscaleModeLabel.setEnabled(vmin is None or vmax is None)
with utils.blockSignals(self._histoWidget):
self._histoWidget.setVisible(True)
@@ -1559,8 +1676,8 @@ class ColormapDialog(qt.QDialog):
dataRange = self._getFiniteColormapRange()
# Final colormap range
- vmin = (dataRange[0] if not autoRange[0] else None)
- vmax = (dataRange[1] if not autoRange[1] else None)
+ vmin = dataRange[0] if not autoRange[0] else None
+ vmax = dataRange[1] if not autoRange[1] else None
with self._colormapChange:
colormap = self.getColormap()
@@ -1580,7 +1697,7 @@ class ColormapDialog(qt.QDialog):
colormap = self.getColormap()
if colormap is not None:
normalization = self._comboBoxNormalization.itemData(index)
- self._gammaSpinBox.setEnabled(normalization == 'gamma')
+ self._gammaSpinBox.setEnabled(normalization == "gamma")
with self._colormapChange:
colormap.setNormalization(normalization)
@@ -1653,12 +1770,13 @@ class ColormapDialog(qt.QDialog):
self._maxValue.setValue(xmax)
self._setColormapRange(xmin, xmax)
- def _histogramRangeMoving(self, vmin, vmax):
+ def _histogramRangeMoving(self, vmin, vmax, gammaPos):
"""Callback executed when for colormap range displayed in
the histogram widget is moving.
:param vmin: Update of the minimum range, else None
:param vmax: Update of the maximum range, else None
+ :param gammaPos: Update of the gamma location, else None
"""
colormap = self.getColormap()
if vmin is not None:
@@ -1669,11 +1787,31 @@ class ColormapDialog(qt.QDialog):
with self._colormapChange:
colormap.setVMax(vmax)
self._maxValue.setValue(vmax)
+ if gammaPos is not None:
+ vmin, vmax = self._histoWidget.getFiniteRange()
+ if vmax < vmin:
+ gamma = 1
+ elif gammaPos >= vmax:
+ gamma = self._gammaSpinBox.maximum()
+ elif gammaPos <= vmin:
+ gamma = self._gammaSpinBox.minimum()
+ else:
+ gamma = numpy.clip(
+ numpy.log(0.5) / numpy.log((gammaPos - vmin) / (vmax - vmin)),
+ self._gammaSpinBox.minimum(),
+ self._gammaSpinBox.maximum(),
+ )
+ with self._colormapChange:
+ colormap.setGammaNormalizationParameter(gamma)
+ with utils.blockSignals(self._gammaSpinBox):
+ self._gammaSpinBox.setValue(gamma)
- def _histogramRangeMoved(self, vmin, vmax):
+ def _histogramRangeMoved(self, vmin, vmax, gammaPos):
"""Callback executed when for colormap range displayed in
the histogram widget has finished to move
"""
+ if vmin is None and vmax is None:
+ return
xmin = self._minValue.getValue()
xmax = self._maxValue.getValue()
if vmin is None:
@@ -1685,7 +1823,9 @@ class ColormapDialog(qt.QDialog):
def _syncScaleToButtonsEnabled(self):
"""Set the state of scale to buttons according to current item and colormap"""
colormap = self.getColormap()
- enabled = self._item is not None and colormap is not None and colormap.isEditable()
+ enabled = (
+ self._item is not None and colormap is not None and colormap.isEditable()
+ )
self._scaleToAreaGroup.setVisible(enabled)
self._visibleAreaButton.setEnabled(enabled)
if not enabled:
@@ -1713,7 +1853,6 @@ class ColormapDialog(qt.QDialog):
self._roiForColormapManager = None
if not checked: # Reset button status
- self._selectedAreaButton.setWaiting(False)
self._selectedAreaButton.setText("Selection")
return
@@ -1727,27 +1866,31 @@ class ColormapDialog(qt.QDialog):
self._selectedAreaButton.setChecked(False)
return # no-op
- self._selectedAreaButton.setWaiting(True)
self._selectedAreaButton.setText("Draw Area...")
self._roiForColormapManager = RegionOfInterestManager(parent=plotWidget)
cmap = self.getColormap()
self._roiForColormapManager.setColor(
- 'black' if cmap is None else cursorColorForColormap(cmap.getName()))
+ "black" if cmap is None else cursorColorForColormap(cmap.getName())
+ )
self._roiForColormapManager.sigInteractiveModeFinished.connect(
- self.__roiInteractiveModeFinished)
- self._roiForColormapManager.sigInteractiveRoiFinalized.connect(self.__roiFinalized)
+ self.__roiInteractiveModeFinished
+ )
+ self._roiForColormapManager.sigInteractiveRoiFinalized.connect(
+ self.__roiFinalized
+ )
self._roiForColormapManager.start(RectangleROI)
def __roiInteractiveModeFinished(self):
self._selectedAreaButton.setChecked(False)
def __roiFinalized(self, roi):
- self._selectedAreaButton.setChecked(False)
if roi is not None:
ox, oy = roi.getOrigin()
width, height = roi.getSize()
- self.setColormapRangeFromDataBounds((ox, ox+width, oy, oy+height))
+ self.setColormapRangeFromDataBounds((ox, ox + width, oy, oy + height))
+ # clear ROI
+ self._roiForColormapManager.removeRoi(roi)
def keyPressEvent(self, event):
"""Override key handling.
diff --git a/src/silx/gui/dialog/DataFileDialog.py b/src/silx/gui/dialog/DataFileDialog.py
index 0d0382d..4c6891e 100644
--- a/src/silx/gui/dialog/DataFileDialog.py
+++ b/src/silx/gui/dialog/DataFileDialog.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -37,8 +36,6 @@ from silx.gui.hdf5.Hdf5Formatter import Hdf5Formatter
import silx.io
from .AbstractDataFileDialog import AbstractDataFileDialog
-import fabio
-
_logger = logging.getLogger(__name__)
@@ -337,4 +334,4 @@ class DataFileDialog(AbstractDataFileDialog):
selection widget (basically the data from the browsing widget)
:rtype: bool
"""
- return u""
+ return ""
diff --git a/src/silx/gui/dialog/DatasetDialog.py b/src/silx/gui/dialog/DatasetDialog.py
index c5ee295..1bc2722 100644
--- a/src/silx/gui/dialog/DatasetDialog.py
+++ b/src/silx/gui/dialog/DatasetDialog.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
@@ -61,17 +60,22 @@ class DatasetDialog(_Hdf5ItemSelectionDialog):
print("Operation cancelled :(")
"""
+
def __init__(self, parent=None):
_Hdf5ItemSelectionDialog.__init__(self, parent)
# customization for groups
self.setWindowTitle("HDF5 dataset selection")
- self._header.setSections([self._model.NAME_COLUMN,
- self._model.NODE_COLUMN,
- self._model.LINK_COLUMN,
- self._model.TYPE_COLUMN,
- self._model.SHAPE_COLUMN])
+ self._header.setSections(
+ [
+ self._model.NAME_COLUMN,
+ self._model.NODE_COLUMN,
+ self._model.LINK_COLUMN,
+ self._model.TYPE_COLUMN,
+ self._model.SHAPE_COLUMN,
+ ]
+ )
self._selectDatasetStatusText = "Select a dataset or type a new dataset name"
def setMode(self, mode):
@@ -81,7 +85,9 @@ class DatasetDialog(_Hdf5ItemSelectionDialog):
"""
_Hdf5ItemSelectionDialog.setMode(self, mode)
if mode == DatasetDialog.SaveMode:
- self._selectDatasetStatusText = "Select a dataset or type a new dataset name"
+ self._selectDatasetStatusText = (
+ "Select a dataset or type a new dataset name"
+ )
elif mode == DatasetDialog.LoadMode:
self._selectDatasetStatusText = "Select a dataset"
@@ -111,11 +117,11 @@ class DatasetDialog(_Hdf5ItemSelectionDialog):
isDatasetSelected = True
if isDatasetSelected:
- self._selectedUrl = DataUrl(file_path=node.local_filename,
- data_path=data_path)
+ self._selectedUrl = DataUrl(
+ file_path=node.local_filename, data_path=data_path
+ )
self._okButton.setEnabled(True)
- self._labelSelection.setText(
- self._selectedUrl.path())
+ self._labelSelection.setText(self._selectedUrl.path())
else:
self._selectedUrl = None
self._okButton.setEnabled(False)
diff --git a/src/silx/gui/dialog/FileTypeComboBox.py b/src/silx/gui/dialog/FileTypeComboBox.py
index 92529bc..85ad3b1 100644
--- a/src/silx/gui/dialog/FileTypeComboBox.py
+++ b/src/silx/gui/dialog/FileTypeComboBox.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,12 +30,13 @@ __license__ = "MIT"
__date__ = "17/01/2019"
import fabio
+from fabio import fabioutils
+
import silx.io
from silx.gui import qt
class Codec(object):
-
def __init__(self, any_fabio=False, any_silx=False, fabio_codec=None, auto=False):
self.__any_fabio = any_fabio
self.__any_silx = any_silx
@@ -64,7 +64,7 @@ class FileTypeComboBox(qt.QComboBox):
CODEC_ROLE = qt.Qt.UserRole + 2
- INDENTATION = u"\u2022 "
+ INDENTATION = "\u2022 "
def __init__(self, parent=None):
qt.QComboBox.__init__(self, parent)
@@ -135,20 +135,13 @@ class FileTypeComboBox(qt.QComboBox):
def __insertFabioFormats(self):
formats = fabio.fabioformats.get_classes(reader=True)
- from fabio import fabioutils
- if hasattr(fabioutils, "COMPRESSED_EXTENSIONS"):
- compressedExtensions = fabioutils.COMPRESSED_EXTENSIONS
- else:
- # Support for fabio < 0.9
- compressedExtensions = set(["gz", "bz2"])
-
extensions = []
allExtensions = set([])
def extensionsIterator(reader):
for extension in reader.DEFAULT_EXTENSIONS:
yield "*.%s" % extension
- for compressedExtension in compressedExtensions:
+ for compressedExtension in fabioutils.COMPRESSED_EXTENSIONS:
for extension in reader.DEFAULT_EXTENSIONS:
yield "*.%s.%s" % (extension, compressedExtension)
@@ -164,7 +157,9 @@ class FileTypeComboBox(qt.QComboBox):
allExtensions.update(ext)
if ext == []:
ext = ["*"]
- extensions.append((reader.DESCRIPTION, displayext, ext, reader.codec_name()))
+ extensions.append(
+ (reader.DESCRIPTION, displayext, ext, reader.codec_name())
+ )
extensions = list(sorted(extensions))
allExtensions = list(sorted(list(allExtensions)))
@@ -177,7 +172,9 @@ class FileTypeComboBox(qt.QComboBox):
description, displayExt, allExt, _codecName = e
index = self.count()
if len(e[1]) < 10:
- self.addItem("%s%s (%s)" % (self.INDENTATION, description, " ".join(displayExt)))
+ self.addItem(
+ "%s%s (%s)" % (self.INDENTATION, description, " ".join(displayExt))
+ )
else:
self.addItem("%s%s" % (self.INDENTATION, description))
codec = Codec(fabio_codec=_codecName)
diff --git a/src/silx/gui/dialog/GroupDialog.py b/src/silx/gui/dialog/GroupDialog.py
index e129a51..ca669f2 100644
--- a/src/silx/gui/dialog/GroupDialog.py
+++ b/src/silx/gui/dialog/GroupDialog.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
@@ -55,8 +54,7 @@ class _Hdf5ItemSelectionDialog(qt.QDialog):
self._tree = Hdf5TreeView(self)
self._tree.setSelectionMode(qt.QAbstractItemView.SingleSelection)
self._tree.activated.connect(self._onActivation)
- self._tree.selectionModel().selectionChanged.connect(
- self._onSelectionChange)
+ self._tree.selectionModel().selectionChanged.connect(self._onSelectionChange)
self._model = self._tree.findHdf5TreeModel()
@@ -68,10 +66,9 @@ class _Hdf5ItemSelectionDialog(qt.QDialog):
self._labelNewItem.setText("Create new item in selected group (optional):")
self._lineEditNewItem = qt.QLineEdit(self._newItemWidget)
self._lineEditNewItem.setToolTip(
- "Specify the name of a new item "
- "to be created in the selected group.")
- self._lineEditNewItem.textChanged.connect(
- self._onNewItemNameChange)
+ "Specify the name of a new item " "to be created in the selected group."
+ )
+ self._lineEditNewItem.textChanged.connect(self._onNewItemNameChange)
newItemLayout.addWidget(self._labelNewItem)
newItemLayout.addWidget(self._lineEditNewItem)
@@ -152,11 +149,11 @@ class _Hdf5ItemSelectionDialog(qt.QDialog):
if not data_path.endswith("/"):
data_path += "/"
data_path += subgroupName.lstrip("/")
- self._selectedUrl = DataUrl(file_path=node.local_filename,
- data_path=data_path)
+ self._selectedUrl = DataUrl(
+ file_path=node.local_filename, data_path=data_path
+ )
self._okButton.setEnabled(True)
- self._labelSelection.setText(
- self._selectedUrl.path())
+ self._labelSelection.setText(self._selectedUrl.path())
def getSelectedDataUrl(self):
"""Return a :class:`DataUrl` with a file path and a data path.
@@ -190,15 +187,16 @@ class GroupDialog(_Hdf5ItemSelectionDialog):
print("Operation cancelled :(")
"""
+
def __init__(self, parent=None):
_Hdf5ItemSelectionDialog.__init__(self, parent)
# customization for groups
self.setWindowTitle("HDF5 group selection")
- self._header.setSections([self._model.NAME_COLUMN,
- self._model.NODE_COLUMN,
- self._model.LINK_COLUMN])
+ self._header.setSections(
+ [self._model.NAME_COLUMN, self._model.NODE_COLUMN, self._model.LINK_COLUMN]
+ )
def _onActivation(self, idx):
# double-click or enter press: filter for groups
@@ -219,11 +217,11 @@ class GroupDialog(_Hdf5ItemSelectionDialog):
if not data_path.endswith("/"):
data_path += "/"
data_path += subgroupName.lstrip("/")
- self._selectedUrl = DataUrl(file_path=node.local_filename,
- data_path=data_path)
+ self._selectedUrl = DataUrl(
+ file_path=node.local_filename, data_path=data_path
+ )
self._okButton.setEnabled(True)
- self._labelSelection.setText(
- self._selectedUrl.path())
+ self._labelSelection.setText(self._selectedUrl.path())
else:
self._selectedUrl = None
self._okButton.setEnabled(False)
diff --git a/src/silx/gui/dialog/ImageFileDialog.py b/src/silx/gui/dialog/ImageFileDialog.py
index 83c6d95..e7ce38f 100644
--- a/src/silx/gui/dialog/ImageFileDialog.py
+++ b/src/silx/gui/dialog/ImageFileDialog.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -199,7 +198,9 @@ class _ImagePreview(qt.QWidget):
axis = self.__plot.getXAxis()
axis.setLimitsConstraints(midWidth - widthContraint, midWidth + widthContraint)
axis = self.__plot.getYAxis()
- axis.setLimitsConstraints(midHeight - heightContraint, midHeight + heightContraint)
+ axis.setLimitsConstraints(
+ midHeight - heightContraint, midHeight + heightContraint
+ )
def __imageItem(self):
image = self.__plot.getImage("data")
@@ -341,14 +342,14 @@ class ImageFileDialog(AbstractDataFileDialog):
"""
destination = self.__formatShape(dataAfterSelection.shape)
source = self.__formatShape(dataBeforeSelection.shape)
- return u"%s \u2192 %s" % (source, destination)
+ return "%s \u2192 %s" % (source, destination)
def __formatShape(self, shape):
result = []
for s in shape:
if isinstance(s, slice):
- v = u"\u2026"
+ v = "\u2026"
else:
v = str(s)
result.append(v)
- return u" \u00D7 ".join(result)
+ return " \u00D7 ".join(result)
diff --git a/src/silx/gui/dialog/SafeFileIconProvider.py b/src/silx/gui/dialog/SafeFileIconProvider.py
index 1e06b64..7022876 100644
--- a/src/silx/gui/dialog/SafeFileIconProvider.py
+++ b/src/silx/gui/dialog/SafeFileIconProvider.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -92,6 +91,7 @@ class SafeFileIconProvider(qt.QFileIconProvider):
def __windowsDriveTypeId(self, info):
try:
import ctypes
+
path = info.filePath()
dtype = ctypes.cdll.kernel32.GetDriveTypeW(path)
except Exception:
diff --git a/src/silx/gui/dialog/SafeFileSystemModel.py b/src/silx/gui/dialog/SafeFileSystemModel.py
index 1ec7153..7cacc1e 100644
--- a/src/silx/gui/dialog/SafeFileSystemModel.py
+++ b/src/silx/gui/dialog/SafeFileSystemModel.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -42,7 +41,6 @@ _logger = logging.getLogger(__name__)
class _Item(object):
-
def __init__(self, fileInfo):
self.__fileInfo = fileInfo
self.__parent = None
@@ -102,7 +100,9 @@ class _Item(object):
elif self.isDrive():
path = self.__fileInfo.filePath()
else:
- path = os.path.join(self.parent().absoluteFilePath(), self.__fileInfo.fileName())
+ path = os.path.join(
+ self.parent().absoluteFilePath(), self.__fileInfo.fileName()
+ )
if path == "":
return "/"
self.__absolutePath = path
@@ -237,7 +237,9 @@ class _RawFileSystemModel(qt.QAbstractItemModel):
self.__header = "Name", "Size", "Type", "Last modification"
self.__currentPath = ""
self.__iconProvider = SafeFileIconProvider()
- self.__directoryLoadedSync.connect(self.__emitDirectoryLoaded, qt.Qt.QueuedConnection)
+ self.__directoryLoadedSync.connect(
+ self.__emitDirectoryLoaded, qt.Qt.QueuedConnection
+ )
def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
if orientation == qt.Qt.Horizontal:
@@ -497,7 +499,7 @@ class _RawFileSystemModel(qt.QAbstractItemModel):
return
def setReadOnly(self, enable):
- assert(enable is True)
+ assert enable is True
def isReadOnly(self):
return False
@@ -613,20 +615,20 @@ class SafeFileSystemModel(qt.QSortFilterProxyModel):
filterPermissions = (filters & qt.QDir.PermissionMask) != 0
if filterPermissions and (filters & (qt.QDir.Dirs | qt.QDir.Files)):
- if (filters & qt.QDir.Readable):
+ if filters & qt.QDir.Readable:
# Hide unreadable
if not fileInfo.isReadable():
return False
- if (filters & qt.QDir.Writable):
+ if filters & qt.QDir.Writable:
# Hide unwritable
if not fileInfo.isWritable():
return False
- if (filters & qt.QDir.Executable):
+ if filters & qt.QDir.Executable:
# Hide unexecutable
if not fileInfo.isExecutable():
return False
- if (filters & qt.QDir.NoSymLinks):
+ if filters & qt.QDir.NoSymLinks:
# Hide sym links
if fileInfo.isSymLink():
return False
@@ -712,7 +714,9 @@ class SafeFileSystemModel(qt.QSortFilterProxyModel):
def setNameFilters(self, filters):
self.__nameFilters = []
isCaseSensitive = self.__filters & qt.QDir.CaseSensitive
- caseSensitive = qt.Qt.CaseSensitive if isCaseSensitive else qt.Qt.CaseInsensitive
+ caseSensitive = (
+ qt.Qt.CaseSensitive if isCaseSensitive else qt.Qt.CaseInsensitive
+ )
for f in filters:
reg = qt.QRegExp(f, caseSensitive, qt.QRegExp.Wildcard)
self.__nameFilters.append(reg)
@@ -731,7 +735,7 @@ class SafeFileSystemModel(qt.QSortFilterProxyModel):
self.invalidate()
def setReadOnly(self, enable):
- assert(enable is True)
+ assert enable is True
def isReadOnly(self):
return False
diff --git a/src/silx/gui/dialog/__init__.py b/src/silx/gui/dialog/__init__.py
index 77c5949..c1dc89a 100644
--- a/src/silx/gui/dialog/__init__.py
+++ b/src/silx/gui/dialog/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/dialog/setup.py b/src/silx/gui/dialog/setup.py
deleted file mode 100644
index 48ab8d8..0000000
--- a/src/silx/gui/dialog/setup.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# 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.
-#
-# ############################################################################*/
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "23/10/2017"
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('dialog', parent_package, top_path)
- config.add_subpackage('test')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
diff --git a/src/silx/gui/dialog/test/__init__.py b/src/silx/gui/dialog/test/__init__.py
index 71128fb..b03339f 100644
--- a/src/silx/gui/dialog/test/__init__.py
+++ b/src/silx/gui/dialog/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/dialog/test/test_colormapdialog.py b/src/silx/gui/dialog/test/test_colormapdialog.py
index 16a5ab2..1afafc0 100644
--- a/src/silx/gui/dialog/test/test_colormapdialog.py
+++ b/src/silx/gui/dialog/test/test_colormapdialog.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2024 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,366 +29,369 @@ __date__ = "09/11/2018"
import pytest
-import weakref
from silx.gui import qt
from silx.gui.dialog import ColormapDialog
-from silx.gui.utils.testutils import TestCaseQt
from silx.gui.colors import Colormap, preferredColormaps
-from silx.utils.testutils import ParametricTestCase
from silx.gui.plot.items.image import ImageData
import numpy
-@pytest.fixture
-def colormap():
- colormap = Colormap(name='gray',
- vmin=10.0, vmax=20.0,
- normalization='linear')
- yield colormap
+def testGUIEdition(qWidgetFactory):
+ """Make sure the colormap is correctly edited and also that the
+ modification are correctly updated if an other colormapdialog is
+ editing the same colormap"""
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ dialog.setColormap(colormap)
+ dialog2 = qWidgetFactory(ColormapDialog.ColormapDialog)
+ dialog2.setColormap(colormap)
+
+ dialog._comboBoxColormap._setCurrentName("red")
+ dialog._comboBoxNormalization.setCurrentIndex(
+ dialog._comboBoxNormalization.findData(Colormap.LOGARITHM)
+ )
+ assert colormap.getName() == "red"
+ assert dialog.getColormap().getName() == "red"
+ assert colormap.getNormalization() == "log"
+ assert colormap.getVMin() == 10
+ assert colormap.getVMax() == 20
+ # checked second colormap dialog
+ assert dialog2._comboBoxColormap.getCurrentName() == "red"
+ assert dialog2._comboBoxNormalization.currentData() == Colormap.LOGARITHM
+ assert int(dialog2._minValue.getValue()) == 10
+ assert int(dialog2._maxValue.getValue()) == 20
+
+
+def testGUIModalOk(qapp, qapp_utils, qWidgetFactory):
+ """Make sure the colormap is modified if gone through accept"""
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ assert colormap.isAutoscale() is False
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ dialog.setModal(True)
+ qapp.processEvents()
+
+ dialog.setColormap(colormap)
+ assert colormap.getVMin() is not None
+ dialog._minValue.sigAutoScaleChanged.emit(True)
+ assert colormap.getVMin() is None
+ dialog._maxValue.sigAutoScaleChanged.emit(True)
+ qapp_utils.mouseClick(
+ widget=dialog._buttonsModal.button(qt.QDialogButtonBox.Ok),
+ button=qt.Qt.LeftButton,
+ )
+ assert colormap.getVMin() is None
+ assert colormap.getVMax() is None
+ assert colormap.isAutoscale() is True
+
+
+def testGUIModalCancel(qapp, qapp_utils, qWidgetFactory):
+ """Make sure the colormap is not modified if gone through reject"""
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ assert colormap.isAutoscale() is False
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ dialog.setModal(True)
+ qapp.processEvents()
+
+ dialog.setColormap(colormap)
+ assert colormap.getVMin() is not None
+ dialog._minValue.sigAutoScaleChanged.emit(True)
+ assert colormap.getVMin() is None
+ qapp_utils.mouseClick(
+ widget=dialog._buttonsModal.button(qt.QDialogButtonBox.Cancel),
+ button=qt.Qt.LeftButton,
+ )
+ assert colormap.getVMin() is not None
+
+
+def testGUIModalClose(qapp, qapp_utils, qWidgetFactory):
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ assert colormap.isAutoscale() is False
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ dialog.setModal(False)
+ qapp.processEvents()
+ dialog.setColormap(colormap)
+ assert colormap.getVMin() is not None
+ dialog._minValue.sigAutoScaleChanged.emit(True)
+ assert colormap.getVMin() is None
+ qapp_utils.mouseClick(
+ widget=dialog._buttonsNonModal.button(qt.QDialogButtonBox.Close),
+ button=qt.Qt.LeftButton,
+ )
+ assert colormap.getVMin() is None
+
+
+def testGUIModalReset(qapp, qapp_utils, qWidgetFactory):
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ assert colormap.isAutoscale() is False
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ dialog.setModal(False)
+ dialog.show()
+ qapp.processEvents()
+ dialog.setColormap(colormap)
+ assert colormap.getVMin() is not None
+ dialog._minValue.sigAutoScaleChanged.emit(True)
+ assert colormap.getVMin() is None
+ qapp_utils.mouseClick(
+ widget=dialog._buttonsNonModal.button(qt.QDialogButtonBox.Reset),
+ button=qt.Qt.LeftButton,
+ )
+ assert colormap.getVMin() is not None
+ dialog.close()
+
+
+def testGUIClose(qapp, qWidgetFactory):
+ """Make sure the colormap is modify if go through reject"""
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ assert colormap.isAutoscale() is False
+ qapp.processEvents()
+
+ dialog.setColormap(colormap)
+ assert colormap.getVMin() is not None
+ dialog._minValue.sigAutoScaleChanged.emit(True)
+ assert colormap.getVMin() is None
+ dialog.close()
+ qapp.processEvents()
+ assert colormap.getVMin() is None
+
+
+@pytest.mark.parametrize("norm", Colormap.NORMALIZATIONS)
+@pytest.mark.parametrize("autoscale", (True, False))
+def testSetColormapIsCorrect(norm, autoscale, qapp, qWidgetFactory):
+ """Make sure the interface fir the colormap when set a new colormap"""
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ colormap.setName("red")
+ if autoscale is True:
+ colormap.setVRange(None, None)
+ else:
+ colormap.setVRange(11, 101)
+ colormap.setNormalization(norm)
+ dialog.setColormap(colormap)
+ qapp.processEvents()
-@pytest.fixture
-def colormapDialog(qapp, qapp_utils):
- dialog = ColormapDialog.ColormapDialog()
- dialog.setAttribute(qt.Qt.WA_DeleteOnClose)
- yield weakref.proxy(dialog)
+ assert dialog._comboBoxNormalization.currentData() == norm
+ assert dialog._comboBoxColormap.getCurrentName() == "red"
+ assert dialog._minValue.isAutoChecked() == autoscale
+ assert dialog._maxValue.isAutoChecked() == autoscale
+ if autoscale is False:
+ assert dialog._minValue.getValue() == 11
+ assert dialog._maxValue.getValue() == 101
+ assert dialog._minValue.isEnabled()
+ assert dialog._maxValue.isEnabled()
+ else:
+ assert dialog._minValue._numVal.isReadOnly()
+ assert dialog._maxValue._numVal.isReadOnly()
+
+
+def testColormapDel(qapp, qWidgetFactory):
+ """Check behavior if the colormap has been deleted outside. For now
+ we make sure the colormap is still running and nothing more"""
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ colormap = Colormap(name="gray")
+ dialog.setColormap(colormap)
qapp.processEvents()
- from silx.gui.qt import inspect
- if inspect.isValid(dialog):
- dialog.close()
- qapp.processEvents()
+ colormap = None
+ assert dialog.getColormap() is None
+ dialog._comboBoxColormap._setCurrentName("blue")
-@pytest.fixture
-def colormap_class_attr(request, qapp_utils, colormap, colormapDialog):
- """Provides few fixtures to a class as class attribute
- Used as transition from TestCase to pytest
+def testColormapEditedOutside(qapp, qWidgetFactory):
+ """Make sure the GUI is still up to date if the colormap is modified
+ outside"""
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ dialog.setColormap(colormap)
+ qapp.processEvents()
+
+ colormap.setName("red")
+ assert dialog._comboBoxColormap.getCurrentName() == "red"
+ colormap.setNormalization(Colormap.LOGARITHM)
+ assert dialog._comboBoxNormalization.currentData() == Colormap.LOGARITHM
+ colormap.setVRange(11, 201)
+ assert dialog._minValue.getValue() == 11
+ assert dialog._maxValue.getValue() == 201
+ assert not (dialog._minValue._numVal.isReadOnly())
+ assert not (dialog._maxValue._numVal.isReadOnly())
+ assert not (dialog._minValue.isAutoChecked())
+ assert not (dialog._maxValue.isAutoChecked())
+ colormap.setVRange(None, None)
+ qapp.processEvents()
+
+ assert dialog._minValue._numVal.isReadOnly()
+ assert dialog._maxValue._numVal.isReadOnly()
+ assert dialog._minValue.isAutoChecked()
+ assert dialog._maxValue.isAutoChecked()
+
+
+def testSetColormapScenario(qWidgetFactory):
+ """Test of a simple scenario of a colormap dialog editing several
+ colormap"""
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ colormap1 = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
+ colormap2 = Colormap(name="red", vmin=10.0, vmax=20.0, normalization="log")
+ colormap3 = Colormap(name="blue", vmin=None, vmax=None, normalization="linear")
+
+ dialog.setColormap(colormap)
+ dialog.setColormap(colormap1)
+ del colormap1
+ dialog.setColormap(colormap2)
+ del colormap2
+ dialog.setColormap(colormap3)
+ del colormap3
+
+
+def testNotPreferredColormap(qapp, qWidgetFactory):
+ """Test that the colormapEditor is able to edit a colormap which is not
+ part of the 'prefered colormap'
"""
- request.cls.qapp_utils = qapp_utils
- request.cls.colormap = colormap
- request.cls.colormapDiag = colormapDialog
- yield
- request.cls.qapp_utils = None
- request.cls.colormap = None
- request.cls.colormapDiag = None
-
-
-@pytest.mark.usefixtures("colormap_class_attr")
-class TestColormapDialog(TestCaseQt, ParametricTestCase):
-
- def testGUIEdition(self):
- """Make sure the colormap is correctly edited and also that the
- modification are correctly updated if an other colormapdialog is
- editing the same colormap"""
- colormapDiag2 = ColormapDialog.ColormapDialog()
- colormapDiag2.setColormap(self.colormap)
- colormapDiag2.show()
- self.colormapDiag.setColormap(self.colormap)
- self.colormapDiag.show()
- self.qapp.processEvents()
-
- self.colormapDiag._comboBoxColormap._setCurrentName('red')
- self.colormapDiag._comboBoxNormalization.setCurrentIndex(
- self.colormapDiag._comboBoxNormalization.findData(Colormap.LOGARITHM))
- self.assertTrue(self.colormap.getName() == 'red')
- self.assertTrue(self.colormapDiag.getColormap().getName() == 'red')
- self.assertTrue(self.colormap.getNormalization() == 'log')
- self.assertTrue(self.colormap.getVMin() == 10)
- self.assertTrue(self.colormap.getVMax() == 20)
- # checked second colormap dialog
- self.assertTrue(colormapDiag2._comboBoxColormap.getCurrentName() == 'red')
- self.assertEqual(colormapDiag2._comboBoxNormalization.currentData(),
- Colormap.LOGARITHM)
- self.assertTrue(int(colormapDiag2._minValue.getValue()) == 10)
- self.assertTrue(int(colormapDiag2._maxValue.getValue()) == 20)
- colormapDiag2.close()
-
- def testGUIModalOk(self):
- """Make sure the colormap is modified if gone through accept"""
- assert self.colormap.isAutoscale() is False
- self.colormapDiag.setModal(True)
- self.colormapDiag.show()
- self.qapp.processEvents()
- self.colormapDiag.setColormap(self.colormap)
- self.assertTrue(self.colormap.getVMin() is not None)
- self.colormapDiag._minValue.sigAutoScaleChanged.emit(True)
- self.assertTrue(self.colormap.getVMin() is None)
- self.colormapDiag._maxValue.sigAutoScaleChanged.emit(True)
- self.mouseClick(
- widget=self.colormapDiag._buttonsModal.button(qt.QDialogButtonBox.Ok),
- button=qt.Qt.LeftButton
- )
- self.assertTrue(self.colormap.getVMin() is None)
- self.assertTrue(self.colormap.getVMax() is None)
- self.assertTrue(self.colormap.isAutoscale() is True)
-
- def testGUIModalCancel(self):
- """Make sure the colormap is not modified if gone through reject"""
- assert self.colormap.isAutoscale() is False
- self.colormapDiag.setModal(True)
- self.colormapDiag.show()
- self.qapp.processEvents()
- self.colormapDiag.setColormap(self.colormap)
- self.assertTrue(self.colormap.getVMin() is not None)
- self.colormapDiag._minValue.sigAutoScaleChanged.emit(True)
- self.assertTrue(self.colormap.getVMin() is None)
- self.mouseClick(
- widget=self.colormapDiag._buttonsModal.button(qt.QDialogButtonBox.Cancel),
- button=qt.Qt.LeftButton
- )
- self.assertTrue(self.colormap.getVMin() is not None)
-
- def testGUIModalClose(self):
- assert self.colormap.isAutoscale() is False
- self.colormapDiag.setModal(False)
- self.colormapDiag.show()
- self.qapp.processEvents()
- self.colormapDiag.setColormap(self.colormap)
- self.assertTrue(self.colormap.getVMin() is not None)
- self.colormapDiag._minValue.sigAutoScaleChanged.emit(True)
- self.assertTrue(self.colormap.getVMin() is None)
- self.mouseClick(
- widget=self.colormapDiag._buttonsNonModal.button(qt.QDialogButtonBox.Close),
- button=qt.Qt.LeftButton
- )
- self.assertTrue(self.colormap.getVMin() is None)
-
- def testGUIModalReset(self):
- assert self.colormap.isAutoscale() is False
- self.colormapDiag.setModal(False)
- self.colormapDiag.show()
- self.qapp.processEvents()
- self.colormapDiag.setColormap(self.colormap)
- self.assertTrue(self.colormap.getVMin() is not None)
- self.colormapDiag._minValue.sigAutoScaleChanged.emit(True)
- self.assertTrue(self.colormap.getVMin() is None)
- self.mouseClick(
- widget=self.colormapDiag._buttonsNonModal.button(qt.QDialogButtonBox.Reset),
- button=qt.Qt.LeftButton
- )
- self.assertTrue(self.colormap.getVMin() is not None)
- self.colormapDiag.close()
-
- def testGUIClose(self):
- """Make sure the colormap is modify if go through reject"""
- assert self.colormap.isAutoscale() is False
- self.colormapDiag.show()
- self.qapp.processEvents()
- self.colormapDiag.setColormap(self.colormap)
- self.assertTrue(self.colormap.getVMin() is not None)
- self.colormapDiag._minValue.sigAutoScaleChanged.emit(True)
- self.assertTrue(self.colormap.getVMin() is None)
- self.colormapDiag.close()
- self.qapp.processEvents()
- self.assertTrue(self.colormap.getVMin() is None)
-
- def testSetColormapIsCorrect(self):
- """Make sure the interface fir the colormap when set a new colormap"""
- self.colormap.setName('red')
- self.colormapDiag.show()
- self.qapp.processEvents()
- for norm in (Colormap.NORMALIZATIONS):
- for autoscale in (True, False):
- if autoscale is True:
- self.colormap.setVRange(None, None)
- else:
- self.colormap.setVRange(11, 101)
- self.colormap.setNormalization(norm)
- with self.subTest(colormap=self.colormap):
- self.colormapDiag.setColormap(self.colormap)
- self.assertEqual(
- self.colormapDiag._comboBoxNormalization.currentData(), norm)
- self.assertTrue(
- self.colormapDiag._comboBoxColormap.getCurrentName() == 'red')
- self.assertTrue(
- self.colormapDiag._minValue.isAutoChecked() == autoscale)
- self.assertTrue(
- self.colormapDiag._maxValue.isAutoChecked() == autoscale)
- if autoscale is False:
- self.assertTrue(self.colormapDiag._minValue.getValue() == 11)
- self.assertTrue(self.colormapDiag._maxValue.getValue() == 101)
- self.assertTrue(self.colormapDiag._minValue.isEnabled())
- self.assertTrue(self.colormapDiag._maxValue.isEnabled())
- else:
- self.assertFalse(self.colormapDiag._minValue._numVal.isEnabled())
- self.assertFalse(self.colormapDiag._maxValue._numVal.isEnabled())
-
- def testColormapDel(self):
- """Check behavior if the colormap has been deleted outside. For now
- we make sure the colormap is still running and nothing more"""
- colormap = Colormap(name='gray')
- self.colormapDiag.setColormap(colormap)
- self.colormapDiag.show()
- self.qapp.processEvents()
- colormap = None
- self.assertTrue(self.colormapDiag.getColormap() is None)
- self.colormapDiag._comboBoxColormap._setCurrentName('blue')
-
- def testColormapEditedOutside(self):
- """Make sure the GUI is still up to date if the colormap is modified
- outside"""
- self.colormapDiag.setColormap(self.colormap)
- self.colormapDiag.show()
- self.qapp.processEvents()
-
- self.colormap.setName('red')
- self.assertTrue(
- self.colormapDiag._comboBoxColormap.getCurrentName() == 'red')
- self.colormap.setNormalization(Colormap.LOGARITHM)
- self.assertEqual(self.colormapDiag._comboBoxNormalization.currentData(),
- Colormap.LOGARITHM)
- self.colormap.setVRange(11, 201)
- self.assertTrue(self.colormapDiag._minValue.getValue() == 11)
- self.assertTrue(self.colormapDiag._maxValue.getValue() == 201)
- self.assertTrue(self.colormapDiag._minValue._numVal.isEnabled())
- self.assertTrue(self.colormapDiag._maxValue._numVal.isEnabled())
- self.assertFalse(self.colormapDiag._minValue.isAutoChecked())
- self.assertFalse(self.colormapDiag._maxValue.isAutoChecked())
- self.colormap.setVRange(None, None)
- self.assertFalse(self.colormapDiag._minValue._numVal.isEnabled())
- self.assertFalse(self.colormapDiag._maxValue._numVal.isEnabled())
- self.assertTrue(self.colormapDiag._minValue.isAutoChecked())
- self.assertTrue(self.colormapDiag._maxValue.isAutoChecked())
-
- def testSetColormapScenario(self):
- """Test of a simple scenario of a colormap dialog editing several
- colormap"""
- colormap1 = Colormap(name='gray', vmin=10.0, vmax=20.0,
- normalization='linear')
- colormap2 = Colormap(name='red', vmin=10.0, vmax=20.0,
- normalization='log')
- colormap3 = Colormap(name='blue', vmin=None, vmax=None,
- normalization='linear')
- self.colormapDiag.setColormap(self.colormap)
- self.colormapDiag.setColormap(colormap1)
- del colormap1
- self.colormapDiag.setColormap(colormap2)
- del colormap2
- self.colormapDiag.setColormap(colormap3)
- del colormap3
-
- def testNotPreferredColormap(self):
- """Test that the colormapEditor is able to edit a colormap which is not
- part of the 'prefered colormap'
- """
- def getFirstNotPreferredColormap():
- cms = Colormap.getSupportedColormaps()
- preferred = preferredColormaps()
- for cm in cms:
- if cm not in preferred:
- return cm
- return None
-
- colormapName = getFirstNotPreferredColormap()
- assert colormapName is not None
- colormap = Colormap(name=colormapName)
- self.colormapDiag.setColormap(colormap)
- self.colormapDiag.show()
- self.qapp.processEvents()
- cb = self.colormapDiag._comboBoxColormap
- self.assertTrue(cb.getCurrentName() == colormapName)
- cb.setCurrentIndex(0)
- index = cb.findLutName(colormapName)
- assert index != 0 # if 0 then the rest of the test has no sense
- cb.setCurrentIndex(index)
- self.assertTrue(cb.getCurrentName() == colormapName)
-
- def testColormapEditableMode(self):
- """Test that the colormapDialog is correctly updated when changing the
- colormap editable status"""
- colormap = Colormap(normalization='linear', vmin=1.0, vmax=10.0)
- self.colormapDiag.show()
- self.qapp.processEvents()
- self.colormapDiag.setColormap(colormap)
- for editable in (True, False):
- with self.subTest(editable=editable):
- colormap.setEditable(editable)
- self.assertTrue(
- self.colormapDiag._comboBoxColormap.isEnabled() is editable)
- self.assertTrue(
- self.colormapDiag._minValue.isEnabled() is editable)
- self.assertTrue(
- self.colormapDiag._maxValue.isEnabled() is editable)
- self.assertTrue(
- self.colormapDiag._comboBoxNormalization.isEnabled() is editable)
-
- # Make sure the reset button is also set to enable when edition mode is
- # False
- self.colormapDiag.setModal(False)
- colormap.setEditable(True)
- self.colormapDiag._comboBoxNormalization.setCurrentIndex(
- self.colormapDiag._comboBoxNormalization.findData(Colormap.LOGARITHM))
- resetButton = self.colormapDiag._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
- self.assertTrue(resetButton.isEnabled())
- colormap.setEditable(False)
- self.assertFalse(resetButton.isEnabled())
-
- def testImageData(self):
- data = numpy.random.rand(5, 5)
- self.colormapDiag.setData(data)
-
- def testEmptyData(self):
- data = numpy.empty((10, 0))
- self.colormapDiag.setData(data)
-
- def testNoneData(self):
- data = numpy.random.rand(5, 5)
- self.colormapDiag.setData(data)
- self.colormapDiag.setData(None)
-
- def testImageItem(self):
- """Check that an ImageData plot item can be used"""
- dialog = self.colormapDiag
- colormap = Colormap(name='gray', vmin=None, vmax=None)
- data = numpy.arange(3**2).reshape(3, 3)
- item = ImageData()
- item.setData(data, copy=False)
-
- dialog.setColormap(colormap)
- dialog.show()
- self.qapp.processEvents()
- dialog.setItem(item)
- vrange = dialog._getFiniteColormapRange()
- self.assertEqual(vrange, (0, 8))
-
- def testItemDel(self):
- """Check that the plot items are not hard linked to the dialog"""
- dialog = self.colormapDiag
- colormap = Colormap(name='gray', vmin=None, vmax=None)
- data = numpy.arange(3**2).reshape(3, 3)
- item = ImageData()
- item.setData(data, copy=False)
-
- dialog.setColormap(colormap)
- dialog.show()
- self.qapp.processEvents()
- dialog.setItem(item)
- previousRange = dialog._getFiniteColormapRange()
- del item
- vrange = dialog._getFiniteColormapRange()
- self.assertNotEqual(vrange, previousRange)
-
- def testDataDel(self):
- """Check that the data are not hard linked to the dialog"""
- dialog = self.colormapDiag
- colormap = Colormap(name='gray', vmin=None, vmax=None)
- data = numpy.arange(5)
-
- dialog.setColormap(colormap)
- dialog.show()
- self.qapp.processEvents()
- dialog.setData(data)
- previousRange = dialog._getFiniteColormapRange()
- del data
- vrange = dialog._getFiniteColormapRange()
- self.assertNotEqual(vrange, previousRange)
-
- def testDeleteWhileExec(self):
- colormapDiag = self.colormapDiag
- self.colormapDiag = None
- qt.QTimer.singleShot(1000, colormapDiag.deleteLater)
- result = colormapDiag.exec()
- self.assertEqual(result, 0)
+
+ def getFirstNotPreferredColormap():
+ cms = Colormap.getSupportedColormaps()
+ preferred = preferredColormaps()
+ for cm in cms:
+ if cm not in preferred:
+ return cm
+ return None
+
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ colormapName = getFirstNotPreferredColormap()
+ assert colormapName is not None
+ colormap = Colormap(name=colormapName)
+ dialog.setColormap(colormap)
+ qapp.processEvents()
+
+ cb = dialog._comboBoxColormap
+ assert cb.getCurrentName() == colormapName
+ cb.setCurrentIndex(0)
+ index = cb.findLutName(colormapName)
+ assert index != 0 # if 0 then the rest of the test has no sense
+ cb.setCurrentIndex(index)
+ assert cb.getCurrentName() == colormapName
+
+
+def testColormapEditableMode(qWidgetFactory):
+ """Test that the colormapDialog is correctly updated when changing the
+ colormap editable status"""
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ colormap = Colormap(normalization="linear", vmin=1.0, vmax=10.0)
+
+ dialog.setColormap(colormap)
+
+ for editable in (True, False):
+ colormap.setEditable(editable)
+ assert dialog._comboBoxColormap.isEnabled() is editable
+ assert dialog._minValue.isEnabled() is editable
+ assert dialog._maxValue.isEnabled() is editable
+ assert dialog._comboBoxNormalization.isEnabled() is editable
+
+ # Make sure the reset button is also set to enable when edition mode is
+ # False
+ dialog.setModal(False)
+ colormap.setEditable(True)
+ dialog._comboBoxNormalization.setCurrentIndex(
+ dialog._comboBoxNormalization.findData(Colormap.LOGARITHM)
+ )
+ resetButton = dialog._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
+ assert resetButton.isEnabled()
+ colormap.setEditable(False)
+ assert not (resetButton.isEnabled())
+
+
+def testImageData(qWidgetFactory):
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ data = numpy.random.rand(5, 5)
+ dialog.setData(data)
+
+
+def testEmptyData(qWidgetFactory):
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ data = numpy.empty((10, 0))
+ dialog.setData(data)
+
+
+def testNoneData(qWidgetFactory):
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ data = numpy.random.rand(5, 5)
+ dialog.setData(data)
+ dialog.setData(None)
+
+
+def testImageItem(qapp, qWidgetFactory):
+ """Check that an ImageData plot item can be used"""
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ colormap = Colormap(name="gray", vmin=None, vmax=None)
+ data = numpy.arange(3**2).reshape(3, 3)
+ item = ImageData()
+ item.setData(data, copy=False)
+
+ dialog.setColormap(colormap)
+ qapp.processEvents()
+
+ dialog.setItem(item)
+ vrange = dialog._getFiniteColormapRange()
+ assert vrange == (0, 8)
+
+
+def testItemDel(qapp, qWidgetFactory):
+ """Check that the plot items are not hard linked to the dialog"""
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ colormap = Colormap(name="gray", vmin=None, vmax=None)
+ data = numpy.arange(3**2).reshape(3, 3)
+ item = ImageData()
+ item.setData(data, copy=False)
+
+ dialog.setColormap(colormap)
+ dialog.show()
+ qapp.processEvents()
+ dialog.setItem(item)
+ previousRange = dialog._getFiniteColormapRange()
+ del item
+ vrange = dialog._getFiniteColormapRange()
+ assert vrange != previousRange
+
+
+def testDataDel(qapp, qWidgetFactory):
+ """Check that the data are not hard linked to the dialog"""
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ colormap = Colormap(name="gray", vmin=None, vmax=None)
+ data = numpy.arange(5)
+
+ dialog.setColormap(colormap)
+ qapp.processEvents()
+
+ dialog.setData(data)
+ previousRange = dialog._getFiniteColormapRange()
+ del data
+ vrange = dialog._getFiniteColormapRange()
+ assert vrange != previousRange
+
+
+def testDeleteWhileExec(qWidgetFactory):
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+ qt.QTimer.singleShot(1000, dialog.deleteLater)
+ result = dialog.exec()
+ assert result == 0
+
+
+def testUpdateImageData(qapp, qWidgetFactory):
+ """Test that range/histogram takes into account item updates"""
+ dialog = qWidgetFactory(ColormapDialog.ColormapDialog)
+
+ item = ImageData()
+ item.setColormap(Colormap())
+ dialog.setItem(item)
+ dialog.setColormap(item.getColormap())
+ qapp.processEvents()
+
+ assert dialog._histoWidget.getFiniteRange() == (0, 1)
+
+ item.setData([(1, 2), (3, 4)])
+
+ assert dialog._histoWidget.getFiniteRange() == (1, 4)
diff --git a/src/silx/gui/dialog/test/test_datafiledialog.py b/src/silx/gui/dialog/test/test_datafiledialog.py
index 8411c67..887ff1c 100644
--- a/src/silx/gui/dialog/test/test_datafiledialog.py
+++ b/src/silx/gui/dialog/test/test_datafiledialog.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "08/03/2019"
-import unittest
import tempfile
import numpy
import shutil
@@ -66,7 +64,7 @@ def setUpModule():
f["complex_image"] = data * 1j
f["group/image"] = data
f["nxdata/foo"] = 10
- f["nxdata"].attrs["NX_class"] = u"NXdata"
+ f["nxdata"].attrs["NX_class"] = "NXdata"
f.close()
directory = os.path.join(_tmpDirectory, "data")
@@ -79,7 +77,7 @@ def setUpModule():
f["complex_image"] = data * 1j
f["group/image"] = data
f["nxdata/foo"] = 10
- f["nxdata"].attrs["NX_class"] = u"NXdata"
+ f["nxdata"].attrs["NX_class"] = "NXdata"
f.close()
filename = _tmpDirectory + "/badformat.h5"
@@ -89,12 +87,17 @@ def setUpModule():
def tearDownModule():
global _tmpDirectory
- shutil.rmtree(_tmpDirectory)
+ for _ in range(10):
+ try:
+ shutil.rmtree(_tmpDirectory)
+ except PermissionError: # Might fail on appveyor
+ testutils.TestCaseQt.qWait(500)
+ else:
+ break
_tmpDirectory = None
class _UtilsMixin(object):
-
def createDialog(self):
self._deleteDialog()
self._dialog = self._createDialog()
@@ -134,7 +137,6 @@ class _UtilsMixin(object):
class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
-
def tearDown(self):
self._deleteDialog()
testutils.TestCaseQt.tearDown(self)
@@ -214,7 +216,11 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
# select, then double click on the file
- index = browser.rootIndex().model().indexFromH5Object(dialog._AbstractDataFileDialog__h5["/group"])
+ index = (
+ browser.rootIndex()
+ .model()
+ .indexFromH5Object(dialog._AbstractDataFileDialog__h5["/group"])
+ )
browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
@@ -244,7 +250,11 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
# select, then double click on the file
- index = browser.rootIndex().model().indexFromH5Object(dialog._AbstractDataFileDialog__h5["/scalar"])
+ index = (
+ browser.rootIndex()
+ .model()
+ .indexFromH5Object(dialog._AbstractDataFileDialog__h5["/scalar"])
+ )
browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
@@ -271,12 +281,16 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(file_path=filename, data_path="/group/image").path()
dialog.selectUrl(path)
self.qWaitForPendingActions(dialog)
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group/image").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group/image"
+ ).path()
self.assertSamePath(url.text(), path)
# test
self.mouseClick(toParentButton, qt.Qt.LeftButton)
self.qWaitForPendingActions(dialog)
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/"
+ ).path()
self.assertSamePath(url.text(), path)
self.mouseClick(toParentButton, qt.Qt.LeftButton)
@@ -298,7 +312,9 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
filename = _tmpDirectory + "/data.h5"
# init state
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group/image").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group/image"
+ ).path()
dialog.selectUrl(path)
self.qWaitForPendingActions(dialog)
self.assertSamePath(url.text(), path)
@@ -306,7 +322,9 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# test
self.mouseClick(button, qt.Qt.LeftButton)
self.qWaitForPendingActions(dialog)
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/"
+ ).path()
self.assertSamePath(url.text(), path)
# self.assertFalse(button.isEnabled())
@@ -324,7 +342,9 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(file_path=filename, data_path="/group/image").path()
dialog.selectUrl(path)
self.qWaitForPendingActions(dialog)
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group/image").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group/image"
+ ).path()
self.assertSamePath(url.text(), path)
self.assertTrue(button.isEnabled())
# test
@@ -343,8 +363,12 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForWindowExposed(dialog)
url = testutils.findChildren(dialog, qt.QLineEdit, name="url")[0]
- forwardAction = testutils.findChildren(dialog, qt.QAction, name="forwardAction")[0]
- backwardAction = testutils.findChildren(dialog, qt.QAction, name="backwardAction")[0]
+ forwardAction = testutils.findChildren(
+ dialog, qt.QAction, name="forwardAction"
+ )[0]
+ backwardAction = testutils.findChildren(
+ dialog, qt.QAction, name="backwardAction"
+ )[0]
filename = _tmpDirectory + "/data.h5"
dialog.setDirectory(_tmpDirectory)
@@ -353,10 +377,14 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# Then we feed the history using selectPath
dialog.selectUrl(filename)
self.qWaitForPendingActions(dialog)
- path2 = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/").path()
+ path2 = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/"
+ ).path()
dialog.selectUrl(path2)
self.qWaitForPendingActions(dialog)
- path3 = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group").path()
+ path3 = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group"
+ ).path()
dialog.selectUrl(path3)
self.qWaitForPendingActions(dialog)
self.assertFalse(forwardAction.isEnabled())
@@ -383,7 +411,11 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# init state
filename = _tmpDirectory + "/singleimage.edf"
- url = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/scan_0/instrument/detector_0/data")
+ url = silx.io.url.DataUrl(
+ scheme="silx",
+ file_path=filename,
+ data_path="/scan_0/instrument/detector_0/data",
+ )
dialog.selectUrl(url.path())
self.assertEqual(dialog._selectedData().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
@@ -396,7 +428,9 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# init state
filename = _tmpDirectory + "/data.h5"
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/image").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/image"
+ ).path()
dialog.selectUrl(path)
# test
self.assertEqual(dialog._selectedData().shape, (100, 100))
@@ -410,7 +444,9 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# init state
filename = _tmpDirectory + "/data.h5"
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/scalar").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/scalar"
+ ).path()
dialog.selectUrl(path)
# test
self.assertEqual(dialog._selectedData()[()], 10)
@@ -459,7 +495,9 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filename = _tmpDirectory + "/data.h5"
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/"
+ ).path()
index = browser.rootIndex().model().index(filename)
# click
browser.selectIndex(index)
@@ -491,7 +529,7 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
for i in range(model.rowCount(rootIndex)):
index = model.index(i, 0, rootIndex)
flags = model.flags(index)
- isEnabled = (int(flags) & qt.Qt.ItemIsEnabled) != 0
+ isEnabled = flags & qt.Qt.ItemIsEnabled == qt.Qt.ItemIsEnabled
if isEnabled:
selectable += 1
return selectable
@@ -503,11 +541,12 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForWindowExposed(dialog)
dialog.selectUrl(_tmpDirectory)
self.qWaitForPendingActions(dialog)
- self.assertEqual(self._countSelectableItems(browser.model(), browser.rootIndex()), 4)
+ self.assertEqual(
+ self._countSelectableItems(browser.model(), browser.rootIndex()), 4
+ )
class TestDataFileDialog_FilterDataset(testutils.TestCaseQt, _UtilsMixin):
-
def tearDown(self):
self._deleteDialog()
testutils.TestCaseQt.tearDown(self)
@@ -534,7 +573,11 @@ class TestDataFileDialog_FilterDataset(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
# select, then double click on the file
- index = browser.rootIndex().model().indexFromH5Object(dialog._AbstractDataFileDialog__h5["/group"])
+ index = (
+ browser.rootIndex()
+ .model()
+ .indexFromH5Object(dialog._AbstractDataFileDialog__h5["/group"])
+ )
browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
@@ -559,7 +602,11 @@ class TestDataFileDialog_FilterDataset(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
# select, then double click on the file
- index = browser.rootIndex().model().indexFromH5Object(dialog._AbstractDataFileDialog__h5["/scalar"])
+ index = (
+ browser.rootIndex()
+ .model()
+ .indexFromH5Object(dialog._AbstractDataFileDialog__h5["/scalar"])
+ )
browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
@@ -577,7 +624,6 @@ class TestDataFileDialog_FilterDataset(testutils.TestCaseQt, _UtilsMixin):
class TestDataFileDialog_FilterGroup(testutils.TestCaseQt, _UtilsMixin):
-
def tearDown(self):
self._deleteDialog()
testutils.TestCaseQt.tearDown(self)
@@ -604,7 +650,11 @@ class TestDataFileDialog_FilterGroup(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
# select, then double click on the file
- index = browser.rootIndex().model().indexFromH5Object(dialog._AbstractDataFileDialog__h5["/group"])
+ index = (
+ browser.rootIndex()
+ .model()
+ .indexFromH5Object(dialog._AbstractDataFileDialog__h5["/group"])
+ )
browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
@@ -636,7 +686,11 @@ class TestDataFileDialog_FilterGroup(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
# select, then double click on the file
- index = browser.rootIndex().model().indexFromH5Object(dialog._AbstractDataFileDialog__h5["/scalar"])
+ index = (
+ browser.rootIndex()
+ .model()
+ .indexFromH5Object(dialog._AbstractDataFileDialog__h5["/scalar"])
+ )
browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
@@ -646,7 +700,6 @@ class TestDataFileDialog_FilterGroup(testutils.TestCaseQt, _UtilsMixin):
class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
-
def tearDown(self):
self._deleteDialog()
testutils.TestCaseQt.tearDown(self)
@@ -654,7 +707,7 @@ class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
def _createDialog(self):
def customFilter(obj):
if "NX_class" in obj.attrs:
- return obj.attrs["NX_class"] == u"NXdata"
+ return obj.attrs["NX_class"] == "NXdata"
return False
dialog = DataFileDialog()
@@ -679,7 +732,11 @@ class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
# select, then double click on the file
- index = browser.rootIndex().model().indexFromH5Object(dialog._AbstractDataFileDialog__h5["/group"])
+ index = (
+ browser.rootIndex()
+ .model()
+ .indexFromH5Object(dialog._AbstractDataFileDialog__h5["/group"])
+ )
browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
@@ -706,7 +763,11 @@ class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
# select, then double click on the file
- index = browser.rootIndex().model().indexFromH5Object(dialog._AbstractDataFileDialog__h5["/nxdata"])
+ index = (
+ browser.rootIndex()
+ .model()
+ .indexFromH5Object(dialog._AbstractDataFileDialog__h5["/nxdata"])
+ )
browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
@@ -721,7 +782,6 @@ class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
-
def tearDown(self):
self._deleteDialog()
testutils.TestCaseQt.tearDown(self)
@@ -774,46 +834,50 @@ class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
print()
print("\\\n".join(strings))
- STATE_VERSION1_QT4 = b''\
- b'\x00\x00\x00Z\x00s\x00i\x00l\x00x\x00.\x00g\x00u\x00i\x00.\x00'\
- b'd\x00i\x00a\x00l\x00o\x00g\x00.\x00D\x00a\x00t\x00a\x00F\x00i'\
- b'\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00.\x00D\x00a\x00t\x00'\
- b'a\x00F\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00\x00\x00'\
- b'\x01\x00\x00\x00\x0C\x00\x00\x00\x00"\x00\x00\x00\xFF\x00\x00'\
- b'\x00\x00\x00\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'\
- b'\xFF\xFF\x01\x00\x00\x00\x06\x01\x00\x00\x00\x01\x00\x00\x00\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x00\x00\x00'\
- b'}\x00\x00\x00\x0E\x00B\x00r\x00o\x00w\x00s\x00e\x00r\x00\x00\x00'\
- b'\x01\x00\x00\x00\x0C\x00\x00\x00\x00Z\x00\x00\x00\xFF\x00\x00'\
- b'\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
- b'\x00\x01\x90\x00\x00\x00\x04\x01\x01\x00\x00\x00\x00\x00\x00\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00d\xFF\xFF\xFF\xFF\x00\x00\x00\x81'\
- b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x01\x90\x00\x00\x00\x04'\
- b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00'\
- b'\x01\xFF\xFF\xFF\xFF'
+ STATE_VERSION1_QT4 = (
+ b""
+ b"\x00\x00\x00Z\x00s\x00i\x00l\x00x\x00.\x00g\x00u\x00i\x00.\x00"
+ b"d\x00i\x00a\x00l\x00o\x00g\x00.\x00D\x00a\x00t\x00a\x00F\x00i"
+ b"\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00.\x00D\x00a\x00t\x00"
+ b"a\x00F\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00\x00\x00"
+ b'\x01\x00\x00\x00\x0C\x00\x00\x00\x00"\x00\x00\x00\xFF\x00\x00'
+ b"\x00\x00\x00\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
+ b"\xFF\xFF\x01\x00\x00\x00\x06\x01\x00\x00\x00\x01\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x00\x00\x00"
+ b"}\x00\x00\x00\x0E\x00B\x00r\x00o\x00w\x00s\x00e\x00r\x00\x00\x00"
+ b"\x01\x00\x00\x00\x0C\x00\x00\x00\x00Z\x00\x00\x00\xFF\x00\x00"
+ b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x01\x90\x00\x00\x00\x04\x01\x01\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00d\xFF\xFF\xFF\xFF\x00\x00\x00\x81"
+ b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x01\x90\x00\x00\x00\x04"
+ b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00"
+ b"\x01\xFF\xFF\xFF\xFF"
+ )
"""Serialized state on Qt4. Generated using :meth:`printState`"""
- STATE_VERSION1_QT5 = b''\
- b'\x00\x00\x00Z\x00s\x00i\x00l\x00x\x00.\x00g\x00u\x00i\x00.\x00'\
- b'd\x00i\x00a\x00l\x00o\x00g\x00.\x00D\x00a\x00t\x00a\x00F\x00i'\
- b'\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00.\x00D\x00a\x00t\x00'\
- b'a\x00F\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00\x00\x00'\
- b'\x01\x00\x00\x00\x0C\x00\x00\x00\x00#\x00\x00\x00\xFF\x00\x00'\
- b'\x00\x01\x00\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'\
- b'\xFF\xFF\x01\xFF\xFF\xFF\xFF\x01\x00\x00\x00\x01\x00\x00\x00\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x00\x00'\
- b'\x00\xAA\x00\x00\x00\x0E\x00B\x00r\x00o\x00w\x00s\x00e\x00r\x00'\
- b'\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00\x87\x00\x00\x00\xFF'\
- b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00'\
- b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
- b'\x00\x00\x00\x01\x90\x00\x00\x00\x04\x01\x01\x00\x00\x00\x00\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00d\xFF\xFF\xFF\xFF\x00\x00'\
- b'\x00\x81\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00d\x00\x00'\
- b'\x00\x01\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x01\x00\x00'\
- b'\x00\x00\x00\x00\x00d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00'\
- b'\x00d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03\xE8\x00\xFF'\
- b'\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01'
+ STATE_VERSION1_QT5 = (
+ b""
+ b"\x00\x00\x00Z\x00s\x00i\x00l\x00x\x00.\x00g\x00u\x00i\x00.\x00"
+ b"d\x00i\x00a\x00l\x00o\x00g\x00.\x00D\x00a\x00t\x00a\x00F\x00i"
+ b"\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00.\x00D\x00a\x00t\x00"
+ b"a\x00F\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00\x00\x00"
+ b"\x01\x00\x00\x00\x0C\x00\x00\x00\x00#\x00\x00\x00\xFF\x00\x00"
+ b"\x00\x01\x00\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
+ b"\xFF\xFF\x01\xFF\xFF\xFF\xFF\x01\x00\x00\x00\x01\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x00\x00"
+ b"\x00\xAA\x00\x00\x00\x0E\x00B\x00r\x00o\x00w\x00s\x00e\x00r\x00"
+ b"\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00\x87\x00\x00\x00\xFF"
+ b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x01\x90\x00\x00\x00\x04\x01\x01\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00d\xFF\xFF\xFF\xFF\x00\x00"
+ b"\x00\x81\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00d\x00\x00"
+ b"\x00\x01\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x01\x00\x00"
+ b"\x00\x00\x00\x00\x00d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
+ b"\x00d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03\xE8\x00\xFF"
+ b"\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x01"
+ )
"""Serialized state on Qt5. Generated using :meth:`printState`"""
def testAvoidRestoreRegression_Version1(self):
@@ -898,7 +962,9 @@ class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filename = _tmpDirectory + "/data.h5"
- url = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group/foobar")
+ url = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group/foobar"
+ )
dialog.selectUrl(url.path())
self.qWaitForPendingActions(dialog)
self.assertIsNotNone(dialog._selectedData())
diff --git a/src/silx/gui/dialog/test/test_imagefiledialog.py b/src/silx/gui/dialog/test/test_imagefiledialog.py
index 9e204b9..9d2c414 100644
--- a/src/silx/gui/dialog/test/test_imagefiledialog.py
+++ b/src/silx/gui/dialog/test/test_imagefiledialog.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "08/03/2019"
-import unittest
import tempfile
import numpy
import shutil
@@ -96,12 +94,17 @@ def setUpModule():
def tearDownModule():
global _tmpDirectory
- shutil.rmtree(_tmpDirectory)
+ for _ in range(10):
+ try:
+ shutil.rmtree(_tmpDirectory)
+ except PermissionError: # Might fail on appveyor
+ testutils.TestCaseQt.qWait(500)
+ else:
+ break
_tmpDirectory = None
class _UtilsMixin(object):
-
def createDialog(self):
self._deleteDialog()
self._dialog = self._createDialog()
@@ -141,7 +144,6 @@ class _UtilsMixin(object):
class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
-
def tearDown(self):
self._deleteDialog()
testutils.TestCaseQt.tearDown(self)
@@ -196,6 +198,9 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Accepted)
def testClickOnShortcut(self):
+ if qt.BINDING == "PySide6":
+ self.skipTest("Avoid segmentation fault with PySide6")
+
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -259,12 +264,16 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(file_path=filename, data_path="/group/image").path()
dialog.selectUrl(path)
self.qWaitForPendingActions(dialog)
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group/image").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group/image"
+ ).path()
self.assertSamePath(url.text(), path)
# test
self.mouseClick(toParentButton, qt.Qt.LeftButton)
self.qWaitForPendingActions(dialog)
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/"
+ ).path()
self.assertSamePath(url.text(), path)
self.mouseClick(toParentButton, qt.Qt.LeftButton)
@@ -286,7 +295,9 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
filename = _tmpDirectory + "/data.h5"
# init state
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group/image").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group/image"
+ ).path()
dialog.selectUrl(path)
self.qWaitForPendingActions(dialog)
self.assertSamePath(url.text(), path)
@@ -294,7 +305,9 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# test
self.mouseClick(button, qt.Qt.LeftButton)
self.qWaitForPendingActions(dialog)
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/"
+ ).path()
self.assertSamePath(url.text(), path)
# self.assertFalse(button.isEnabled())
@@ -312,7 +325,9 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(file_path=filename, data_path="/group/image").path()
dialog.selectUrl(path)
self.qWaitForPendingActions(dialog)
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group/image").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group/image"
+ ).path()
self.assertSamePath(url.text(), path)
self.assertTrue(button.isEnabled())
# test
@@ -331,8 +346,12 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForWindowExposed(dialog)
url = testutils.findChildren(dialog, qt.QLineEdit, name="url")[0]
- forwardAction = testutils.findChildren(dialog, qt.QAction, name="forwardAction")[0]
- backwardAction = testutils.findChildren(dialog, qt.QAction, name="backwardAction")[0]
+ forwardAction = testutils.findChildren(
+ dialog, qt.QAction, name="forwardAction"
+ )[0]
+ backwardAction = testutils.findChildren(
+ dialog, qt.QAction, name="backwardAction"
+ )[0]
filename = _tmpDirectory + "/data.h5"
dialog.setDirectory(_tmpDirectory)
@@ -341,10 +360,14 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# Then we feed the history using selectPath
dialog.selectUrl(filename)
self.qWaitForPendingActions(dialog)
- path2 = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/").path()
+ path2 = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/"
+ ).path()
dialog.selectUrl(path2)
self.qWaitForPendingActions(dialog)
- path3 = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group").path()
+ path3 = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group"
+ ).path()
dialog.selectUrl(path3)
self.qWaitForPendingActions(dialog)
self.assertFalse(forwardAction.isEnabled())
@@ -407,7 +430,9 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# init state
filename = _tmpDirectory + "/multiframe.edf"
- path = silx.io.url.DataUrl(scheme="fabio", file_path=filename, data_slice=(1,)).path()
+ path = silx.io.url.DataUrl(
+ scheme="fabio", file_path=filename, data_slice=(1,)
+ ).path()
dialog.selectUrl(path)
# test
image = dialog.selectedImage()
@@ -437,7 +462,9 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# init state
filename = _tmpDirectory + "/data.h5"
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/image").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/image"
+ ).path()
dialog.selectUrl(path)
# test
self.assertEqual(dialog.selectedImage().shape, (100, 100))
@@ -454,7 +481,9 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filename = _tmpDirectory + "/data.h5"
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/").path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/"
+ ).path()
index = browser.rootIndex().model().index(filename)
# click
browser.selectIndex(index)
@@ -471,7 +500,9 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# init state
filename = _tmpDirectory + "/data.h5"
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/cube", data_slice=(1, )).path()
+ path = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/cube", data_slice=(1,)
+ ).path()
dialog.selectUrl(path)
# test
self.assertEqual(dialog.selectedImage().shape, (100, 100))
@@ -486,7 +517,12 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# init state
filename = _tmpDirectory + "/data.h5"
- path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/single_frame", data_slice=(0, )).path()
+ path = silx.io.url.DataUrl(
+ scheme="silx",
+ file_path=filename,
+ data_path="/single_frame",
+ data_slice=(0,),
+ ).path()
dialog.selectUrl(path)
# test
self.assertEqual(dialog.selectedImage().shape, (100, 100))
@@ -516,7 +552,7 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
for i in range(model.rowCount(rootIndex)):
index = model.index(i, 0, rootIndex)
flags = model.flags(index)
- isEnabled = (int(flags) & qt.Qt.ItemIsEnabled) != 0
+ isEnabled = flags & qt.Qt.ItemIsEnabled == qt.Qt.ItemIsEnabled
if isEnabled:
selectable += 1
return selectable
@@ -529,25 +565,30 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForWindowExposed(dialog)
dialog.selectUrl(_tmpDirectory)
self.qWaitForPendingActions(dialog)
- self.assertEqual(self._countSelectableItems(browser.model(), browser.rootIndex()), 6)
+ self.assertEqual(
+ self._countSelectableItems(browser.model(), browser.rootIndex()), 6
+ )
codecName = fabio.edfimage.EdfImage.codec_name()
index = filters.indexFromCodec(codecName)
filters.setCurrentIndex(index)
filters.activated[int].emit(index)
self.qWait(50)
- self.assertEqual(self._countSelectableItems(browser.model(), browser.rootIndex()), 4)
+ self.assertEqual(
+ self._countSelectableItems(browser.model(), browser.rootIndex()), 4
+ )
codecName = fabio.fit2dmaskimage.Fit2dMaskImage.codec_name()
index = filters.indexFromCodec(codecName)
filters.setCurrentIndex(index)
filters.activated[int].emit(index)
self.qWait(50)
- self.assertEqual(self._countSelectableItems(browser.model(), browser.rootIndex()), 2)
+ self.assertEqual(
+ self._countSelectableItems(browser.model(), browser.rootIndex()), 2
+ )
class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
-
def tearDown(self):
self._deleteDialog()
testutils.TestCaseQt.tearDown(self)
@@ -601,51 +642,55 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
print()
print("\\\n".join(strings))
- STATE_VERSION1_QT4 = b''\
- b'\x00\x00\x00^\x00s\x00i\x00l\x00x\x00.\x00g\x00u\x00i\x00.\x00'\
- b'd\x00i\x00a\x00l\x00o\x00g\x00.\x00I\x00m\x00a\x00g\x00e\x00F'\
- b'\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00.\x00I\x00m\x00'\
- b'a\x00g\x00e\x00F\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g'\
- b'\x00\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00"\x00\x00\x00'\
- b'\xFF\x00\x00\x00\x00\x00\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF'\
- b'\xFF\xFF\xFF\xFF\xFF\x01\x00\x00\x00\x06\x01\x00\x00\x00\x01\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00'\
- b'\x00\x00\x00}\x00\x00\x00\x0E\x00B\x00r\x00o\x00w\x00s\x00e\x00'\
- b'r\x00\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00Z\x00\x00\x00'\
- b'\xFF\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00'\
- b'\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
- b'\x00\x00\x00\x00\x01\x90\x00\x00\x00\x04\x01\x01\x00\x00\x00\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d\xFF\xFF\xFF\xFF\x00'\
- b'\x00\x00\x81\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x01\x90\x00'\
- b'\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00'\
- b'\x00\x00\x0C\x00\x00\x00\x000\x00\x00\x00\x10\x00C\x00o\x00l\x00'\
- b'o\x00r\x00m\x00a\x00p\x00\x00\x00\x01\x00\x00\x00\x08\x00g\x00'\
- b'r\x00a\x00y\x01\x01\x00\x00\x00\x06\x00l\x00o\x00g'
+ STATE_VERSION1_QT4 = (
+ b""
+ b"\x00\x00\x00^\x00s\x00i\x00l\x00x\x00.\x00g\x00u\x00i\x00.\x00"
+ b"d\x00i\x00a\x00l\x00o\x00g\x00.\x00I\x00m\x00a\x00g\x00e\x00F"
+ b"\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00.\x00I\x00m\x00"
+ b"a\x00g\x00e\x00F\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g"
+ b'\x00\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00"\x00\x00\x00'
+ b"\xFF\x00\x00\x00\x00\x00\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
+ b"\xFF\xFF\xFF\xFF\xFF\x01\x00\x00\x00\x06\x01\x00\x00\x00\x01\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00"
+ b"\x00\x00\x00}\x00\x00\x00\x0E\x00B\x00r\x00o\x00w\x00s\x00e\x00"
+ b"r\x00\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00Z\x00\x00\x00"
+ b"\xFF\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x01\x90\x00\x00\x00\x04\x01\x01\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d\xFF\xFF\xFF\xFF\x00"
+ b"\x00\x00\x81\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x01\x90\x00"
+ b"\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00"
+ b"\x00\x00\x0C\x00\x00\x00\x000\x00\x00\x00\x10\x00C\x00o\x00l\x00"
+ b"o\x00r\x00m\x00a\x00p\x00\x00\x00\x01\x00\x00\x00\x08\x00g\x00"
+ b"r\x00a\x00y\x01\x01\x00\x00\x00\x06\x00l\x00o\x00g"
+ )
"""Serialized state on Qt4. Generated using :meth:`printState`"""
- STATE_VERSION1_QT5 = b''\
- b'\x00\x00\x00^\x00s\x00i\x00l\x00x\x00.\x00g\x00u\x00i\x00.\x00'\
- b'd\x00i\x00a\x00l\x00o\x00g\x00.\x00I\x00m\x00a\x00g\x00e\x00F'\
- b'\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00.\x00I\x00m\x00'\
- b'a\x00g\x00e\x00F\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g'\
- b'\x00\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00#\x00\x00\x00'\
- b'\xFF\x00\x00\x00\x01\x00\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF'\
- b'\xFF\xFF\xFF\xFF\xFF\x01\xFF\xFF\xFF\xFF\x01\x00\x00\x00\x01\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C'\
- b'\x00\x00\x00\x00\xAA\x00\x00\x00\x0E\x00B\x00r\x00o\x00w\x00s'\
- b'\x00e\x00r\x00\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00\x87'\
- b'\x00\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00'\
- b'\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x01\x90\x00\x00\x00\x04\x01\x01\x00'\
- b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d\xFF\xFF'\
- b'\xFF\xFF\x00\x00\x00\x81\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00'\
- b'\x00d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00'\
- b'\x01\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x01\x00\x00\x00'\
- b'\x00\x00\x00\x00d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03'\
- b'\xE8\x00\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00'\
- b'\x00\x0C\x00\x00\x00\x000\x00\x00\x00\x10\x00C\x00o\x00l\x00o'\
- b'\x00r\x00m\x00a\x00p\x00\x00\x00\x01\x00\x00\x00\x08\x00g\x00'\
- b'r\x00a\x00y\x01\x01\x00\x00\x00\x06\x00l\x00o\x00g'
+ STATE_VERSION1_QT5 = (
+ b""
+ b"\x00\x00\x00^\x00s\x00i\x00l\x00x\x00.\x00g\x00u\x00i\x00.\x00"
+ b"d\x00i\x00a\x00l\x00o\x00g\x00.\x00I\x00m\x00a\x00g\x00e\x00F"
+ b"\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g\x00.\x00I\x00m\x00"
+ b"a\x00g\x00e\x00F\x00i\x00l\x00e\x00D\x00i\x00a\x00l\x00o\x00g"
+ b"\x00\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00#\x00\x00\x00"
+ b"\xFF\x00\x00\x00\x01\x00\x00\x00\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
+ b"\xFF\xFF\xFF\xFF\xFF\x01\xFF\xFF\xFF\xFF\x01\x00\x00\x00\x01\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C"
+ b"\x00\x00\x00\x00\xAA\x00\x00\x00\x0E\x00B\x00r\x00o\x00w\x00s"
+ b"\x00e\x00r\x00\x00\x00\x01\x00\x00\x00\x0C\x00\x00\x00\x00\x87"
+ b"\x00\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x01\x90\x00\x00\x00\x04\x01\x01\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d\xFF\xFF"
+ b"\xFF\xFF\x00\x00\x00\x81\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00"
+ b"\x00d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00"
+ b"\x01\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x01\x00\x00\x00"
+ b"\x00\x00\x00\x00d\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03"
+ b"\xE8\x00\xFF\xFF\xFF\xFF\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00"
+ b"\x00\x0C\x00\x00\x00\x000\x00\x00\x00\x10\x00C\x00o\x00l\x00o"
+ b"\x00r\x00m\x00a\x00p\x00\x00\x00\x01\x00\x00\x00\x08\x00g\x00"
+ b"r\x00a\x00y\x01\x01\x00\x00\x00\x06\x00l\x00o\x00g"
+ )
"""Serialized state on Qt5. Generated using :meth:`printState`"""
def testAvoidRestoreRegression_Version1(self):
@@ -752,7 +797,9 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filename = _tmpDirectory + "/data.h5"
- url = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/group/foobar")
+ url = silx.io.url.DataUrl(
+ scheme="silx", file_path=filename, data_path="/group/foobar"
+ )
dialog.selectUrl(url.path())
self.qWaitForPendingActions(dialog)
self.assertIsNone(dialog._selectedData())
diff --git a/src/silx/gui/dialog/utils.py b/src/silx/gui/dialog/utils.py
index 4c48930..1697bcf 100644
--- a/src/silx/gui/dialog/utils.py
+++ b/src/silx/gui/dialog/utils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -86,7 +85,7 @@ def patchToConsumeReturnKey(widget):
Monkey-patch a widget to consume the return key instead of propagating it
to the dialog.
"""
- assert(not hasattr(widget, "_oldKeyPressEvent"))
+ assert not hasattr(widget, "_oldKeyPressEvent")
def keyPressEvent(self, event):
k = event.key()
diff --git a/src/silx/gui/fit/BackgroundWidget.py b/src/silx/gui/fit/BackgroundWidget.py
index 7703ee1..d9cfcc8 100644
--- a/src/silx/gui/fit/BackgroundWidget.py
+++ b/src/silx/gui/fit/BackgroundWidget.py
@@ -1,5 +1,4 @@
-# coding: utf-8
-#/*##########################################################################
+# /*##########################################################################
# Copyright (C) 2004-2021 V.A. Sole, European Synchrotron Radiation Facility
#
# This file is part of the PyMca X-ray Fluorescence Toolkit developed at
@@ -45,8 +44,9 @@ __date__ = "28/06/2017"
class HorizontalSpacer(qt.QWidget):
def __init__(self, *args):
qt.QWidget.__init__(self, *args)
- self.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Fixed))
+ self.setSizePolicy(
+ qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
+ )
class BackgroundParamWidget(qt.QWidget):
@@ -57,6 +57,7 @@ class BackgroundParamWidget(qt.QWidget):
Updating the widgets causes :attr:`sigBackgroundParamWidgetSignal` to
be emitted.
"""
+
sigBackgroundParamWidgetSignal = qt.pyqtSignal(object)
def __init__(self, parent=None):
@@ -71,8 +72,7 @@ class BackgroundParamWidget(qt.QWidget):
self.algorithmCombo = qt.QComboBox(self)
self.algorithmCombo.addItem("Strip")
self.algorithmCombo.addItem("Snip")
- self.algorithmCombo.activated[int].connect(
- self._algorithmComboActivated)
+ self.algorithmCombo.activated[int].connect(self._algorithmComboActivated)
# Strip parameters ---------------------------------------------------
self.stripWidthLabel = qt.QLabel(self)
@@ -91,9 +91,10 @@ class BackgroundParamWidget(qt.QWidget):
self.stripIterValue.setText("0")
self.stripIterValue.editingFinished[()].connect(self._emitSignal)
self.stripIterValue.setToolTip(
- "Number of iterations for strip algorithm.\n" +
- "If greater than 999, an 2nd pass of strip filter is " +
- "applied to remove artifacts created by first pass.")
+ "Number of iterations for strip algorithm.\n"
+ + "If greater than 999, an 2nd pass of strip filter is "
+ + "applied to remove artifacts created by first pass."
+ )
# Snip parameters ----------------------------------------------------
self.snipWidthLabel = qt.QLabel(self)
@@ -104,7 +105,6 @@ class BackgroundParamWidget(qt.QWidget):
self.snipWidthSpin.setMinimum(0)
self.snipWidthSpin.valueChanged[int].connect(self._emitSignal)
-
# Smoothing parameters -----------------------------------------------
self.smoothingFlagCheck = qt.QCheckBox(self)
self.smoothingFlagCheck.setText("Smoothing Width (Savitsky-Golay)")
@@ -112,7 +112,7 @@ class BackgroundParamWidget(qt.QWidget):
self.smoothingSpin = qt.QSpinBox(self)
self.smoothingSpin.setMinimum(3)
- #self.smoothingSpin.setMaximum(40)
+ # self.smoothingSpin.setMaximum(40)
self.smoothingSpin.setSingleStep(2)
self.smoothingSpin.valueChanged[int].connect(self._emitSignal)
@@ -126,12 +126,12 @@ class BackgroundParamWidget(qt.QWidget):
self.anchorsFlagCheck = qt.QCheckBox(self.anchorsGroup)
self.anchorsFlagCheck.setText("Use anchors")
self.anchorsFlagCheck.setToolTip(
- "Define X coordinates of points that must remain fixed")
- self.anchorsFlagCheck.stateChanged[int].connect(
- self._anchorsToggled)
+ "Define X coordinates of points that must remain fixed"
+ )
+ self.anchorsFlagCheck.stateChanged[int].connect(self._anchorsToggled)
anchorsLayout.addWidget(self.anchorsFlagCheck)
- maxnchannel = 16384 * 4 # Fixme ?
+ maxnchannel = 16384 * 4 # Fixme ?
self.anchorsList = []
num_anchors = 4
for i in range(num_anchors):
@@ -171,8 +171,7 @@ class BackgroundParamWidget(qt.QWidget):
:param algorithm: "snip" or "strip"
"""
if algorithm not in ["strip", "snip"]:
- raise ValueError(
- "Unknown background filter algorithm %s" % algorithm)
+ raise ValueError("Unknown background filter algorithm %s" % algorithm)
self.algorithm = algorithm
self.stripWidthSpin.setEnabled(algorithm == "strip")
@@ -221,7 +220,7 @@ class BackgroundParamWidget(qt.QWidget):
if "AnchorsList" in ddict:
anchorslist = ddict["AnchorsList"]
- if anchorslist in [None, 'None']:
+ if anchorslist in [None, "None"]:
anchorslist = []
for spin in self.anchorsList:
spin.setValue(0)
@@ -249,20 +248,22 @@ class BackgroundParamWidget(qt.QWidget):
stripitertext = self.stripIterValue.text()
stripiter = int(stripitertext) if len(stripitertext) else 0
- return {"algorithm": self.algorithm,
- "StripThreshold": 1.0,
- "SnipWidth": self.snipWidthSpin.value(),
- "StripIterations": stripiter,
- "StripWidth": self.stripWidthSpin.value(),
- "SmoothingFlag": self.smoothingFlagCheck.isChecked(),
- "SmoothingWidth": self.smoothingSpin.value(),
- "AnchorsFlag": self.anchorsFlagCheck.isChecked(),
- "AnchorsList": [spin.value() for spin in self.anchorsList]}
+ return {
+ "algorithm": self.algorithm,
+ "StripThreshold": 1.0,
+ "SnipWidth": self.snipWidthSpin.value(),
+ "StripIterations": stripiter,
+ "StripWidth": self.stripWidthSpin.value(),
+ "SmoothingFlag": self.smoothingFlagCheck.isChecked(),
+ "SmoothingWidth": self.smoothingSpin.value(),
+ "AnchorsFlag": self.anchorsFlagCheck.isChecked(),
+ "AnchorsList": [spin.value() for spin in self.anchorsList],
+ }
def _emitSignal(self, dummy=None):
self.sigBackgroundParamWidgetSignal.emit(
- {'event': 'ParametersChanged',
- 'parameters': self.getParameters()})
+ {"event": "ParametersChanged", "parameters": self.getParameters()}
+ )
class BackgroundWidget(qt.QWidget):
@@ -271,6 +272,7 @@ class BackgroundWidget(qt.QWidget):
Strip and snip filters parameters can be adjusted using input widgets,
and the computed backgrounds are plotted next to the original data to
show the result."""
+
def __init__(self, parent=None):
qt.QWidget.__init__(self, parent)
self.setWindowTitle("Strip and SNIP Configuration Window")
@@ -329,8 +331,7 @@ class BackgroundWidget(qt.QWidget):
self._update()
def _update(self, resetzoom=False):
- """Compute strip and snip backgrounds, update the curves
- """
+ """Compute strip and snip backgrounds, update the curves"""
if self._y is None:
return
@@ -339,7 +340,7 @@ class BackgroundWidget(qt.QWidget):
# smoothed data
y = numpy.ravel(numpy.array(self._y)).astype(numpy.float64)
if pars["SmoothingFlag"]:
- ysmooth = filters.savitsky_golay(y, pars['SmoothingWidth'])
+ ysmooth = filters.savitsky_golay(y, pars["SmoothingWidth"])
f = [0.25, 0.5, 0.25]
ysmooth[1:-1] = numpy.convolve(ysmooth, f, mode=0)
ysmooth[0] = 0.5 * (ysmooth[0] + ysmooth[1])
@@ -347,14 +348,13 @@ class BackgroundWidget(qt.QWidget):
else:
ysmooth = y
-
# loop for anchors
x = self._x
- niter = pars['StripIterations']
+ niter = pars["StripIterations"]
anchors_indices = []
- if pars['AnchorsFlag'] and pars['AnchorsList'] is not None:
+ if pars["AnchorsFlag"] and pars["AnchorsList"] is not None:
ravelled = x
- for channel in pars['AnchorsList']:
+ for channel in pars["AnchorsList"]:
if channel <= ravelled[0]:
continue
index = numpy.nonzero(ravelled >= channel)[0]
@@ -363,52 +363,56 @@ class BackgroundWidget(qt.QWidget):
if index > 0:
anchors_indices.append(index)
- stripBackground = filters.strip(ysmooth,
- w=pars['StripWidth'],
- niterations=niter,
- factor=pars['StripThreshold'],
- anchors=anchors_indices)
+ stripBackground = filters.strip(
+ ysmooth,
+ w=pars["StripWidth"],
+ niterations=niter,
+ factor=pars["StripThreshold"],
+ anchors=anchors_indices,
+ )
if niter >= 1000:
# final smoothing
- stripBackground = filters.strip(stripBackground,
- w=1,
- niterations=50*pars['StripWidth'],
- factor=pars['StripThreshold'],
- anchors=anchors_indices)
+ stripBackground = filters.strip(
+ stripBackground,
+ w=1,
+ niterations=50 * pars["StripWidth"],
+ factor=pars["StripThreshold"],
+ anchors=anchors_indices,
+ )
if len(anchors_indices) == 0:
- anchors_indices = [0, len(ysmooth)-1]
+ anchors_indices = [0, len(ysmooth) - 1]
anchors_indices.sort()
snipBackground = 0.0 * ysmooth
lastAnchor = 0
for anchor in anchors_indices:
if (anchor > lastAnchor) and (anchor < len(ysmooth)):
- snipBackground[lastAnchor:anchor] =\
- filters.snip1d(ysmooth[lastAnchor:anchor],
- pars['SnipWidth'])
+ snipBackground[lastAnchor:anchor] = filters.snip1d(
+ ysmooth[lastAnchor:anchor], pars["SnipWidth"]
+ )
lastAnchor = anchor
if lastAnchor < len(ysmooth):
- snipBackground[lastAnchor:] =\
- filters.snip1d(ysmooth[lastAnchor:],
- pars['SnipWidth'])
-
- self.graphWidget.addCurve(x, y,
- legend='Input Data',
- replace=True,
- resetzoom=resetzoom)
- self.graphWidget.addCurve(x, stripBackground,
- legend='Strip Background',
- resetzoom=False)
- self.graphWidget.addCurve(x, snipBackground,
- legend='SNIP Background',
- resetzoom=False)
+ snipBackground[lastAnchor:] = filters.snip1d(
+ ysmooth[lastAnchor:], pars["SnipWidth"]
+ )
+
+ self.graphWidget.addCurve(
+ x, y, legend="Input Data", replace=True, resetzoom=resetzoom
+ )
+ self.graphWidget.addCurve(
+ x, stripBackground, legend="Strip Background", resetzoom=False
+ )
+ self.graphWidget.addCurve(
+ x, snipBackground, legend="SNIP Background", resetzoom=False
+ )
if self._xmin is not None and self._xmax is not None:
self.graphWidget.getXAxis().setLimits(self._xmin, self._xmax)
class BackgroundDialog(qt.QDialog):
"""QDialog window featuring a :class:`BackgroundWidget`"""
+
def __init__(self, parent=None):
qt.QDialog.__init__(self, parent)
self.setWindowTitle("Strip and Snip Configuration Window")
@@ -453,14 +457,15 @@ class BackgroundDialog(qt.QDialog):
# self.output = ddict
def accept(self):
- """Update :attr:`output`, then call :meth:`QDialog.accept`
- """
+ """Update :attr:`output`, then call :meth:`QDialog.accept`"""
self.output = self.getParameters()
super(BackgroundDialog, self).accept()
def sizeHint(self):
- return qt.QSize(int(1.5*qt.QDialog.sizeHint(self).width()),
- qt.QDialog.sizeHint(self).height())
+ return qt.QSize(
+ int(1.5 * qt.QDialog.sizeHint(self).width()),
+ qt.QDialog.sizeHint(self).height(),
+ )
def setData(self, x, y, xmin=None, xmax=None):
"""See :meth:`BackgroundWidget.setData`"""
@@ -499,11 +504,7 @@ def main():
x = numpy.arange(5000)
# (height1, center1, fwhm1, ...) 5 peaks
- params1 = (50, 500, 100,
- 20, 2000, 200,
- 50, 2250, 100,
- 40, 3000, 75,
- 23, 4000, 150)
+ params1 = (50, 500, 100, 20, 2000, 200, 50, 2250, 100, 40, 3000, 75, 23, 4000, 150)
y0 = sum_gauss(x, *params1)
# random values between [-1;1]
@@ -528,7 +529,8 @@ def main():
w.parametersWidget.parametersWidget.sigBackgroundParamWidgetSignal.connect(mySlot)
w.setData(x, y)
w.exec()
- #a.exec()
+ # a.exec()
+
if __name__ == "__main__":
main()
diff --git a/src/silx/gui/fit/FitConfig.py b/src/silx/gui/fit/FitConfig.py
index 48ebca2..5887b4a 100644
--- a/src/silx/gui/fit/FitConfig.py
+++ b/src/silx/gui/fit/FitConfig.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
# Copyright (C) 2004-2021 V.A. Sole, European Synchrotron Radiation Facility
#
@@ -46,6 +45,7 @@ class TabsDialog(qt.QDialog):
This dialog defines a __len__ returning the number of tabs,
and an __iter__ method yielding the tab widgets.
"""
+
def __init__(self, parent=None):
qt.QDialog.__init__(self, parent)
self.tabWidget = qt.QTabWidget(self)
@@ -63,9 +63,9 @@ class TabsDialog(qt.QDialog):
self.buttonDefault.setText("Undo changes")
layout2.addWidget(self.buttonDefault)
- spacer = qt.QSpacerItem(20, 20,
- qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Minimum)
+ spacer = qt.QSpacerItem(
+ 20, 20, qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum
+ )
layout2.addItem(spacer)
self.buttonOk = qt.QPushButton(self)
@@ -120,6 +120,7 @@ class TabsDialogData(TabsDialog):
A default dictionary can be supplied when this dialog is initialized, to
be used as default data for :attr:`output`.
"""
+
def __init__(self, parent=None, modal=True, default=None):
"""
@@ -198,11 +199,14 @@ class ConstraintsPage(qt.QGroupBox):
"""Checkable QGroupBox widget filled with QCheckBox widgets,
to configure the fit estimation for standard fit theories.
"""
+
def __init__(self, parent=None, title="Set constraints"):
super(ConstraintsPage, self).__init__(parent)
self.setTitle(title)
- self.setToolTip("Disable 'Set constraints' to remove all " +
- "constraints on all fit parameters")
+ self.setToolTip(
+ "Disable 'Set constraints' to remove all "
+ + "constraints on all fit parameters"
+ )
self.setCheckable(True)
layout = qt.QVBoxLayout(self)
@@ -213,8 +217,7 @@ class ConstraintsPage(qt.QGroupBox):
layout.addWidget(self.positiveHeightCB)
self.positionInIntervalCB = qt.QCheckBox("Force position in interval", self)
- self.positionInIntervalCB.setToolTip(
- "Fit must position peak within X limits")
+ self.positionInIntervalCB.setToolTip("Fit must position peak within X limits")
layout.addWidget(self.positionInIntervalCB)
self.positiveFwhmCB = qt.QCheckBox("Force positive FWHM", self)
@@ -227,7 +230,8 @@ class ConstraintsPage(qt.QGroupBox):
self.quotedEtaCB = qt.QCheckBox("Force Eta between 0 and 1", self)
self.quotedEtaCB.setToolTip(
- "Fit must find Eta between 0 and 1 for pseudo-Voigt function")
+ "Fit must find Eta between 0 and 1 for pseudo-Voigt function"
+ )
layout.addWidget(self.quotedEtaCB)
layout.addStretch()
@@ -242,29 +246,27 @@ class ConstraintsPage(qt.QGroupBox):
if default_dict is None:
default_dict = {}
# this one uses reverse logic: if checked, NoConstraintsFlag must be False
- self.setChecked(
- not default_dict.get('NoConstraintsFlag', False))
+ self.setChecked(not default_dict.get("NoConstraintsFlag", False))
self.positiveHeightCB.setChecked(
- default_dict.get('PositiveHeightAreaFlag', True))
+ default_dict.get("PositiveHeightAreaFlag", True)
+ )
self.positionInIntervalCB.setChecked(
- default_dict.get('QuotedPositionFlag', False))
- self.positiveFwhmCB.setChecked(
- default_dict.get('PositiveFwhmFlag', True))
- self.sameFwhmCB.setChecked(
- default_dict.get('SameFwhmFlag', False))
- self.quotedEtaCB.setChecked(
- default_dict.get('QuotedEtaFlag', False))
+ default_dict.get("QuotedPositionFlag", False)
+ )
+ self.positiveFwhmCB.setChecked(default_dict.get("PositiveFwhmFlag", True))
+ self.sameFwhmCB.setChecked(default_dict.get("SameFwhmFlag", False))
+ self.quotedEtaCB.setChecked(default_dict.get("QuotedEtaFlag", False))
def get(self):
"""Return a dictionary of constraint flags, to be processed by the
:meth:`configure` method of the selected fit theory."""
ddict = {
- 'NoConstraintsFlag': not self.isChecked(),
- 'PositiveHeightAreaFlag': self.positiveHeightCB.isChecked(),
- 'QuotedPositionFlag': self.positionInIntervalCB.isChecked(),
- 'PositiveFwhmFlag': self.positiveFwhmCB.isChecked(),
- 'SameFwhmFlag': self.sameFwhmCB.isChecked(),
- 'QuotedEtaFlag': self.quotedEtaCB.isChecked(),
+ "NoConstraintsFlag": not self.isChecked(),
+ "PositiveHeightAreaFlag": self.positiveHeightCB.isChecked(),
+ "QuotedPositionFlag": self.positionInIntervalCB.isChecked(),
+ "PositiveFwhmFlag": self.positiveFwhmCB.isChecked(),
+ "SameFwhmFlag": self.sameFwhmCB.isChecked(),
+ "QuotedEtaFlag": self.quotedEtaCB.isChecked(),
}
return ddict
@@ -277,8 +279,9 @@ class SearchPage(qt.QWidget):
self.manualFwhmGB = qt.QGroupBox("Define FWHM manually", self)
self.manualFwhmGB.setCheckable(True)
self.manualFwhmGB.setToolTip(
- "If disabled, the FWHM parameter used for peak search is " +
- "estimated based on the highest peak in the data")
+ "If disabled, the FWHM parameter used for peak search is "
+ + "estimated based on the highest peak in the data"
+ )
layout.addWidget(self.manualFwhmGB)
# ------------ GroupBox fwhm--------------------------
layout2 = qt.QHBoxLayout(self.manualFwhmGB)
@@ -296,8 +299,9 @@ class SearchPage(qt.QWidget):
self.manualScalingGB = qt.QGroupBox("Define scaling manually", self)
self.manualScalingGB.setCheckable(True)
self.manualScalingGB.setToolTip(
- "If disabled, the Y scaling used for peak search is " +
- "estimated automatically")
+ "If disabled, the Y scaling used for peak search is "
+ + "estimated automatically"
+ )
layout.addWidget(self.manualScalingGB)
# ------------ GroupBox scaling-----------------------
layout3 = qt.QHBoxLayout(self.manualScalingGB)
@@ -308,8 +312,8 @@ class SearchPage(qt.QWidget):
self.yScalingEntry = qt.QLineEdit(self.manualScalingGB)
self.yScalingEntry.setToolTip(
- "Data values will be multiplied by this value prior to peak" +
- " search")
+ "Data values will be multiplied by this value prior to peak" + " search"
+ )
self.yScalingEntry.setValidator(qt.QDoubleValidator(self))
layout3.addWidget(self.yScalingEntry)
# ----------------------------------------------------
@@ -324,9 +328,10 @@ class SearchPage(qt.QWidget):
self.sensitivityEntry = qt.QLineEdit(containerWidget)
self.sensitivityEntry.setToolTip(
- "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)")
+ "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(self)
sensivalidator.setBottom(1.0)
self.sensitivityEntry.setValidator(sensivalidator)
@@ -336,8 +341,9 @@ class SearchPage(qt.QWidget):
self.forcePeakPresenceCB = qt.QCheckBox("Force peak presence", self)
self.forcePeakPresenceCB.setToolTip(
- "If peak search algorithm is unsuccessful, place one peak " +
- "at the maximum of the curve")
+ "If peak search algorithm is unsuccessful, place one peak "
+ + "at the maximum of the curve"
+ )
layout.addWidget(self.forcePeakPresenceCB)
layout.addStretch()
@@ -351,29 +357,25 @@ class SearchPage(qt.QWidget):
a parameter, its values are used as default values."""
if default_dict is None:
default_dict = {}
- self.manualFwhmGB.setChecked(
- not default_dict.get('AutoFwhm', True))
- self.fwhmPointsSpin.setValue(
- default_dict.get('FwhmPoints', 8))
- self.sensitivityEntry.setText(
- str(default_dict.get('Sensitivity', 1.0)))
- self.manualScalingGB.setChecked(
- not default_dict.get('AutoScaling', False))
- self.yScalingEntry.setText(
- str(default_dict.get('Yscaling', 1.0)))
+ self.manualFwhmGB.setChecked(not default_dict.get("AutoFwhm", True))
+ self.fwhmPointsSpin.setValue(default_dict.get("FwhmPoints", 8))
+ self.sensitivityEntry.setText(str(default_dict.get("Sensitivity", 1.0)))
+ self.manualScalingGB.setChecked(not default_dict.get("AutoScaling", False))
+ self.yScalingEntry.setText(str(default_dict.get("Yscaling", 1.0)))
self.forcePeakPresenceCB.setChecked(
- default_dict.get('ForcePeakPresence', False))
+ default_dict.get("ForcePeakPresence", False)
+ )
def get(self):
"""Return a dictionary of peak search parameters, to be processed by
the :meth:`configure` method of the selected fit theory."""
ddict = {
- 'AutoFwhm': not self.manualFwhmGB.isChecked(),
- 'FwhmPoints': self.fwhmPointsSpin.value(),
- 'Sensitivity': safe_float(self.sensitivityEntry.text()),
- 'AutoScaling': not self.manualScalingGB.isChecked(),
- 'Yscaling': safe_float(self.yScalingEntry.text()),
- 'ForcePeakPresence': self.forcePeakPresenceCB.isChecked()
+ "AutoFwhm": not self.manualFwhmGB.isChecked(),
+ "FwhmPoints": self.fwhmPointsSpin.value(),
+ "Sensitivity": safe_float(self.sensitivityEntry.text()),
+ "AutoScaling": not self.manualScalingGB.isChecked(),
+ "Yscaling": safe_float(self.yScalingEntry.text()),
+ "ForcePeakPresence": self.forcePeakPresenceCB.isChecked(),
}
return ddict
@@ -381,60 +383,69 @@ class SearchPage(qt.QWidget):
class BackgroundPage(qt.QGroupBox):
"""Background subtraction configuration, specific to fittheories
estimation functions."""
- def __init__(self, parent=None,
- title="Subtract strip background prior to estimation"):
+
+ def __init__(
+ self, parent=None, title="Subtract strip background prior to estimation"
+ ):
super(BackgroundPage, self).__init__(parent)
self.setTitle(title)
self.setCheckable(True)
self.setToolTip(
- "The strip algorithm strips away peaks to compute the " +
- "background signal.\nAt each iteration, a sample is compared " +
- "to the average of the two samples at a given distance in both" +
- " directions,\n and if its value is higher than the average,"
- "it is replaced by the average.")
+ "The strip algorithm strips away peaks to compute the "
+ + "background signal.\nAt each iteration, a sample is compared "
+ + "to the average of the two samples at a given distance in both"
+ + " directions,\n and if its value is higher than the average,"
+ "it is replaced by the average."
+ )
layout = qt.QGridLayout(self)
self.setLayout(layout)
for i, label_text in enumerate(
- ["Strip width (in samples)",
- "Number of iterations",
- "Strip threshold factor"]):
+ [
+ "Strip width (in samples)",
+ "Number of iterations",
+ "Strip threshold factor",
+ ]
+ ):
label = qt.QLabel(label_text)
layout.addWidget(label, i, 0)
self.stripWidthSpin = qt.QSpinBox(self)
self.stripWidthSpin.setToolTip(
- "Width, in number of samples, of the strip operator")
+ "Width, in number of samples, of the strip operator"
+ )
self.stripWidthSpin.setRange(1, 999999)
layout.addWidget(self.stripWidthSpin, 0, 1)
self.numIterationsSpin = qt.QSpinBox(self)
- self.numIterationsSpin.setToolTip(
- "Number of iterations of the strip algorithm")
+ self.numIterationsSpin.setToolTip("Number of iterations of the strip algorithm")
self.numIterationsSpin.setRange(1, 999999)
layout.addWidget(self.numIterationsSpin, 1, 1)
self.thresholdFactorEntry = qt.QLineEdit(self)
self.thresholdFactorEntry.setToolTip(
- "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")
+ "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))
layout.addWidget(self.thresholdFactorEntry, 2, 1)
self.smoothStripGB = qt.QGroupBox("Apply smoothing prior to strip", self)
self.smoothStripGB.setCheckable(True)
self.smoothStripGB.setToolTip(
- "Apply a smoothing before subtracting strip background" +
- " in fit and estimate processes")
+ "Apply a smoothing before subtracting strip background"
+ + " in fit and estimate processes"
+ )
smoothlayout = qt.QHBoxLayout(self.smoothStripGB)
label = qt.QLabel("Smoothing width (Savitsky-Golay)")
smoothlayout.addWidget(label)
self.smoothingWidthSpin = qt.QSpinBox(self)
self.smoothingWidthSpin.setToolTip(
- "Width parameter for Savitsky-Golay smoothing (number of samples, must be odd)")
+ "Width parameter for Savitsky-Golay smoothing (number of samples, must be odd)"
+ )
self.smoothingWidthSpin.setRange(3, 101)
self.smoothingWidthSpin.setSingleStep(2)
smoothlayout.addWidget(self.smoothingWidthSpin)
@@ -453,31 +464,25 @@ class BackgroundPage(qt.QGroupBox):
if default_dict is None:
default_dict = {}
- self.setChecked(
- default_dict.get('StripBackgroundFlag', True))
+ self.setChecked(default_dict.get("StripBackgroundFlag", True))
- self.stripWidthSpin.setValue(
- default_dict.get('StripWidth', 2))
- self.numIterationsSpin.setValue(
- default_dict.get('StripIterations', 5000))
- self.thresholdFactorEntry.setText(
- str(default_dict.get('StripThreshold', 1.0)))
- self.smoothStripGB.setChecked(
- default_dict.get('SmoothingFlag', False))
- self.smoothingWidthSpin.setValue(
- default_dict.get('SmoothingWidth', 3))
+ self.stripWidthSpin.setValue(default_dict.get("StripWidth", 2))
+ self.numIterationsSpin.setValue(default_dict.get("StripIterations", 5000))
+ self.thresholdFactorEntry.setText(str(default_dict.get("StripThreshold", 1.0)))
+ self.smoothStripGB.setChecked(default_dict.get("SmoothingFlag", False))
+ self.smoothingWidthSpin.setValue(default_dict.get("SmoothingWidth", 3))
def get(self):
"""Return a dictionary of background subtraction parameters, to be
processed by the :meth:`configure` method of the selected fit theory.
"""
ddict = {
- 'StripBackgroundFlag': self.isChecked(),
- 'StripWidth': self.stripWidthSpin.value(),
- 'StripIterations': self.numIterationsSpin.value(),
- 'StripThreshold': safe_float(self.thresholdFactorEntry.text()),
- 'SmoothingFlag': self.smoothStripGB.isChecked(),
- 'SmoothingWidth': self.smoothingWidthSpin.value()
+ "StripBackgroundFlag": self.isChecked(),
+ "StripWidth": self.stripWidthSpin.value(),
+ "StripIterations": self.numIterationsSpin.value(),
+ "StripThreshold": safe_float(self.thresholdFactorEntry.text()),
+ "SmoothingFlag": self.smoothStripGB.isChecked(),
+ "SmoothingWidth": self.smoothingWidthSpin.value(),
}
return ddict
@@ -539,5 +544,6 @@ def main():
a.exec()
+
if __name__ == "__main__":
main()
diff --git a/src/silx/gui/fit/FitWidget.py b/src/silx/gui/fit/FitWidget.py
index 52ecafe..2487c23 100644
--- a/src/silx/gui/fit/FitWidget.py
+++ b/src/silx/gui/fit/FitWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# This file is part of the PyMca X-ray Fluorescence Toolkit developed at
# the ESRF by the Software group.
@@ -47,11 +46,14 @@ import traceback
from silx.math.fit import fittheories
from silx.math.fit import fitmanager, functions
from silx.gui import qt
-from .FitWidgets import (FitActionsButtons, FitStatusLines,
- FitConfigWidget, ParametersTab)
+from .FitWidgets import (
+ FitActionsButtons,
+ FitStatusLines,
+ FitConfigWidget,
+ ParametersTab,
+)
from .FitConfig import getFitConfigDialog
from .BackgroundWidget import getBgDialog, BackgroundDialog
-from ...utils.deprecation import deprecated
DEBUG = 0
_logger = logging.getLogger(__name__)
@@ -89,6 +91,7 @@ class FitWidget(qt.QWidget):
.. image:: img/FitWidget.png
"""
+
sigFitWidgetSignal = qt.Signal(object)
"""This signal is emitted by the estimation and fit methods.
It carries a dictionary with two items:
@@ -106,8 +109,15 @@ class FitWidget(qt.QWidget):
:attr:`silx.math.fit.fitmanager.FitManager.fit_results`)
"""
- def __init__(self, parent=None, title=None, fitmngr=None,
- enableconfig=True, enablestatus=True, enablebuttons=True):
+ def __init__(
+ self,
+ parent=None,
+ title=None,
+ fitmngr=None,
+ enableconfig=True,
+ enablestatus=True,
+ enablebuttons=True,
+ ):
"""
:param parent: Parent widget
@@ -200,15 +210,16 @@ class FitWidget(qt.QWidget):
"""Function selector and configuration widget"""
self.guiConfig.FunConfigureButton.clicked.connect(
- self.__funConfigureGuiSlot)
- self.guiConfig.BgConfigureButton.clicked.connect(
- self.__bgConfigureGuiSlot)
+ self.__funConfigureGuiSlot
+ )
+ self.guiConfig.BgConfigureButton.clicked.connect(self.__bgConfigureGuiSlot)
self.guiConfig.WeightCheckBox.setChecked(
- self.fitconfig.get("WeightFlag", False))
+ self.fitconfig.get("WeightFlag", False)
+ )
self.guiConfig.WeightCheckBox.stateChanged[int].connect(self.weightEvent)
- if qt.BINDING in ('PySide2', 'PyQt5'):
+ if qt.BINDING == "PyQt5":
self.guiConfig.BkgComBox.activated[str].connect(self.bkgEvent)
self.guiConfig.FunComBox.activated[str].connect(self.funEvent)
else: # Qt6
@@ -263,21 +274,21 @@ class FitWidget(qt.QWidget):
# associate silx.gui.fit.FitConfig with all theories
# Users can later associate their own custom dialogs to
# replace the default.
- configdialog = getFitConfigDialog(parent=self,
- default=self.fitconfig)
+ configdialog = getFitConfigDialog(parent=self, default=self.fitconfig)
for theory in self.fitmanager.theories:
self.associateConfigDialog(theory, configdialog)
for bgtheory in self.fitmanager.bgtheories:
- self.associateConfigDialog(bgtheory, configdialog,
- theory_is_background=True)
+ self.associateConfigDialog(
+ bgtheory, configdialog, theory_is_background=True
+ )
# associate silx.gui.fit.BackgroundWidget with Strip and Snip
- bgdialog = getBgDialog(parent=self,
- default=self.fitconfig)
+ bgdialog = getBgDialog(parent=self, default=self.fitconfig)
for bgtheory in ["Strip", "Snip"]:
if bgtheory in self.fitmanager.bgtheories:
- self.associateConfigDialog(bgtheory, bgdialog,
- theory_is_background=True)
+ self.associateConfigDialog(
+ bgtheory, bgdialog, theory_is_background=True
+ )
def _populateFunctions(self):
"""Fill combo-boxes with fit theories and background theories
@@ -287,16 +298,18 @@ class FitWidget(qt.QWidget):
for theory_name in self.fitmanager.bgtheories:
self.guiConfig.BkgComBox.addItem(theory_name)
self.guiConfig.BkgComBox.setItemData(
- self.guiConfig.BkgComBox.findText(theory_name),
- self.fitmanager.bgtheories[theory_name].description,
- qt.Qt.ToolTipRole)
+ self.guiConfig.BkgComBox.findText(theory_name),
+ self.fitmanager.bgtheories[theory_name].description,
+ qt.Qt.ToolTipRole,
+ )
for theory_name in self.fitmanager.theories:
self.guiConfig.FunComBox.addItem(theory_name)
self.guiConfig.FunComBox.setItemData(
- self.guiConfig.FunComBox.findText(theory_name),
- self.fitmanager.theories[theory_name].description,
- qt.Qt.ToolTipRole)
+ self.guiConfig.FunComBox.findText(theory_name),
+ self.fitmanager.theories[theory_name].description,
+ qt.Qt.ToolTipRole,
+ )
# - activate selected fit theory (if any)
# - activate selected bg theory (if any)
@@ -320,10 +333,6 @@ class FitWidget(qt.QWidget):
configuration.update(self.configure())
- @deprecated(replacement='setData', since_version='0.3.0')
- def setdata(self, x, y, sigmay=None, xmin=None, xmax=None):
- self.setData(x, y, sigmay, xmin, xmax)
-
def setData(self, x=None, y=None, sigmay=None, xmin=None, xmax=None):
"""Set data to be fitted.
@@ -346,14 +355,14 @@ class FitWidget(qt.QWidget):
else:
self.guibuttons.EstimateButton.setEnabled(True)
self.guibuttons.StartFitButton.setEnabled(True)
- self.fitmanager.setdata(x=x, y=y, sigmay=sigmay,
- xmin=xmin, xmax=xmax)
+ self.fitmanager.setdata(x=x, y=y, sigmay=sigmay, xmin=xmin, xmax=xmax)
for config_dialog in self.bgconfigdialogs.values():
if isinstance(config_dialog, BackgroundDialog):
config_dialog.setData(x, y, xmin=xmin, xmax=xmax)
- def associateConfigDialog(self, theory_name, config_widget,
- theory_is_background=False):
+ def associateConfigDialog(
+ self, theory_name, config_widget, theory_is_background=False
+ ):
"""Associate an instance of custom configuration dialog widget to
a fit theory or to a background theory.
@@ -373,23 +382,30 @@ class FitWidget(qt.QWidget):
methods (*show*, *exec*, *result*, *setDefault*) or the mandatory
attribute (*output*).
"""
- theories = self.fitmanager.bgtheories if theory_is_background else\
- self.fitmanager.theories
+ theories = (
+ self.fitmanager.bgtheories
+ if theory_is_background
+ else self.fitmanager.theories
+ )
if theory_name not in theories:
raise KeyError("%s does not match an existing fitmanager theory")
if config_widget is not None:
- if (not hasattr(config_widget, "exec") and
- not hasattr(config_widget, "exec_")):
+ if not hasattr(config_widget, "exec") and not hasattr(
+ config_widget, "exec_"
+ ):
raise AttributeError(
- "Custom configuration widget must define exec or exec_")
+ "Custom configuration widget must define exec or exec_"
+ )
for mandatory_attr in ["show", "result", "output"]:
if not hasattr(config_widget, mandatory_attr):
raise AttributeError(
- "Custom configuration widget must define " +
- "attribute or method " + mandatory_attr)
+ "Custom configuration widget must define "
+ + "attribute or method "
+ + mandatory_attr
+ )
if theory_is_background:
self.bgconfigdialogs[theory_name] = config_widget
@@ -428,25 +444,23 @@ class FitWidget(qt.QWidget):
configuration.update(self.configure(**newconfiguration))
# set fit function theory
try:
- i = 1 + \
- list(self.fitmanager.theories.keys()).index(
- self.fitmanager.selectedtheory)
+ i = 1 + list(self.fitmanager.theories.keys()).index(
+ self.fitmanager.selectedtheory
+ )
self.guiConfig.FunComBox.setCurrentIndex(i)
self.funEvent(self.fitmanager.selectedtheory)
except ValueError:
- _logger.error("Function not in list %s",
- self.fitmanager.selectedtheory)
+ _logger.error("Function not in list %s", self.fitmanager.selectedtheory)
self.funEvent(list(self.fitmanager.theories.keys())[0])
# current background
try:
- i = 1 + \
- list(self.fitmanager.bgtheories.keys()).index(
- self.fitmanager.selectedbg)
+ i = 1 + list(self.fitmanager.bgtheories.keys()).index(
+ self.fitmanager.selectedbg
+ )
self.guiConfig.BkgComBox.setCurrentIndex(i)
self.bkgEvent(self.fitmanager.selectedbg)
except ValueError:
- _logger.error("Background not in list %s",
- self.fitmanager.selectedbg)
+ _logger.error("Background not in list %s", self.fitmanager.selectedbg)
self.bkgEvent(list(self.fitmanager.bgtheories.keys())[0])
# update the Gui
@@ -510,8 +524,7 @@ class FitWidget(qt.QWidget):
theory_name = self.fitmanager.selectedtheory
estimation_function = self.fitmanager.theories[theory_name].estimate
if estimation_function is not None:
- ddict = {'event': 'EstimateStarted',
- 'data': None}
+ ddict = {"event": "EstimateStarted", "data": None}
self._emitSignal(ddict)
self.fitmanager.estimate(callback=self.fitStatus)
else:
@@ -521,34 +534,25 @@ class FitWidget(qt.QWidget):
text += "the initial parameters. Please, fill them\n"
text += "yourself in the table and press Start Fit\n"
msg.setText(text)
- msg.setWindowTitle('FitWidget Message')
+ msg.setWindowTitle("FitWidget Message")
msg.exec()
return
- except Exception as e: # noqa (we want to catch and report all errors)
- _logger.warning('Estimate error: %s', traceback.format_exc())
+ except Exception as e: # noqa (we want to catch and report all errors)
+ _logger.warning("Estimate error: %s", traceback.format_exc())
msg = qt.QMessageBox(self)
msg.setIcon(qt.QMessageBox.Critical)
msg.setWindowTitle("Estimate Error")
msg.setText("Error on estimate: %s" % e)
msg.exec()
- ddict = {
- 'event': 'EstimateFailed',
- 'data': None}
+ ddict = {"event": "EstimateFailed", "data": None}
self._emitSignal(ddict)
return
- self.guiParameters.fillFromFit(
- self.fitmanager.fit_results, view='Fit')
- self.guiParameters.removeAllViews(keep='Fit')
- ddict = {
- 'event': 'EstimateFinished',
- 'data': self.fitmanager.fit_results}
+ self.guiParameters.fillFromFit(self.fitmanager.fit_results, view="Fit")
+ self.guiParameters.removeAllViews(keep="Fit")
+ ddict = {"event": "EstimateFinished", "data": self.fitmanager.fit_results}
self._emitSignal(ddict)
- @deprecated(replacement='startFit', since_version='0.3.0')
- def startfit(self):
- self.startFit()
-
def startFit(self):
"""Run fit, then emit :attr:`sigFitWidgetSignal` with a dictionary
containing a status message and a list of fit
@@ -564,31 +568,23 @@ class FitWidget(qt.QWidget):
"""
self.fitmanager.fit_results = self.guiParameters.getFitResults()
try:
- ddict = {'event': 'FitStarted',
- 'data': None}
+ ddict = {"event": "FitStarted", "data": None}
self._emitSignal(ddict)
self.fitmanager.runfit(callback=self.fitStatus)
except Exception as e: # noqa (we want to catch and report all errors)
- _logger.warning('Estimate error: %s', traceback.format_exc())
+ _logger.warning("Estimate error: %s", traceback.format_exc())
msg = qt.QMessageBox(self)
msg.setIcon(qt.QMessageBox.Critical)
msg.setWindowTitle("Fit Error")
msg.setText("Error on Fit: %s" % e)
msg.exec()
- ddict = {
- 'event': 'FitFailed',
- 'data': None
- }
+ ddict = {"event": "FitFailed", "data": None}
self._emitSignal(ddict)
return
- self.guiParameters.fillFromFit(
- self.fitmanager.fit_results, view='Fit')
- self.guiParameters.removeAllViews(keep='Fit')
- ddict = {
- 'event': 'FitFinished',
- 'data': self.fitmanager.fit_results
- }
+ self.guiParameters.fillFromFit(self.fitmanager.fit_results, view="Fit")
+ self.guiParameters.removeAllViews(keep="Fit")
+ ddict = {"event": "FitFinished", "data": self.fitmanager.fit_results}
self._emitSignal(ddict)
return
@@ -599,15 +595,17 @@ class FitWidget(qt.QWidget):
self.fitmanager.setbackground(bgtheory)
else:
functionsfile = qt.QFileDialog.getOpenFileName(
- self, "Select python module with your function(s)", "",
- "Python Files (*.py);;All Files (*)")
+ self,
+ "Select python module with your function(s)",
+ "",
+ "Python Files (*.py);;All Files (*)",
+ )
if len(functionsfile):
try:
self.fitmanager.loadbgtheories(functionsfile)
except ImportError:
- qt.QMessageBox.critical(self, "ERROR",
- "Function not imported")
+ qt.QMessageBox.critical(self, "ERROR", "Function not imported")
return
else:
# empty the ComboBox
@@ -617,9 +615,9 @@ class FitWidget(qt.QWidget):
for key in self.fitmanager.bgtheories:
self.guiConfig.BkgComBox.addItem(str(key))
- i = 1 + \
- list(self.fitmanager.bgtheories.keys()).index(
- self.fitmanager.selectedbg)
+ i = 1 + list(self.fitmanager.bgtheories.keys()).index(
+ self.fitmanager.selectedbg
+ )
self.guiConfig.BkgComBox.setCurrentIndex(i)
self.__initialParameters()
@@ -638,15 +636,17 @@ class FitWidget(qt.QWidget):
else:
# open a load file dialog
functionsfile = qt.QFileDialog.getOpenFileName(
- self, "Select python module with your function(s)", "",
- "Python Files (*.py);;All Files (*)")
+ self,
+ "Select python module with your function(s)",
+ "",
+ "Python Files (*.py);;All Files (*)",
+ )
if len(functionsfile):
try:
self.fitmanager.loadtheories(functionsfile)
except ImportError:
- qt.QMessageBox.critical(self, "ERROR",
- "Function not imported")
+ qt.QMessageBox.critical(self, "ERROR", "Function not imported")
return
else:
# empty the ComboBox
@@ -656,9 +656,9 @@ class FitWidget(qt.QWidget):
for key in self.fitmanager.theories:
self.guiConfig.FunComBox.addItem(str(key))
- i = 1 + \
- list(self.fitmanager.theories.keys()).index(
- self.fitmanager.selectedtheory)
+ i = 1 + list(self.fitmanager.theories.keys()).index(
+ self.fitmanager.selectedtheory
+ )
self.guiConfig.FunComBox.setCurrentIndex(i)
self.__initialParameters()
@@ -683,45 +683,52 @@ class FitWidget(qt.QWidget):
self.fitmanager.fit_results = []
for pname in self.fitmanager.bgtheories[self.fitmanager.selectedbg].parameters:
self.fitmanager.parameter_names.append(pname)
- self.fitmanager.fit_results.append({'name': pname,
- 'estimation': 0,
- 'group': 0,
- 'code': 'FREE',
- 'cons1': 0,
- 'cons2': 0,
- 'fitresult': 0.0,
- 'sigma': 0.0,
- 'xmin': None,
- 'xmax': None})
+ self.fitmanager.fit_results.append(
+ {
+ "name": pname,
+ "estimation": 0,
+ "group": 0,
+ "code": "FREE",
+ "cons1": 0,
+ "cons2": 0,
+ "fitresult": 0.0,
+ "sigma": 0.0,
+ "xmin": None,
+ "xmax": None,
+ }
+ )
if self.fitmanager.selectedtheory is not None:
theory = self.fitmanager.selectedtheory
for pname in self.fitmanager.theories[theory].parameters:
self.fitmanager.parameter_names.append(pname + "1")
- self.fitmanager.fit_results.append({'name': pname + "1",
- 'estimation': 0,
- 'group': 1,
- 'code': 'FREE',
- 'cons1': 0,
- 'cons2': 0,
- 'fitresult': 0.0,
- 'sigma': 0.0,
- 'xmin': None,
- 'xmax': None})
-
- self.guiParameters.fillFromFit(
- self.fitmanager.fit_results, view='Fit')
+ self.fitmanager.fit_results.append(
+ {
+ "name": pname + "1",
+ "estimation": 0,
+ "group": 1,
+ "code": "FREE",
+ "cons1": 0,
+ "cons2": 0,
+ "fitresult": 0.0,
+ "sigma": 0.0,
+ "xmin": None,
+ "xmax": None,
+ }
+ )
+
+ self.guiParameters.fillFromFit(self.fitmanager.fit_results, view="Fit")
def fitStatus(self, data):
"""Set *status* and *chisq* in status bar"""
- if 'chisq' in data:
- if data['chisq'] is None:
+ if "chisq" in data:
+ if data["chisq"] is None:
self.guistatus.ChisqLine.setText(" ")
else:
- chisq = data['chisq']
+ chisq = data["chisq"]
self.guistatus.ChisqLine.setText("%6.2f" % chisq)
- if 'status' in data:
- status = data['status']
+ if "status" in data:
+ status = data["status"]
self.guistatus.StatusLine.setText(str(status))
def dismiss(self):
@@ -735,13 +742,29 @@ if __name__ == "__main__":
x = numpy.arange(1500).astype(numpy.float64)
constant_bg = 3.14
- p = [1000, 100., 30.0,
- 500, 300., 25.,
- 1700, 500., 35.,
- 750, 700., 30.0,
- 1234, 900., 29.5,
- 302, 1100., 30.5,
- 75, 1300., 21.]
+ p = [
+ 1000,
+ 100.0,
+ 30.0,
+ 500,
+ 300.0,
+ 25.0,
+ 1700,
+ 500.0,
+ 35.0,
+ 750,
+ 700.0,
+ 30.0,
+ 1234,
+ 900.0,
+ 29.5,
+ 302,
+ 1100.0,
+ 30.5,
+ 75,
+ 1300.0,
+ 21.0,
+ ]
y = functions.sum_gauss(x, *p) + constant_bg
a = qt.QApplication(sys.argv)
diff --git a/src/silx/gui/fit/FitWidgets.py b/src/silx/gui/fit/FitWidgets.py
index 0fcc6b7..b7aef07 100644
--- a/src/silx/gui/fit/FitWidgets.py
+++ b/src/silx/gui/fit/FitWidgets.py
@@ -1,6 +1,5 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (C) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,8 +23,6 @@
"""Collection of widgets used to build
:class:`silx.gui.fit.FitWidget.FitWidget`"""
-from collections import OrderedDict
-
from silx.gui import qt
from silx.gui.fit.Parameters import Parameters
@@ -70,17 +67,17 @@ class FitActionsButtons(qt.QWidget):
self.EstimateButton = qt.QPushButton(self)
self.EstimateButton.setText("Estimate")
layout.addWidget(self.EstimateButton)
- spacer = qt.QSpacerItem(20, 20,
- qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Minimum)
+ spacer = qt.QSpacerItem(
+ 20, 20, qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum
+ )
layout.addItem(spacer)
self.StartFitButton = qt.QPushButton(self)
self.StartFitButton.setText("Start Fit")
layout.addWidget(self.StartFitButton)
- spacer_2 = qt.QSpacerItem(20, 20,
- qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Minimum)
+ spacer_2 = qt.QSpacerItem(
+ 20, 20, qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum
+ )
layout.addItem(spacer_2)
self.DismissButton = qt.QPushButton(self)
@@ -149,6 +146,7 @@ class FitConfigWidget(qt.QWidget):
- open a dialog for modifying advanced parameters through
:attr:`FunConfigureButton`
"""
+
def __init__(self, parent=None):
qt.QWidget.__init__(self, parent)
@@ -164,9 +162,11 @@ class FitConfigWidget(qt.QWidget):
self.FunComBox = qt.QComboBox(self)
self.FunComBox.addItem("Add Function(s)")
- self.FunComBox.setItemData(self.FunComBox.findText("Add Function(s)"),
- "Load fit theories from a file",
- qt.Qt.ToolTipRole)
+ self.FunComBox.setItemData(
+ self.FunComBox.findText("Add Function(s)"),
+ "Load fit theories from a file",
+ qt.Qt.ToolTipRole,
+ )
layout.addWidget(self.FunComBox, 0, 1)
self.BkgLabel = qt.QLabel(self)
@@ -175,28 +175,33 @@ class FitConfigWidget(qt.QWidget):
self.BkgComBox = qt.QComboBox(self)
self.BkgComBox.addItem("Add Background(s)")
- self.BkgComBox.setItemData(self.BkgComBox.findText("Add Background(s)"),
- "Load background theories from a file",
- qt.Qt.ToolTipRole)
+ self.BkgComBox.setItemData(
+ self.BkgComBox.findText("Add Background(s)"),
+ "Load background theories from a file",
+ qt.Qt.ToolTipRole,
+ )
layout.addWidget(self.BkgComBox, 1, 1)
self.FunConfigureButton = qt.QPushButton(self)
self.FunConfigureButton.setText("Configure")
self.FunConfigureButton.setToolTip(
- "Open a configuration dialog for the selected function")
+ "Open a configuration dialog for the selected function"
+ )
layout.addWidget(self.FunConfigureButton, 0, 2)
self.BgConfigureButton = qt.QPushButton(self)
self.BgConfigureButton.setText("Configure")
self.BgConfigureButton.setToolTip(
- "Open a configuration dialog for the selected background")
+ "Open a configuration dialog for the selected background"
+ )
layout.addWidget(self.BgConfigureButton, 1, 2)
self.WeightCheckBox = qt.QCheckBox(self)
self.WeightCheckBox.setText("Weighted fit")
self.WeightCheckBox.setToolTip(
- "Enable usage of weights in the least-square problem.\n Use" +
- " the uncertainties (sigma) if provided, else use sqrt(y).")
+ "Enable usage of weights in the least-square problem.\n Use"
+ + " the uncertainties (sigma) if provided, else use sqrt(y)."
+ )
layout.addWidget(self.WeightCheckBox, 0, 3, 2, 1)
@@ -282,7 +287,7 @@ class ParametersTab(qt.QTabWidget):
self.setWindowTitle(name)
self.setContentsMargins(0, 0, 0, 0)
- self.views = OrderedDict()
+ self.views = {}
"""Dictionary of views. Keys are view names,
items are :class:`Parameters` widgets"""
@@ -311,8 +316,8 @@ class ParametersTab(qt.QTabWidget):
view = self.latest_view
else:
raise KeyError(
- "No view available. You must specify a view" +
- " name the first time you call this method."
+ "No view available. You must specify a view"
+ + " name the first time you call this method."
)
if view in self.tables.keys():
@@ -404,7 +409,7 @@ class ParametersTab(qt.QTabWidget):
text += "<tr>"
ncols = table.columnCount()
for l in range(ncols):
- text += ('<td align="left" bgcolor="%s"><b>' % hcolor)
+ text += '<td align="left" bgcolor="%s"><b>' % hcolor
text += str(table.horizontalHeaderItem(l).text())
text += "</b></td>"
text += "</tr>"
@@ -438,11 +443,9 @@ class ParametersTab(qt.QTabWidget):
else:
finalcolor = "white"
if c < 2:
- text += ('<td align="left" bgcolor="%s">%s' %
- (finalcolor, b))
+ text += '<td align="left" bgcolor="%s">%s' % (finalcolor, b)
else:
- text += ('<td align="right" bgcolor="%s">%s' %
- (finalcolor, b))
+ text += '<td align="right" bgcolor="%s">%s' % (finalcolor, b)
text += newtext
if len(b):
text += "</td>"
@@ -506,14 +509,18 @@ def test():
fit = fitmanager.FitManager(x=x, y=y1)
fitfuns = fittheories.FitTheories()
- fit.addtheory(name="Gaussian",
- function=functions.sum_gauss,
- parameters=("height", "peak center", "fwhm"),
- estimate=fitfuns.estimate_height_position_fwhm)
- fit.settheory('Gaussian')
- fit.configure(PositiveFwhmFlag=True,
- PositiveHeightAreaFlag=True,
- AutoFwhm=True,)
+ fit.addtheory(
+ name="Gaussian",
+ function=functions.sum_gauss,
+ parameters=("height", "peak center", "fwhm"),
+ estimate=fitfuns.estimate_height_position_fwhm,
+ )
+ fit.settheory("Gaussian")
+ fit.configure(
+ PositiveFwhmFlag=True,
+ PositiveHeightAreaFlag=True,
+ AutoFwhm=True,
+ )
# Fit
fit.estimate()
@@ -521,26 +528,27 @@ def test():
w = ParametersTab()
w.show()
- w.fillFromFit(fit.fit_results, view='Gaussians')
+ w.fillFromFit(fit.fit_results, view="Gaussians")
- y2 = functions.sum_splitgauss(x,
- 100, 400, 100, 40,
- 10, 600, 50, 500,
- 80, 850, 10, 50)
+ y2 = functions.sum_splitgauss(
+ x, 100, 400, 100, 40, 10, 600, 50, 500, 80, 850, 10, 50
+ )
fit.setdata(x=x, y=y2)
# Define new theory
- fit.addtheory(name="Asymetric gaussian",
- function=functions.sum_splitgauss,
- parameters=("height", "peak center", "left fwhm", "right fwhm"),
- estimate=fitfuns.estimate_splitgauss)
- fit.settheory('Asymetric gaussian')
+ fit.addtheory(
+ name="Asymetric gaussian",
+ function=functions.sum_splitgauss,
+ parameters=("height", "peak center", "left fwhm", "right fwhm"),
+ estimate=fitfuns.estimate_splitgauss,
+ )
+ fit.settheory("Asymetric gaussian")
# Fit
fit.estimate()
fit.runfit()
- w.fillFromFit(fit.fit_results, view='Asymetric gaussians')
+ w.fillFromFit(fit.fit_results, view="Asymetric gaussians")
# Plot
pw = PlotWindow(control=True)
diff --git a/src/silx/gui/fit/Parameters.py b/src/silx/gui/fit/Parameters.py
index daa72f3..bd2605e 100644
--- a/src/silx/gui/fit/Parameters.py
+++ b/src/silx/gui/fit/Parameters.py
@@ -1,6 +1,5 @@
-# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (C) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,7 +27,6 @@ __license__ = "MIT"
__date__ = "25/11/2016"
import sys
-from collections import OrderedDict
from silx.gui import qt
from silx.gui.widgets.TableWidget import TableWidget
@@ -56,6 +54,7 @@ class QComboTableItem(qt.QComboBox):
:param row: Row number of the table cell containing this widget
:param col: Column number of the table cell containing this widget"""
+
sigCellChanged = qt.Signal(int, int)
"""Signal emitted when this ``QComboBox`` is activated.
A ``(row, column)`` tuple is passed."""
@@ -79,6 +78,7 @@ class QCheckBoxItem(qt.QCheckBox):
:param row: Row number of the table cell containing this widget
:param col: Column number of the table cell containing this widget"""
+
sigCellChanged = qt.Signal(int, int)
"""Signal emitted when this ``QCheckBox`` is clicked.
A ``(row, column)`` tuple is passed."""
@@ -107,22 +107,39 @@ class Parameters(TableWidget):
peak.
:type paramlist: list[str] or None
"""
+
def __init__(self, parent=None, paramlist=None):
TableWidget.__init__(self, parent)
self.setContentsMargins(0, 0, 0, 0)
- labels = ['Parameter', 'Estimation', 'Fit Value', 'Sigma',
- 'Constraints', 'Min/Parame', 'Max/Factor/Delta']
- tooltips = ["Fit parameter name",
- "Estimated value for fit parameter. You can edit this column.",
- "Actual value for parameter, after fit",
- "Uncertainty (same unit as the parameter)",
- "Constraint to be applied to the parameter for fit",
- "First parameter for constraint (name of another param or min value)",
- "Second parameter for constraint (max value, or factor/delta)"]
-
- self.columnKeys = ['name', 'estimation', 'fitresult',
- 'sigma', 'code', 'val1', 'val2']
+ labels = [
+ "Parameter",
+ "Estimation",
+ "Fit Value",
+ "Sigma",
+ "Constraints",
+ "Min/Parame",
+ "Max/Factor/Delta",
+ ]
+ tooltips = [
+ "Fit parameter name",
+ "Estimated value for fit parameter. You can edit this column.",
+ "Actual value for parameter, after fit",
+ "Uncertainty (same unit as the parameter)",
+ "Constraint to be applied to the parameter for fit",
+ "First parameter for constraint (name of another param or min value)",
+ "Second parameter for constraint (max value, or factor/delta)",
+ ]
+
+ self.columnKeys = [
+ "name",
+ "estimation",
+ "fitresult",
+ "sigma",
+ "code",
+ "val1",
+ "val2",
+ ]
"""This list assigns shorter keys to refer to columns than the
displayed labels."""
@@ -134,8 +151,7 @@ class Parameters(TableWidget):
for i, label in enumerate(labels):
item = self.horizontalHeaderItem(i)
if item is None:
- item = qt.QTableWidgetItem(label,
- qt.QTableWidgetItem.Type)
+ item = qt.QTableWidgetItem(label, qt.QTableWidgetItem.Type)
self.setHorizontalHeaderItem(i, item)
item.setText(label)
@@ -149,7 +165,7 @@ class Parameters(TableWidget):
# Initialize the table with one line per supplied parameter
paramlist = paramlist if paramlist is not None else []
- self.parameters = OrderedDict()
+ self.parameters = {}
"""This attribute stores all the data in an ordered dictionary.
New data can be added using :meth:`newParameterLine`.
Existing data can be modified using :meth:`configureLine`
@@ -185,8 +201,17 @@ class Parameters(TableWidget):
for line, param in enumerate(paramlist):
self.newParameterLine(param, line)
- self.code_options = ["FREE", "POSITIVE", "QUOTED", "FIXED",
- "FACTOR", "DELTA", "SUM", "IGNORE", "ADD"]
+ self.code_options = [
+ "FREE",
+ "POSITIVE",
+ "QUOTED",
+ "FIXED",
+ "FACTOR",
+ "DELTA",
+ "SUM",
+ "IGNORE",
+ "ADD",
+ ]
"""Possible values in the combo boxes in the 'Constraints' column.
"""
@@ -211,43 +236,46 @@ class Parameters(TableWidget):
self.setRowCount(line + 1)
# default configuration for fit parameters
- self.parameters[param] = OrderedDict((('line', line),
- ('estimation', '0'),
- ('fitresult', ''),
- ('sigma', ''),
- ('code', 'FREE'),
- ('val1', ''),
- ('val2', ''),
- ('cons1', 0),
- ('cons2', 0),
- ('vmin', '0'),
- ('vmax', '1'),
- ('relatedto', ''),
- ('factor', '1.0'),
- ('delta', '0.0'),
- ('sum', '0.0'),
- ('group', ''),
- ('name', param),
- ('xmin', None),
- ('xmax', None)))
- self.setReadWrite(param, 'estimation')
- self.setReadOnly(param, ['name', 'fitresult', 'sigma', 'val1', 'val2'])
+ self.parameters[param] = dict(
+ (
+ ("line", line),
+ ("estimation", "0"),
+ ("fitresult", ""),
+ ("sigma", ""),
+ ("code", "FREE"),
+ ("val1", ""),
+ ("val2", ""),
+ ("cons1", 0),
+ ("cons2", 0),
+ ("vmin", "0"),
+ ("vmax", "1"),
+ ("relatedto", ""),
+ ("factor", "1.0"),
+ ("delta", "0.0"),
+ ("sum", "0.0"),
+ ("group", ""),
+ ("name", param),
+ ("xmin", None),
+ ("xmax", None),
+ )
+ )
+ self.setReadWrite(param, "estimation")
+ self.setReadOnly(param, ["name", "fitresult", "sigma", "val1", "val2"])
# Constraint codes
a = []
for option in self.code_options:
a.append(option)
- code_column_index = self.columnIndexByField('code')
+ code_column_index = self.columnIndexByField("code")
cellWidget = self.cellWidget(line, code_column_index)
if cellWidget is None:
- cellWidget = QComboTableItem(self, row=line,
- col=code_column_index)
+ cellWidget = QComboTableItem(self, row=line, col=code_column_index)
cellWidget.addItems(a)
self.setCellWidget(line, code_column_index, cellWidget)
cellWidget.sigCellChanged[int, int].connect(self.onCellChanged)
- self.parameters[param]['code_item'] = cellWidget
- self.parameters[param]['relatedto_item'] = None
+ self.parameters[param]["code_item"] = cellWidget
+ self.parameters[param]["relatedto_item"] = None
self.__configuring = False
def columnIndexByField(self, field):
@@ -269,44 +297,48 @@ class Parameters(TableWidget):
self.setRowCount(len(fitresults))
# Reinitialize and fill self.parameters
- self.parameters = OrderedDict()
- for (line, param) in enumerate(fitresults):
- self.newParameterLine(param['name'], line)
+ self.parameters = {}
+ for line, param in enumerate(fitresults):
+ self.newParameterLine(param["name"], line)
for param in fitresults:
- name = param['name']
- code = str(param['code'])
+ name = param["name"]
+ code = str(param["code"])
if code not in self.code_options:
# convert code from int to descriptive string
code = self.code_options[int(code)]
- val1 = param['cons1']
- val2 = param['cons2']
- estimation = param['estimation']
- group = param['group']
- sigma = param['sigma']
- fitresult = param['fitresult']
-
- xmin = param.get('xmin')
- xmax = param.get('xmax')
-
- self.configureLine(name=name,
- code=code,
- val1=val1, val2=val2,
- estimation=estimation,
- fitresult=fitresult,
- sigma=sigma,
- group=group,
- xmin=xmin, xmax=xmax)
+ val1 = param["cons1"]
+ val2 = param["cons2"]
+ estimation = param["estimation"]
+ group = param["group"]
+ sigma = param["sigma"]
+ fitresult = param["fitresult"]
+
+ xmin = param.get("xmin")
+ xmax = param.get("xmax")
+
+ self.configureLine(
+ name=name,
+ code=code,
+ val1=val1,
+ val2=val2,
+ estimation=estimation,
+ fitresult=fitresult,
+ sigma=sigma,
+ group=group,
+ xmin=xmin,
+ xmax=xmax,
+ )
def getConfiguration(self):
"""Return ``FitManager.paramlist`` dictionary
encapsulated in another dictionary"""
- return {'parameters': self.getFitResults()}
+ return {"parameters": self.getFitResults()}
def setConfiguration(self, ddict):
"""Fill table with values from a ``FitManager.paramlist`` dictionary
encapsulated in another dictionary"""
- self.fillFromFit(ddict['parameters'])
+ self.fillFromFit(ddict["parameters"])
def getFitResults(self):
"""Return fit parameters as a list of dictionaries in the format used
@@ -317,33 +349,33 @@ class Parameters(TableWidget):
fitparam = {}
name = param
estimation, [code, cons1, cons2] = self.getEstimationConstraints(name)
- buf = str(self.parameters[param]['fitresult'])
- xmin = self.parameters[param]['xmin']
- xmax = self.parameters[param]['xmax']
+ buf = str(self.parameters[param]["fitresult"])
+ xmin = self.parameters[param]["xmin"]
+ xmax = self.parameters[param]["xmax"]
if len(buf):
fitresult = float(buf)
else:
fitresult = 0.0
- buf = str(self.parameters[param]['sigma'])
+ buf = str(self.parameters[param]["sigma"])
if len(buf):
sigma = float(buf)
else:
sigma = 0.0
- buf = str(self.parameters[param]['group'])
+ buf = str(self.parameters[param]["group"])
if len(buf):
group = float(buf)
else:
group = 0
- fitparam['name'] = name
- fitparam['estimation'] = estimation
- fitparam['fitresult'] = fitresult
- fitparam['sigma'] = sigma
- fitparam['group'] = group
- fitparam['code'] = code
- fitparam['cons1'] = cons1
- fitparam['cons2'] = cons2
- fitparam['xmin'] = xmin
- fitparam['xmax'] = xmax
+ fitparam["name"] = name
+ fitparam["estimation"] = estimation
+ fitparam["fitresult"] = fitresult
+ fitparam["sigma"] = sigma
+ fitparam["group"] = group
+ fitparam["code"] = code
+ fitparam["cons1"] = cons1
+ fitparam["cons2"] = cons2
+ fitparam["xmin"] = xmin
+ fitparam["xmax"] = xmax
fitparameterslist.append(fitparam)
return fitparameterslist
@@ -371,7 +403,7 @@ class Parameters(TableWidget):
if item is not None:
newvalue = item.text()
else:
- newvalue = ''
+ newvalue = ""
else:
# this is the combobox
widget = self.cellWidget(row, col)
@@ -380,12 +412,12 @@ class Parameters(TableWidget):
paramdict = {"name": param, field: newvalue}
self.configureLine(**paramdict)
else:
- if field == 'code':
+ if field == "code":
# New code not valid, try restoring the old one
index = self.code_options.index(oldvalue)
self.__configuring = True
try:
- self.parameters[param]['code_item'].setCurrentIndex(index)
+ self.parameters[param]["code_item"].setCurrentIndex(index)
finally:
self.__configuring = False
else:
@@ -401,10 +433,14 @@ class Parameters(TableWidget):
:param newvalue: New value to be validated
:return: True if new cell value is valid, else False
"""
- if field == 'code':
+ if field == "code":
return self.setCodeValue(param, oldvalue, newvalue)
# FIXME: validate() shouldn't have side effects. Move this bit to configureLine()?
- if field == 'val1' and str(self.parameters[param]['code']) in ['DELTA', 'FACTOR', 'SUM']:
+ if field == "val1" and str(self.parameters[param]["code"]) in [
+ "DELTA",
+ "FACTOR",
+ "SUM",
+ ]:
_, candidates = self.getRelatedCandidates(param)
# We expect val1 to be a fit parameter name
if str(newvalue) in candidates:
@@ -430,52 +466,48 @@ class Parameters(TableWidget):
:return: ``True`` if code was successfully updated
"""
- if str(newvalue) in ['FREE', 'POSITIVE', 'QUOTED', 'FIXED']:
- self.configureLine(name=param,
- code=newvalue)
- if str(oldvalue) == 'IGNORE':
+ if str(newvalue) in ["FREE", "POSITIVE", "QUOTED", "FIXED"]:
+ self.configureLine(name=param, code=newvalue)
+ if str(oldvalue) == "IGNORE":
self.freeRestOfGroup(param)
return True
- elif str(newvalue) in ['FACTOR', 'DELTA', 'SUM']:
+ elif str(newvalue) in ["FACTOR", "DELTA", "SUM"]:
# I should check here that some parameter is set
best, candidates = self.getRelatedCandidates(param)
if len(candidates) == 0:
return False
- self.configureLine(name=param,
- code=newvalue,
- relatedto=best)
- if str(oldvalue) == 'IGNORE':
+ self.configureLine(name=param, code=newvalue, relatedto=best)
+ if str(oldvalue) == "IGNORE":
self.freeRestOfGroup(param)
return True
- elif str(newvalue) == 'IGNORE':
+ elif str(newvalue) == "IGNORE":
# I should check if the group can be ignored
# for the time being I just fix all of them to ignore
- group = int(float(str(self.parameters[param]['group'])))
+ group = int(float(str(self.parameters[param]["group"])))
candidates = []
for param in self.parameters.keys():
- if group == int(float(str(self.parameters[param]['group']))):
+ if group == int(float(str(self.parameters[param]["group"]))):
candidates.append(param)
# print candidates
# I should check here if there is any relation to them
for param in candidates:
- self.configureLine(name=param,
- code=newvalue)
+ self.configureLine(name=param, code=newvalue)
return True
- elif str(newvalue) == 'ADD':
- group = int(float(str(self.parameters[param]['group'])))
+ elif str(newvalue) == "ADD":
+ group = int(float(str(self.parameters[param]["group"])))
if group == 0:
# One cannot add a background group
return False
i = 0
for param in self.parameters:
- if i <= int(float(str(self.parameters[param]['group']))):
+ if i <= int(float(str(self.parameters[param]["group"]))):
i += 1
- if (group == 0) and (i == 1): # FIXME: why +1?
+ if (group == 0) and (i == 1): # FIXME: why +1?
i += 1
self.addGroup(i, group)
return False
- elif str(newvalue) == 'SHOW':
+ elif str(newvalue) == "SHOW":
print(self.getEstimationConstraints(param))
return False
@@ -493,14 +525,14 @@ class Parameters(TableWidget):
newparam = []
# loop through parameters until we encounter group number `gtype`
for param in list(self.parameters):
- paramgroup = int(float(str(self.parameters[param]['group'])))
+ paramgroup = int(float(str(self.parameters[param]["group"])))
# copy parameter names in group number `gtype`
if paramgroup == gtype:
# but replace `gtype` with `newg`
newparam.append(param.rstrip("0123456789") + "%d" % newg)
- xmin = self.parameters[param]['xmin']
- xmax = self.parameters[param]['xmax']
+ xmin = self.parameters[param]["xmin"]
+ xmax = self.parameters[param]["xmax"]
# Add new parameters (one table line per parameter) and configureLine each
# one by updating xmin and xmax to the same values as group `gtype`
@@ -520,16 +552,14 @@ class Parameters(TableWidget):
:param workparam: Fit parameter name
"""
if workparam in self.parameters.keys():
- group = int(float(str(self.parameters[workparam]['group'])))
+ group = int(float(str(self.parameters[workparam]["group"])))
for param in self.parameters:
- if param != workparam and\
- group == int(float(str(self.parameters[param]['group']))):
- self.configureLine(name=param,
- code='FREE',
- cons1=0,
- cons2=0,
- val1='',
- val2='')
+ if param != workparam and group == int(
+ float(str(self.parameters[param]["group"]))
+ ):
+ self.configureLine(
+ name=param, code="FREE", cons1=0, cons2=0, val1="", val2=""
+ )
def getRelatedCandidates(self, workparam):
"""If fit parameter ``workparam`` has a constraint that involves other
@@ -544,12 +574,16 @@ class Parameters(TableWidget):
for param_name in self.parameters:
if param_name != workparam:
# ignore parameters that are fixed by a constraint
- if str(self.parameters[param_name]['code']) not in\
- ['IGNORE', 'FACTOR', 'DELTA', 'SUM']:
+ if str(self.parameters[param_name]["code"]) not in [
+ "IGNORE",
+ "FACTOR",
+ "DELTA",
+ "SUM",
+ ]:
candidates.append(param_name)
# take the previous one (before code cell changed) if possible
- if str(self.parameters[workparam]['relatedto']) in candidates:
- best = str(self.parameters[workparam]['relatedto'])
+ if str(self.parameters[workparam]["relatedto"]) in candidates:
+ best = str(self.parameters[workparam]["relatedto"])
return best, candidates
# take the first with same base name (after removing numbers)
for param_name in candidates:
@@ -585,9 +619,7 @@ class Parameters(TableWidget):
:param fields: Field names identifying the columns
:type fields: str or list[str]
"""
- editflags = qt.Qt.ItemIsSelectable |\
- qt.Qt.ItemIsEnabled |\
- qt.Qt.ItemIsEditable
+ editflags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled | qt.Qt.ItemIsEditable
self.setField(parameter, fields, editflags)
def setField(self, parameter, fields, edit_flags):
@@ -602,13 +634,11 @@ class Parameters(TableWidget):
qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled |
qt.Qt.ItemIsEditable
"""
- if isinstance(parameter, list) or \
- isinstance(parameter, tuple):
+ if isinstance(parameter, list) or isinstance(parameter, tuple):
paramlist = parameter
else:
paramlist = [parameter]
- if isinstance(fields, list) or \
- isinstance(fields, tuple):
+ if isinstance(fields, list) or isinstance(fields, tuple):
fieldlist = fields
else:
fieldlist = [fields]
@@ -624,7 +654,7 @@ class Parameters(TableWidget):
row = list(self.parameters.keys()).index(param)
for field in fieldlist:
col = self.columnIndexByField(field)
- if field != 'code':
+ if field != "code":
key = field + "_item"
item = self.item(row, col)
if item is None:
@@ -639,10 +669,22 @@ class Parameters(TableWidget):
# Restore previous _configuring flag
self.__configuring = _oldvalue
- def configureLine(self, name, code=None, val1=None, val2=None,
- sigma=None, estimation=None, fitresult=None,
- group=None, xmin=None, xmax=None, relatedto=None,
- cons1=None, cons2=None):
+ def configureLine(
+ self,
+ name,
+ code=None,
+ val1=None,
+ val2=None,
+ sigma=None,
+ estimation=None,
+ fitresult=None,
+ group=None,
+ xmin=None,
+ xmax=None,
+ relatedto=None,
+ cons1=None,
+ cons2=None,
+ ):
"""This function updates values in a line of the table
:param name: Name of the parameter (serves as unique identifier for
@@ -676,73 +718,88 @@ class Parameters(TableWidget):
# update code first, if specified
if code is not None:
code = str(code)
- self.parameters[name]['code'] = code
+ self.parameters[name]["code"] = code
# update combobox
- index = self.parameters[name]['code_item'].findText(code)
- self.parameters[name]['code_item'].setCurrentIndex(index)
+ index = self.parameters[name]["code_item"].findText(code)
+ self.parameters[name]["code_item"].setCurrentIndex(index)
else:
# set code to previous value, used later for setting val1 val2
- code = self.parameters[name]['code']
+ code = self.parameters[name]["code"]
# val1 and sigma have special formats
if val1 is not None:
- fmt = None if self.parameters[name]['code'] in\
- ['DELTA', 'FACTOR', 'SUM'] else "%8g"
+ fmt = (
+ None
+ if self.parameters[name]["code"] in ["DELTA", "FACTOR", "SUM"]
+ else "%8g"
+ )
self._updateField(name, "val1", val1, fmat=fmt)
if sigma is not None:
self._updateField(name, "sigma", sigma, fmat="%6.3g")
# other fields are formatted as "%8g"
- keys_params = (("val2", val2), ("estimation", estimation),
- ("fitresult", fitresult))
+ keys_params = (
+ ("val2", val2),
+ ("estimation", estimation),
+ ("fitresult", fitresult),
+ )
for key, value in keys_params:
if value is not None:
self._updateField(name, key, value, fmat="%8g")
# the rest of the parameters are treated as strings and don't need
# validation
- keys_params = (("group", group), ("xmin", xmin),
- ("xmax", xmax), ("relatedto", relatedto),
- ("cons1", cons1), ("cons2", cons2))
+ keys_params = (
+ ("group", group),
+ ("xmin", xmin),
+ ("xmax", xmax),
+ ("relatedto", relatedto),
+ ("cons1", cons1),
+ ("cons2", cons2),
+ )
for key, value in keys_params:
if value is not None:
self.parameters[name][key] = str(value)
# val1 and val2 have different meanings depending on the code
- if code == 'QUOTED':
+ if code == "QUOTED":
if val1 is not None:
- self.parameters[name]['vmin'] = self.parameters[name]['val1']
+ self.parameters[name]["vmin"] = self.parameters[name]["val1"]
else:
- self.parameters[name]['val1'] = self.parameters[name]['vmin']
+ self.parameters[name]["val1"] = self.parameters[name]["vmin"]
if val2 is not None:
- self.parameters[name]['vmax'] = self.parameters[name]['val2']
+ self.parameters[name]["vmax"] = self.parameters[name]["val2"]
else:
- self.parameters[name]['val2'] = self.parameters[name]['vmax']
+ self.parameters[name]["val2"] = self.parameters[name]["vmax"]
# cons1 and cons2 are scalar representations of val1 and val2
- self.parameters[name]['cons1'] =\
- float_else_zero(self.parameters[name]['val1'])
- self.parameters[name]['cons2'] =\
- float_else_zero(self.parameters[name]['val2'])
+ self.parameters[name]["cons1"] = float_else_zero(
+ self.parameters[name]["val1"]
+ )
+ self.parameters[name]["cons2"] = float_else_zero(
+ self.parameters[name]["val2"]
+ )
# cons1, cons2 = min(val1, val2), max(val1, val2)
- if self.parameters[name]['cons1'] > self.parameters[name]['cons2']:
- self.parameters[name]['cons1'], self.parameters[name]['cons2'] =\
- self.parameters[name]['cons2'], self.parameters[name]['cons1']
+ if self.parameters[name]["cons1"] > self.parameters[name]["cons2"]:
+ self.parameters[name]["cons1"], self.parameters[name]["cons2"] = (
+ self.parameters[name]["cons2"],
+ self.parameters[name]["cons1"],
+ )
- elif code in ['DELTA', 'SUM', 'FACTOR']:
+ elif code in ["DELTA", "SUM", "FACTOR"]:
# For these codes, val1 is the fit parameter name on which the
# constraint depends
if val1 is not None and val1 in paramlist:
- self.parameters[name]['relatedto'] = self.parameters[name]["val1"]
+ self.parameters[name]["relatedto"] = self.parameters[name]["val1"]
elif val1 is not None:
# val1 could be the index of the fit parameter
try:
- self.parameters[name]['relatedto'] = paramlist[int(val1)]
+ self.parameters[name]["relatedto"] = paramlist[int(val1)]
except ValueError:
- self.parameters[name]['relatedto'] = self.parameters[name]["val1"]
+ self.parameters[name]["relatedto"] = self.parameters[name]["val1"]
elif relatedto is not None:
# code changed, val1 not specified but relatedto specified:
@@ -754,25 +811,27 @@ class Parameters(TableWidget):
self.parameters[name][key] = self.parameters[name]["val2"]
# FIXME: val1 is sometimes specified as an index rather than a param name
- self.parameters[name]['val1'] = self.parameters[name]['relatedto']
+ self.parameters[name]["val1"] = self.parameters[name]["relatedto"]
# cons1 is the index of the fit parameter in the ordered dictionary
- if self.parameters[name]['val1'] in paramlist:
- self.parameters[name]['cons1'] =\
- paramlist.index(self.parameters[name]['val1'])
+ if self.parameters[name]["val1"] in paramlist:
+ self.parameters[name]["cons1"] = paramlist.index(
+ self.parameters[name]["val1"]
+ )
# cons2 is the constraint value (factor, delta or sum)
try:
- self.parameters[name]['cons2'] =\
- float(str(self.parameters[name]['val2']))
+ self.parameters[name]["cons2"] = float(
+ str(self.parameters[name]["val2"])
+ )
except ValueError:
- self.parameters[name]['cons2'] = 1.0 if code == "FACTOR" else 0.0
+ self.parameters[name]["cons2"] = 1.0 if code == "FACTOR" else 0.0
- elif code in ['FREE', 'POSITIVE', 'IGNORE', 'FIXED']:
- self.parameters[name]['val1'] = ""
- self.parameters[name]['val2'] = ""
- self.parameters[name]['cons1'] = 0
- self.parameters[name]['cons2'] = 0
+ elif code in ["FREE", "POSITIVE", "IGNORE", "FIXED"]:
+ self.parameters[name]["val1"] = ""
+ self.parameters[name]["val2"] = ""
+ self.parameters[name]["cons1"] = 0
+ self.parameters[name]["cons2"] = 0
self._updateCellRWFlags(name, code)
@@ -794,9 +853,9 @@ class Parameters(TableWidget):
newvalue = fmat % float(value) if value != "" else ""
else:
newvalue = value
- self.parameters[name][field] = newvalue if\
- self.validate(name, field, oldvalue, newvalue) else\
- oldvalue
+ self.parameters[name][field] = (
+ newvalue if self.validate(name, field, oldvalue, newvalue) else oldvalue
+ )
def _updateCellRWFlags(self, name, code=None):
"""Set read-only or read-write flags in a row,
@@ -807,12 +866,12 @@ class Parameters(TableWidget):
`'FIXED', 'FACTOR', 'DELTA', 'SUM', 'ADD'`
:return:
"""
- if code in ['FREE', 'POSITIVE', 'IGNORE', 'FIXED']:
- self.setReadWrite(name, 'estimation')
- self.setReadOnly(name, ['fitresult', 'sigma', 'val1', 'val2'])
+ if code in ["FREE", "POSITIVE", "IGNORE", "FIXED"]:
+ self.setReadWrite(name, "estimation")
+ self.setReadOnly(name, ["fitresult", "sigma", "val1", "val2"])
else:
- self.setReadWrite(name, ['estimation', 'val1', 'val2'])
- self.setReadOnly(name, ['fitresult', 'sigma'])
+ self.setReadWrite(name, ["estimation", "val1", "val2"])
+ self.setReadOnly(name, ["fitresult", "sigma"])
def getEstimationConstraints(self, param):
"""
@@ -823,18 +882,17 @@ class Parameters(TableWidget):
estimation = None
constraints = None
if param in self.parameters.keys():
- buf = str(self.parameters[param]['estimation'])
+ buf = str(self.parameters[param]["estimation"])
if len(buf):
estimation = float(buf)
else:
estimation = 0
- if str(self.parameters[param]['code']) in self.code_options:
- code = self.code_options.index(
- str(self.parameters[param]['code']))
+ if str(self.parameters[param]["code"]) in self.code_options:
+ code = self.code_options.index(str(self.parameters[param]["code"]))
else:
- code = str(self.parameters[param]['code'])
- cons1 = self.parameters[param]['cons1']
- cons2 = self.parameters[param]['cons2']
+ code = str(self.parameters[param]["code"])
+ cons1 = self.parameters[param]["cons1"]
+ cons2 = self.parameters[param]["cons2"]
constraints = [code, cons1, cons2]
return estimation, constraints
@@ -842,21 +900,24 @@ class Parameters(TableWidget):
def main(args):
from silx.math.fit import fittheories
from silx.math.fit import fitmanager
+
try:
from PyMca5 import PyMcaDataDir
except ImportError:
raise ImportError("This demo requires PyMca data. Install PyMca5.")
import numpy
import os
+
app = qt.QApplication(args)
- tab = Parameters(paramlist=['Height', 'Position', 'FWHM'])
+ tab = Parameters(paramlist=["Height", "Position", "FWHM"])
tab.showGrid()
- tab.configureLine(name='Height', estimation='1234', group=0)
- tab.configureLine(name='Position', code='FIXED', group=1)
- tab.configureLine(name='FWHM', group=1)
+ tab.configureLine(name="Height", estimation="1234", group=0)
+ tab.configureLine(name="Position", code="FIXED", group=1)
+ tab.configureLine(name="FWHM", group=1)
- y = numpy.loadtxt(os.path.join(PyMcaDataDir.PYMCA_DATA_DIR,
- "XRFSpectrum.mca")) # FIXME
+ y = numpy.loadtxt(
+ os.path.join(PyMcaDataDir.PYMCA_DATA_DIR, "XRFSpectrum.mca")
+ ) # FIXME
x = numpy.arange(len(y)) * 0.0502883 - 0.492773
fit = fitmanager.FitManager()
@@ -864,19 +925,22 @@ def main(args):
fit.loadtheories(fittheories)
- fit.settheory('ahypermet')
- fit.configure(Yscaling=1.,
- PositiveFwhmFlag=True,
- PositiveHeightAreaFlag=True,
- FwhmPoints=16,
- QuotedPositionFlag=1,
- HypermetTails=1)
- fit.setbackground('Linear')
+ fit.settheory("ahypermet")
+ fit.configure(
+ Yscaling=1.0,
+ PositiveFwhmFlag=True,
+ PositiveHeightAreaFlag=True,
+ FwhmPoints=16,
+ QuotedPositionFlag=1,
+ HypermetTails=1,
+ )
+ fit.setbackground("Linear")
fit.estimate()
fit.runfit()
tab.fillFromFit(fit.fit_results)
tab.show()
app.exec()
+
if __name__ == "__main__":
main(sys.argv)
diff --git a/src/silx/gui/fit/__init__.py b/src/silx/gui/fit/__init__.py
index e4fd3ab..478ea22 100644
--- a/src/silx/gui/fit/__init__.py
+++ b/src/silx/gui/fit/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
# Copyright (C) 2016 European Synchrotron Radiation Facility
#
diff --git a/src/silx/gui/fit/test/__init__.py b/src/silx/gui/fit/test/__init__.py
index 71128fb..b03339f 100644
--- a/src/silx/gui/fit/test/__init__.py
+++ b/src/silx/gui/fit/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/fit/test/testBackgroundWidget.py b/src/silx/gui/fit/test/testBackgroundWidget.py
index b8570f7..73e3fba 100644
--- a/src/silx/gui/fit/test/testBackgroundWidget.py
+++ b/src/silx/gui/fit/test/testBackgroundWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -22,8 +21,6 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-import unittest
-
from silx.gui.utils.testutils import TestCaseQt
from .. import BackgroundWidget
@@ -37,8 +34,7 @@ class TestBackgroundWidget(TestCaseQt):
def setUp(self):
super(TestBackgroundWidget, self).setUp()
self.bgdialog = BackgroundWidget.BackgroundDialog()
- self.bgdialog.setData(list([0, 1, 2, 3]),
- list([0, 1, 4, 8]))
+ self.bgdialog.setData(list([0, 1, 2, 3]), list([0, 1, 4, 8]))
self.qWaitForWindowExposed(self.bgdialog)
def tearDown(self):
@@ -61,9 +57,17 @@ class TestBackgroundWidget(TestCaseQt):
self.bgdialog.accept()
output = self.bgdialog.output
- for key in ["algorithm", "StripThreshold", "SnipWidth",
- "StripIterations", "StripWidth", "SmoothingFlag",
- "SmoothingWidth", "AnchorsFlag", "AnchorsList"]:
+ for key in [
+ "algorithm",
+ "StripThreshold",
+ "SnipWidth",
+ "StripIterations",
+ "StripWidth",
+ "SmoothingFlag",
+ "SmoothingWidth",
+ "AnchorsFlag",
+ "AnchorsList",
+ ]:
self.assertIn(key, output)
self.assertFalse(output["AnchorsFlag"])
diff --git a/src/silx/gui/fit/test/testFitConfig.py b/src/silx/gui/fit/test/testFitConfig.py
index 53da2dd..d59562c 100644
--- a/src/silx/gui/fit/test/testFitConfig.py
+++ b/src/silx/gui/fit/test/testFitConfig.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ __authors__ = ["P. Knobel"]
__license__ = "MIT"
__date__ = "05/12/2016"
-import unittest
-
from silx.gui.utils.testutils import TestCaseQt
from .. import FitConfig
@@ -62,22 +59,24 @@ class TestFitConfig(TestCaseQt):
self.fit_config.accept()
output = self.fit_config.output
- for key in ["AutoFwhm",
- "PositiveHeightAreaFlag",
- "QuotedPositionFlag",
- "PositiveFwhmFlag",
- "SameFwhmFlag",
- "QuotedEtaFlag",
- "NoConstraintsFlag",
- "FwhmPoints",
- "Sensitivity",
- "Yscaling",
- "ForcePeakPresence",
- "StripBackgroundFlag",
- "StripWidth",
- "StripIterations",
- "StripThreshold",
- "SmoothingFlag"]:
+ for key in [
+ "AutoFwhm",
+ "PositiveHeightAreaFlag",
+ "QuotedPositionFlag",
+ "PositiveFwhmFlag",
+ "SameFwhmFlag",
+ "QuotedEtaFlag",
+ "NoConstraintsFlag",
+ "FwhmPoints",
+ "Sensitivity",
+ "Yscaling",
+ "ForcePeakPresence",
+ "StripBackgroundFlag",
+ "StripWidth",
+ "StripIterations",
+ "StripThreshold",
+ "SmoothingFlag",
+ ]:
self.assertIn(key, output)
self.assertTrue(output["AutoFwhm"])
diff --git a/src/silx/gui/fit/test/testFitWidget.py b/src/silx/gui/fit/test/testFitWidget.py
index abe9d89..e59fa92 100644
--- a/src/silx/gui/fit/test/testFitWidget.py
+++ b/src/silx/gui/fit/test/testFitWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""Basic tests for :class:`FitWidget`"""
-import unittest
-
from silx.gui.utils.testutils import TestCaseQt
from ... import qt
@@ -83,13 +80,9 @@ class TestFitWidget(TestCaseQt):
y = [fitfun(x_, 2, 3) for x_ in x]
def conf(**kw):
- return {"spam": "eggs",
- "hello": "world!"}
+ return {"spam": "eggs", "hello": "world!"}
- theory = FitTheory(
- function=fitfun,
- parameters=["a", "b"],
- configure=conf)
+ theory = FitTheory(function=fitfun, parameters=["a", "b"], configure=conf)
fitmngr = FitManager()
fitmngr.setdata(x, y)
@@ -98,8 +91,9 @@ class TestFitWidget(TestCaseQt):
fitmngr.addbgtheory("spam", theory)
fw = FitWidget(fitmngr=fitmngr)
- fw.associateConfigDialog("spam", CustomConfigWidget(),
- theory_is_background=True)
+ fw.associateConfigDialog(
+ "spam", CustomConfigWidget(), theory_is_background=True
+ )
fw.associateConfigDialog("foo", CustomConfigWidget())
fw.show()
self.qWaitForWindowExposed(fw)
@@ -107,8 +101,7 @@ class TestFitWidget(TestCaseQt):
fw.bgconfigdialogs["spam"].accept()
self.assertTrue(fw.bgconfigdialogs["spam"].result())
- self.assertEqual(fw.bgconfigdialogs["spam"].output,
- {"hello": "world"})
+ self.assertEqual(fw.bgconfigdialogs["spam"].output, {"hello": "world"})
fw.bgconfigdialogs["spam"].reject()
self.assertFalse(fw.bgconfigdialogs["spam"].result())
diff --git a/src/silx/gui/hdf5/Hdf5Formatter.py b/src/silx/gui/hdf5/Hdf5Formatter.py
index 6c3de41..99e0bb6 100644
--- a/src/silx/gui/hdf5/Hdf5Formatter.py
+++ b/src/silx/gui/hdf5/Hdf5Formatter.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -38,8 +37,7 @@ import h5py
class Hdf5Formatter(qt.QObject):
- """Formatter to convert HDF5 data to string.
- """
+ """Formatter to convert HDF5 data to string."""
formatChanged = qt.Signal()
"""Emitted when properties of the formatter change."""
@@ -88,7 +86,7 @@ class Hdf5Formatter(qt.QObject):
if dataset.shape == tuple():
return "scalar"
shape = [str(i) for i in dataset.shape]
- text = u" \u00D7 ".join(shape)
+ text = " \u00D7 ".join(shape)
return text
def humanReadableValue(self, dataset):
@@ -163,7 +161,7 @@ class Hdf5Formatter(qt.QObject):
if enumType is not None:
return "enum"
- text = str(dtype.newbyteorder('N'))
+ text = str(dtype.newbyteorder("N"))
if numpy.issubdtype(dtype, numpy.floating):
if hasattr(numpy, "float128") and dtype == numpy.float128:
text = "float80"
@@ -182,7 +180,7 @@ class Hdf5Formatter(qt.QObject):
elif dtype.byteorder == "=":
text = "Native " + text
- dtype = dtype.newbyteorder('N')
+ dtype = dtype.newbyteorder("N")
return text
def humanReadableHdf5Type(self, dataset):
diff --git a/src/silx/gui/hdf5/Hdf5HeaderView.py b/src/silx/gui/hdf5/Hdf5HeaderView.py
index 7255ce0..16323dd 100644
--- a/src/silx/gui/hdf5/Hdf5HeaderView.py
+++ b/src/silx/gui/hdf5/Hdf5HeaderView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -73,21 +72,49 @@ class Hdf5HeaderView(qt.QHeaderView):
def __updateAutoResize(self):
"""Update the view according to the state of the auto-resize"""
if self.__auto_resize:
- self.setSectionResizeMode(Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.ResizeToContents)
- self.setSectionResizeMode(Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.ResizeToContents)
- self.setSectionResizeMode(Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.ResizeToContents)
- self.setSectionResizeMode(Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive)
- self.setSectionResizeMode(Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive)
- self.setSectionResizeMode(Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.ResizeToContents)
- self.setSectionResizeMode(Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.ResizeToContents)
+ self.setSectionResizeMode(
+ Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.ResizeToContents
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.ResizeToContents
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.ResizeToContents
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.ResizeToContents
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.ResizeToContents
+ )
else:
- self.setSectionResizeMode(Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.Interactive)
- self.setSectionResizeMode(Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.Interactive)
- self.setSectionResizeMode(Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.Interactive)
- self.setSectionResizeMode(Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive)
- self.setSectionResizeMode(Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive)
- self.setSectionResizeMode(Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.Interactive)
- self.setSectionResizeMode(Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.Interactive)
+ self.setSectionResizeMode(
+ Hdf5TreeModel.NAME_COLUMN, qt.QHeaderView.Interactive
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.TYPE_COLUMN, qt.QHeaderView.Interactive
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.SHAPE_COLUMN, qt.QHeaderView.Interactive
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.VALUE_COLUMN, qt.QHeaderView.Interactive
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.DESCRIPTION_COLUMN, qt.QHeaderView.Interactive
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.NODE_COLUMN, qt.QHeaderView.Interactive
+ )
+ self.setSectionResizeMode(
+ Hdf5TreeModel.LINK_COLUMN, qt.QHeaderView.Interactive
+ )
def setAutoResizeColumns(self, autoResize):
"""Enable/disable auto-resize. When auto-resized, the header take care
@@ -126,7 +153,9 @@ class Hdf5HeaderView(qt.QHeaderView):
"""
return self.__hide_columns_popup
- enableHideColumnsPopup = qt.Property(bool, hasHideColumnsPopup, setAutoResizeColumns)
+ enableHideColumnsPopup = qt.Property(
+ bool, hasHideColumnsPopup, setAutoResizeColumns
+ )
"""Property to enable/disable popup allowing to hide/show columns."""
def __genHideSectionEvent(self, column):
diff --git a/src/silx/gui/hdf5/Hdf5Item.py b/src/silx/gui/hdf5/Hdf5Item.py
index e07f835..2777a94 100755
--- a/src/silx/gui/hdf5/Hdf5Item.py
+++ b/src/silx/gui/hdf5/Hdf5Item.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,8 +28,8 @@ __date__ = "17/01/2019"
import logging
-import collections
import enum
+from typing import Optional
from .. import qt
from .. import icons
@@ -39,6 +38,7 @@ from .Hdf5Node import Hdf5Node
import silx.io.utils
from silx.gui.data.TextFormatter import TextFormatter
from ..hdf5.Hdf5Formatter import Hdf5Formatter
+
_logger = logging.getLogger(__name__)
_formatter = TextFormatter()
_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter)
@@ -46,8 +46,8 @@ _hdf5Formatter = Hdf5Formatter(textFormatter=_formatter)
class DescriptionType(enum.Enum):
- """List of available kind of description.
- """
+ """List of available kind of description."""
+
ERROR = "error"
DESCRIPTION = "description"
TITLE = "title"
@@ -62,10 +62,21 @@ class Hdf5Item(Hdf5Node):
tree structure.
"""
- def __init__(self, text, obj, parent, key=None, h5Class=None, linkClass=None, populateAll=False):
+ def __init__(
+ self,
+ text: Optional[str],
+ obj,
+ parent,
+ key=None,
+ h5Class=None,
+ linkClass=None,
+ populateAll=False,
+ openedPath: Optional[str] = None,
+ ):
"""
- :param str text: text displayed
+ :param text: text displayed
:param object obj: Pointer to a h5py-link object. See the `obj` attribute.
+ :param openedPath: The path with which the item was opened if any
"""
self.__obj = obj
self.__key = key
@@ -76,7 +87,7 @@ class Hdf5Item(Hdf5Node):
self.__linkClass = linkClass
self.__description = None
self.__nx_class = None
- Hdf5Node.__init__(self, parent, populateAll=populateAll)
+ Hdf5Node.__init__(self, parent, populateAll=populateAll, openedPath=openedPath)
def _getCanonicalName(self):
parent = self.parent
@@ -199,9 +210,14 @@ class Hdf5Item(Hdf5Node):
class_ = silx.io.utils.get_h5_class(self.__obj)
if class_ == silx.io.utils.H5Type.EXTERNAL_LINK:
- message = "External link broken. Path %s::%s does not exist" % (self.__obj.filename, self.__obj.path)
+ message = "External link broken. Path %s::%s does not exist" % (
+ self.__obj.filename,
+ self.__obj.path,
+ )
elif class_ == silx.io.utils.H5Type.SOFT_LINK:
- message = "Soft link broken. Path %s does not exist" % (self.__obj.path)
+ message = "Soft link broken. Path %s does not exist" % (
+ self.__obj.path
+ )
else:
name = self.__obj.__class__.__name__.split(".")[-1].capitalize()
message = "%s broken" % (name)
@@ -209,7 +225,10 @@ class Hdf5Item(Hdf5Node):
self.__isBroken = True
else:
self.__obj = obj
- if not self.isGroupObj():
+ if silx.io.utils.get_h5_class(obj) not in [
+ silx.io.utils.H5Type.GROUP,
+ silx.io.utils.H5Type.FILE,
+ ]:
try:
# pre-fetch of the data
if obj.shape is None:
@@ -246,7 +265,10 @@ class Hdf5Item(Hdf5Node):
keys.append(name)
except Exception:
lib_name = self.obj.__class__.__module__.split(".")[0]
- _logger.error("Internal %s error (second time). The file is corrupted.", lib_name)
+ _logger.error(
+ "Internal %s error (second time). The file is corrupted.",
+ lib_name,
+ )
_logger.debug("Backtrace", exc_info=True)
for name in keys:
try:
@@ -270,7 +292,14 @@ class Hdf5Item(Hdf5Node):
h5class = silx.io.utils.get_h5_class(class_=class_)
if h5class is None:
_logger.error("Class %s unsupported", class_)
- item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5Class=h5class, linkClass=link)
+ item = Hdf5Item(
+ text=name,
+ obj=None,
+ parent=self,
+ key=name,
+ h5Class=h5class,
+ linkClass=link,
+ )
self.appendChild(item)
def hasChildren(self):
@@ -319,7 +348,7 @@ class Hdf5Item(Hdf5Node):
:param Dict[str,str] attributeDict: Key/value attributes
"""
- attributeDict = collections.OrderedDict()
+ attributeDict = {}
if self.h5Class == silx.io.utils.H5Type.DATASET:
attributeDict["#Title"] = "HDF5 Dataset"
@@ -327,7 +356,9 @@ class Hdf5Item(Hdf5Node):
attributeDict["Path"] = self.obj.name
attributeDict["Shape"] = self._getFormatter().humanReadableShape(self.obj)
attributeDict["Value"] = self._getFormatter().humanReadableValue(self.obj)
- attributeDict["Data type"] = self._getFormatter().humanReadableType(self.obj, full=True)
+ attributeDict["Data type"] = self._getFormatter().humanReadableType(
+ self.obj, full=True
+ )
elif self.h5Class == silx.io.utils.H5Type.GROUP:
attributeDict["#Title"] = "HDF5 Group"
if self.nexusClassName:
@@ -384,14 +415,18 @@ class Hdf5Item(Hdf5Node):
# Check NX_class formatting
lower = text.lower()
formatedNX_class = ""
- if lower.startswith('nx'):
- formatedNX_class = 'NX' + lower[2:]
- if lower == 'nxcansas':
- formatedNX_class = 'NXcanSAS' # That's the only class with capital letters...
+ if lower.startswith("nx"):
+ formatedNX_class = "NX" + lower[2:]
+ if lower == "nxcansas":
+ formatedNX_class = (
+ "NXcanSAS" # That's the only class with capital letters...
+ )
if text != formatedNX_class:
- _logger.error("NX_class: '%s' is malformed (should be '%s')",
- text,
- formatedNX_class)
+ _logger.error(
+ "NX_class: '%s' is malformed (should be '%s')",
+ text,
+ formatedNX_class,
+ )
text = formatedNX_class
self.__nx_class = text
@@ -458,59 +493,44 @@ class Hdf5Item(Hdf5Node):
return None
_NEXUS_CLASS_TO_VALUE_CHILDREN = {
- 'NXaperture': (
- (DescriptionType.DESCRIPTION, 'description'),
- ),
- 'NXbeam_stop': (
- (DescriptionType.DESCRIPTION, 'description'),
- ),
- 'NXdetector': (
- (DescriptionType.NAME, 'local_name'),
- (DescriptionType.DESCRIPTION, 'description')
- ),
- 'NXentry': (
- (DescriptionType.TITLE, 'title'),
- ),
- 'NXenvironment': (
- (DescriptionType.NAME, 'short_name'),
- (DescriptionType.NAME, 'name'),
- (DescriptionType.DESCRIPTION, 'description')
- ),
- 'NXinstrument': (
- (DescriptionType.NAME, 'name'),
- ),
- 'NXlog': (
- (DescriptionType.DESCRIPTION, 'description'),
- ),
- 'NXmirror': (
- (DescriptionType.DESCRIPTION, 'description'),
- ),
- 'NXpositioner': (
- (DescriptionType.NAME, 'name'),
+ "NXaperture": ((DescriptionType.DESCRIPTION, "description"),),
+ "NXbeam_stop": ((DescriptionType.DESCRIPTION, "description"),),
+ "NXdetector": (
+ (DescriptionType.NAME, "local_name"),
+ (DescriptionType.DESCRIPTION, "description"),
),
- 'NXprocess': (
- (DescriptionType.PROGRAM, 'program'),
+ "NXentry": ((DescriptionType.TITLE, "title"),),
+ "NXenvironment": (
+ (DescriptionType.NAME, "short_name"),
+ (DescriptionType.NAME, "name"),
+ (DescriptionType.DESCRIPTION, "description"),
),
- 'NXsample': (
- (DescriptionType.TITLE, 'short_title'),
- (DescriptionType.NAME, 'name'),
- (DescriptionType.DESCRIPTION, 'description')
+ "NXinstrument": ((DescriptionType.NAME, "name"),),
+ "NXlog": ((DescriptionType.DESCRIPTION, "description"),),
+ "NXmirror": ((DescriptionType.DESCRIPTION, "description"),),
+ "NXnote": ((DescriptionType.DESCRIPTION, "description"),),
+ "NXpositioner": ((DescriptionType.NAME, "name"),),
+ "NXprocess": ((DescriptionType.PROGRAM, "program"),),
+ "NXsample": (
+ (DescriptionType.TITLE, "short_title"),
+ (DescriptionType.NAME, "name"),
+ (DescriptionType.DESCRIPTION, "description"),
),
- 'NXsample_component': (
- (DescriptionType.NAME, 'name'),
- (DescriptionType.DESCRIPTION, 'description')
+ "NXsample_component": (
+ (DescriptionType.NAME, "name"),
+ (DescriptionType.DESCRIPTION, "description"),
),
- 'NXsensor': (
- (DescriptionType.NAME, 'short_name'),
- (DescriptionType.NAME, 'name')
+ "NXsensor": (
+ (DescriptionType.NAME, "short_name"),
+ (DescriptionType.NAME, "name"),
),
- 'NXsource': (
- (DescriptionType.NAME, 'name'),
+ "NXsource": (
+ (DescriptionType.NAME, "name"),
), # or its 'short_name' attribute... This is not supported
- 'NXsubentry': (
- (DescriptionType.DESCRIPTION, 'definition'),
- (DescriptionType.PROGRAM, 'program_name'),
- (DescriptionType.TITLE, 'title'),
+ "NXsubentry": (
+ (DescriptionType.DESCRIPTION, "definition"),
+ (DescriptionType.PROGRAM, "program_name"),
+ (DescriptionType.TITLE, "title"),
),
}
"""Mapping from NeXus class to child names containing data to use as value"""
@@ -525,19 +545,25 @@ class Hdf5Item(Hdf5Node):
return DescriptionType.ERROR, self.__error
if self.h5Class == silx.io.utils.H5Type.DATASET:
- return DescriptionType.VALUE, self._getFormatter().humanReadableValue(self.obj)
+ return DescriptionType.VALUE, self._getFormatter().humanReadableValue(
+ self.obj
+ )
elif self.isGroupObj() and self.nexusClassName:
# For NeXus groups, try to find a title or name
# By default, look for a title (most application definitions should have one)
- defaultSequence = ((DescriptionType.TITLE, 'title'),)
- sequence = self._NEXUS_CLASS_TO_VALUE_CHILDREN.get(self.nexusClassName, defaultSequence)
+ defaultSequence = ((DescriptionType.TITLE, "title"),)
+ sequence = self._NEXUS_CLASS_TO_VALUE_CHILDREN.get(
+ self.nexusClassName, defaultSequence
+ )
for kind, child_name in sequence:
for index in range(self.childCount()):
child = self.child(index)
- if (isinstance(child, Hdf5Item) and
- child.h5Class == silx.io.utils.H5Type.DATASET and
- child.basename == child_name):
+ if (
+ isinstance(child, Hdf5Item)
+ and child.h5Class == silx.io.utils.H5Type.DATASET
+ and child.basename == child_name
+ ):
return kind, self._getFormatter().humanReadableValue(child.obj)
description = self.obj.attrs.get("desc", None)
diff --git a/src/silx/gui/hdf5/Hdf5LoadingItem.py b/src/silx/gui/hdf5/Hdf5LoadingItem.py
index f11d252..70d015c 100644
--- a/src/silx/gui/hdf5/Hdf5LoadingItem.py
+++ b/src/silx/gui/hdf5/Hdf5LoadingItem.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -27,6 +26,7 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "06/07/2018"
+from typing import Optional
from .. import qt
from .Hdf5Node import Hdf5Node
@@ -39,9 +39,15 @@ class Hdf5LoadingItem(Hdf5Node):
At the end of the loading this item is replaced by the loaded one.
"""
- def __init__(self, text, parent, animatedIcon):
+ def __init__(
+ self,
+ text,
+ parent,
+ animatedIcon,
+ openedPath: Optional[str] = None,
+ ):
"""Constructor"""
- Hdf5Node.__init__(self, parent)
+ Hdf5Node.__init__(self, parent, openedPath=openedPath)
self.__text = text
self.__animatedIcon = animatedIcon
self.__animatedIcon.register(self)
diff --git a/src/silx/gui/hdf5/Hdf5Node.py b/src/silx/gui/hdf5/Hdf5Node.py
index be16535..db49594 100644
--- a/src/silx/gui/hdf5/Hdf5Node.py
+++ b/src/silx/gui/hdf5/Hdf5Node.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -28,6 +27,7 @@ __license__ = "MIT"
__date__ = "24/07/2018"
import weakref
+from typing import Optional
class Hdf5Node(object):
@@ -36,16 +36,25 @@ class Hdf5Node(object):
It provides link to the childs and to the parents, and a link to an
external object.
"""
- def __init__(self, parent=None, populateAll=False):
+
+ def __init__(
+ self,
+ parent=None,
+ populateAll=False,
+ openedPath: Optional[str] = None,
+ ):
"""
Constructor
:param Hdf5Node parent: Parent of the node, if exists, else None
:param bool populateAll: If true, populate all the tree node. Else
everything is lazy loaded.
+ :param openedPath:
+ The url or filename the node was created from, None if not directly created
"""
self.__child = None
self.__parent = None
+ self.__openedPath = openedPath
if parent is not None:
self.__parent = weakref.ref(parent)
if populateAll:
@@ -60,6 +69,11 @@ class Hdf5Node(object):
return "%s/?" % (parent._getCanonicalName())
@property
+ def _openedPath(self) -> Optional[str]:
+ """url or filename the node was created from, None if not directly created"""
+ return self.__openedPath
+
+ @property
def parent(self):
"""Parent of the node, or None if the node is a root
diff --git a/src/silx/gui/hdf5/Hdf5TreeModel.py b/src/silx/gui/hdf5/Hdf5TreeModel.py
index a32f7cf..3353ab3 100644
--- a/src/silx/gui/hdf5/Hdf5TreeModel.py
+++ b/src/silx/gui/hdf5/Hdf5TreeModel.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,6 +29,7 @@ __date__ = "12/03/2019"
import os
import logging
+from typing import Optional
import functools
from .. import qt
from .. import icons
@@ -38,6 +38,10 @@ from .Hdf5Item import Hdf5Item
from .Hdf5LoadingItem import Hdf5LoadingItem
from . import _utils
from ... import io as silx_io
+from ...io._sliceh5 import DatasetSlice
+
+import h5py
+
_logger = logging.getLogger(__name__)
@@ -58,6 +62,8 @@ def _createRootLabel(h5obj):
if path.startswith("/"):
path = path[1:]
label = "%s::%s" % (filename, path)
+ if isinstance(h5obj, DatasetSlice):
+ label += str(list(h5obj.indices))
return label
@@ -66,7 +72,8 @@ class LoadingItemRunnable(qt.QRunnable):
class __Signals(qt.QObject):
"""Signal holder"""
- itemReady = qt.Signal(object, object, object)
+
+ itemReady = qt.Signal(object, object, object, str)
runnerFinished = qt.Signal(object)
def __init__(self, filename, item):
@@ -98,7 +105,13 @@ class LoadingItemRunnable(qt.QRunnable):
:rtpye: Hdf5Node
"""
text = _createRootLabel(h5obj)
- item = Hdf5Item(text=text, obj=h5obj, parent=oldItem.parent, populateAll=True)
+ item = Hdf5Item(
+ text=text,
+ obj=h5obj,
+ parent=oldItem.parent,
+ populateAll=True,
+ openedPath=oldItem._openedPath,
+ )
return item
def run(self):
@@ -117,7 +130,7 @@ class LoadingItemRunnable(qt.QRunnable):
if h5file is not None:
h5file.close()
- self.itemReady.emit(self.oldItem, newItem, error)
+ self.itemReady.emit(self.oldItem, newItem, error, self.filename)
self.runnerFinished.emit(self)
def autoDelete(self):
@@ -172,7 +185,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
]
"""List of logical columns available"""
- sigH5pyObjectLoaded = qt.Signal(object)
+ sigH5pyObjectLoaded = qt.Signal(object, str)
"""Emitted when a new root item was loaded and inserted to the model."""
sigH5pyObjectRemoved = qt.Signal(object)
@@ -192,13 +205,13 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
super(Hdf5TreeModel, self).__init__(parent)
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'
+ 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()
@@ -238,7 +251,6 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
"""Static method to close explicit references to internal objects."""
_logger.debug("Clear Hdf5TreeModel")
for obj in fileList:
- _logger.debug("Close file %s", obj.filename)
obj.close()
fileList[:] = []
@@ -257,14 +269,21 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
index2 = self.index(i, self.columnCount() - 1, qt.QModelIndex())
self.dataChanged.emit(index1, index2)
- def __itemReady(self, oldItem, newItem, error):
+ def __itemReady(
+ self,
+ oldItem: Hdf5Node,
+ newItem: Optional[Hdf5Node],
+ error: Optional[Exception],
+ filename: str,
+ ):
"""Called at the end of a concurent file loading, when the loading
item is ready. AN error is defined if an exception occured when
loading the newItem .
- :param Hdf5Node oldItem: current displayed item
- :param Hdf5Node newItem: item loaded, or None if error is defined
- :param Exception error: An exception, or None if newItem is defined
+ :param oldItem: current displayed item
+ :param newItem: item loaded, or None if error is defined
+ :param error: An exception, or None if newItem is defined
+ :param filename: The filename used to load the new item
"""
row = self.__root.indexOfChild(oldItem)
@@ -282,7 +301,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
self.endInsertRows()
if isinstance(oldItem, Hdf5LoadingItem):
- self.sigH5pyObjectLoaded.emit(newItem.obj)
+ self.sigH5pyObjectLoaded.emit(newItem.obj, filename)
else:
self.sigH5pyObjectSynchronized.emit(oldItem.obj, newItem.obj)
@@ -375,7 +394,9 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
if action == qt.Qt.IgnoreAction:
return True
- if self.__fileMoveEnabled and mimedata.hasFormat(_utils.Hdf5DatasetMimeData.MIME_TYPE):
+ if self.__fileMoveEnabled and mimedata.hasFormat(
+ _utils.Hdf5DatasetMimeData.MIME_TYPE
+ ):
if mimedata.isRoot():
dragNode = mimedata.node()
parentNode = self.nodeFromIndex(parentIndex)
@@ -395,10 +416,9 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
return True
if self.__fileDropEnabled and mimedata.hasFormat("text/uri-list"):
-
parentNode = self.nodeFromIndex(parentIndex)
if parentNode is not self.__root:
- while(parentNode is not self.__root):
+ while parentNode is not self.__root:
node = parentNode
parentNode = node.parent
row = parentNode.indexOfChild(node)
@@ -415,7 +435,10 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
messages.append(e.args[0])
if len(messages) > 0:
title = "Error occurred when loading files"
- message = "<html>%s:<ul><li>%s</li><ul></html>" % (title, "</li><li>".join(messages))
+ message = "<html>%s:<ul><li>%s</li><ul></html>" % (
+ title,
+ "</li><li>".join(messages),
+ )
qt.QMessageBox.critical(None, title, message)
return True
@@ -434,14 +457,31 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
self.__root.insertChild(row, node)
self.endInsertRows()
- def moveRow(self, sourceParentIndex, sourceRow, destinationParentIndex, destinationRow):
+ def moveRow(
+ self, sourceParentIndex, sourceRow, destinationParentIndex, destinationRow
+ ):
if sourceRow == destinationRow or sourceRow == destinationRow - 1:
# abort move, same place
return
- return self.moveRows(sourceParentIndex, sourceRow, 1, destinationParentIndex, destinationRow)
-
- def moveRows(self, sourceParentIndex, sourceRow, count, destinationParentIndex, destinationRow):
- self.beginMoveRows(sourceParentIndex, sourceRow, sourceRow, destinationParentIndex, destinationRow)
+ return self.moveRows(
+ sourceParentIndex, sourceRow, 1, destinationParentIndex, destinationRow
+ )
+
+ def moveRows(
+ self,
+ sourceParentIndex,
+ sourceRow,
+ count,
+ destinationParentIndex,
+ destinationRow,
+ ):
+ self.beginMoveRows(
+ sourceParentIndex,
+ sourceRow,
+ sourceRow,
+ destinationParentIndex,
+ destinationRow,
+ )
sourceNode = self.nodeFromIndex(sourceParentIndex)
destinationNode = self.nodeFromIndex(destinationParentIndex)
@@ -523,14 +563,14 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
return qt.QModelIndex()
row = grandparent.indexOfChild(parent)
- assert row != - 1
+ assert row != -1
return self.createIndex(row, 0, parent)
def nodeFromIndex(self, index):
return index.internalPointer() if index.isValid() else self.__root
def _closeFileIfOwned(self, node):
- """"Close the file if it was loaded from a filename or a
+ """Close the file if it was loaded from a filename or a
drag-and-drop"""
obj = node.obj
for f in self.__openedFiles:
@@ -554,10 +594,30 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
filename = node.obj.filename
self.insertFileAsync(filename, index.row(), synchronizingNode=node)
+ @staticmethod
+ def __areH5pyObjectEqual(obj1, obj2):
+ """Compare commonh5/h5py object without comparing data"""
+ if isinstance(obj1, h5py.HLObject): # Priority to h5py __eq__
+ return obj1 == obj2
+
+ # else compare commonh5 objects
+ if not isinstance(obj2, type(obj1)):
+ return False
+
+ def key(item):
+ info = [item.name]
+ if item.file is not None:
+ info += [item.file.filename, item.file.mode]
+ if isinstance(item, DatasetSlice):
+ info.append(item.indices)
+ return tuple(info)
+
+ return key(obj1) == key(obj2)
+
def h5pyObjectRow(self, h5pyObject):
for row in range(self.__root.childCount()):
item = self.__root.child(row)
- if item.obj == h5pyObject:
+ if self.__areH5pyObjectEqual(item.obj, h5pyObject):
return row
return -1
@@ -572,7 +632,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
index = 0
while index < self.__root.childCount():
item = self.__root.child(index)
- if item.obj == h5pyObject:
+ if self.__areH5pyObjectEqual(item.obj, h5pyObject):
qindex = self.index(index, 0, qt.QModelIndex())
self.synchronizeIndex(qindex)
index += 1
@@ -602,13 +662,19 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
index = 0
while index < self.__root.childCount():
item = self.__root.child(index)
- if item.obj == h5pyObject:
+ if self.__areH5pyObjectEqual(item.obj, h5pyObject):
qindex = self.index(index, 0, qt.QModelIndex())
self.removeIndex(qindex)
else:
index += 1
- def insertH5pyObject(self, h5pyObject, text=None, row=-1):
+ def insertH5pyObject(
+ self,
+ h5pyObject,
+ text: Optional[str] = None,
+ row: int = -1,
+ filename: Optional[str] = None,
+ ):
"""Append an HDF5 object from h5py to the tree.
:param h5pyObject: File handle/descriptor for a :class:`h5py.File`
@@ -618,7 +684,15 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
text = _createRootLabel(h5pyObject)
if row == -1:
row = self.__root.childCount()
- self.insertNode(row, Hdf5Item(text=text, obj=h5pyObject, parent=self.__root))
+ self.insertNode(
+ row,
+ Hdf5Item(
+ text=text,
+ obj=h5pyObject,
+ parent=self.__root,
+ openedPath=filename,
+ ),
+ )
def hasPendingOperations(self):
return len(self.__runnerSet) > 0
@@ -630,7 +704,12 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
# create temporary item
if synchronizingNode is None:
text = os.path.basename(filename)
- item = Hdf5LoadingItem(text=text, parent=self.__root, animatedIcon=self.__animatedIcon)
+ item = Hdf5LoadingItem(
+ text=text,
+ parent=self.__root,
+ animatedIcon=self.__animatedIcon,
+ openedPath=filename,
+ )
self.insertNode(row, item)
else:
item = synchronizingNode
@@ -654,8 +733,8 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
h5file = silx_io.open(filename)
if self.__ownFiles:
self.__openedFiles.append(h5file)
- self.sigH5pyObjectLoaded.emit(h5file)
- self.insertH5pyObject(h5file, row=row)
+ self.sigH5pyObjectLoaded.emit(h5file, filename)
+ self.insertH5pyObject(h5file, row=row, filename=filename)
except IOError:
_logger.debug("File '%s' can't be read.", filename, exc_info=True)
raise
diff --git a/src/silx/gui/hdf5/Hdf5TreeView.py b/src/silx/gui/hdf5/Hdf5TreeView.py
index b276618..a477fc3 100644
--- a/src/silx/gui/hdf5/Hdf5TreeView.py
+++ b/src/silx/gui/hdf5/Hdf5TreeView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -58,6 +57,7 @@ class Hdf5TreeView(qt.QTreeView):
:meth:`removeContextMenuCallback` to add your custum actions according
to the selected objects.
"""
+
def __init__(self, parent=None):
"""
Constructor
@@ -168,7 +168,11 @@ class Hdf5TreeView(qt.QTreeView):
def dragEnterEvent(self, event):
model = self.findHdf5TreeModel()
- if model is not None and model.isFileDropEnabled() and event.mimeData().hasFormat("text/uri-list"):
+ if (
+ model is not None
+ and model.isFileDropEnabled()
+ and event.mimeData().hasFormat("text/uri-list")
+ ):
self.setState(qt.QAbstractItemView.DraggingState)
event.accept()
else:
@@ -176,7 +180,11 @@ class Hdf5TreeView(qt.QTreeView):
def dragMoveEvent(self, event):
model = self.findHdf5TreeModel()
- if model is not None and model.isFileDropEnabled() and event.mimeData().hasFormat("text/uri-list"):
+ if (
+ model is not None
+ and model.isFileDropEnabled()
+ and event.mimeData().hasFormat("text/uri-list")
+ ):
event.setDropAction(qt.Qt.CopyAction)
event.accept()
else:
@@ -216,7 +224,9 @@ class Hdf5TreeView(qt.QTreeView):
model = model.sourceModel()
else:
break
- raise RuntimeError("Model from the requested index is not reachable from this view")
+ raise RuntimeError(
+ "Model from the requested index is not reachable from this view"
+ )
def mapToModel(self, index):
"""Map an index from any model reachable by the view to an index from
diff --git a/src/silx/gui/hdf5/NexusSortFilterProxyModel.py b/src/silx/gui/hdf5/NexusSortFilterProxyModel.py
index 9c3533f..0bc7352 100644
--- a/src/silx/gui/hdf5/NexusSortFilterProxyModel.py
+++ b/src/silx/gui/hdf5/NexusSortFilterProxyModel.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
@@ -77,7 +76,8 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel):
"""
if sourceLeft.column() != Hdf5TreeModel.NAME_COLUMN:
return super(NexusSortFilterProxyModel, self).lessThan(
- sourceLeft, sourceRight)
+ sourceLeft, sourceRight
+ )
# Do not sort child of root (files)
if sourceLeft.parent() == qt.QModelIndex():
@@ -218,7 +218,9 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel):
if index.column() == Hdf5TreeModel.NAME_COLUMN:
if role == qt.Qt.DecorationRole:
sourceIndex = self.mapToSource(index)
- item = self.sourceModel().data(sourceIndex, Hdf5TreeModel.H5PY_ITEM_ROLE)
+ item = self.sourceModel().data(
+ sourceIndex, Hdf5TreeModel.H5PY_ITEM_ROLE
+ )
if self.__isNXnode(item):
result = self.__getNxIcon(result)
return result
diff --git a/src/silx/gui/hdf5/__init__.py b/src/silx/gui/hdf5/__init__.py
index 1b5a602..8e07407 100644
--- a/src/silx/gui/hdf5/__init__.py
+++ b/src/silx/gui/hdf5/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
@@ -41,4 +40,10 @@ from ._utils import Hdf5ContextMenuEvent # noqa
from .NexusSortFilterProxyModel import NexusSortFilterProxyModel # noqa
from .Hdf5TreeModel import Hdf5TreeModel # noqa
-__all__ = ['Hdf5TreeView', 'H5Node', 'Hdf5ContextMenuEvent', 'NexusSortFilterProxyModel', 'Hdf5TreeModel']
+__all__ = [
+ "Hdf5TreeView",
+ "H5Node",
+ "Hdf5ContextMenuEvent",
+ "NexusSortFilterProxyModel",
+ "Hdf5TreeModel",
+]
diff --git a/src/silx/gui/hdf5/_utils.py b/src/silx/gui/hdf5/_utils.py
index 8f32252..7232bfe 100644
--- a/src/silx/gui/hdf5/_utils.py
+++ b/src/silx/gui/hdf5/_utils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -34,7 +33,7 @@ __date__ = "17/01/2019"
from html import escape
import logging
import os.path
-
+from silx.gui import constants
import silx.io.utils
import silx.io.url
from .. import qt
@@ -110,19 +109,20 @@ class Hdf5DatasetMimeData(qt.QMimeData):
MIME_TYPE = "application/x-internal-h5py-dataset"
- SILX_URI_TYPE = "application/x-silx-uri"
+ SILX_URI_TYPE = constants.SILX_URI_MIMETYPE
+ """For compatibility with silx <= 1.1"""
def __init__(self, node=None, dataset=None, isRoot=False):
qt.QMimeData.__init__(self)
self.__dataset = dataset
self.__node = node
self.__isRoot = isRoot
- self.setData(self.MIME_TYPE, "".encode(encoding='utf-8'))
+ self.setData(self.MIME_TYPE, "".encode(encoding="utf-8"))
if node is not None:
h5Node = H5Node(node)
silxUrl = h5Node.url
self.setText(silxUrl)
- self.setData(self.SILX_URI_TYPE, silxUrl.encode(encoding='utf-8'))
+ self.setData(constants.SILX_URI_MIMETYPE, silxUrl.encode(encoding="utf-8"))
def isRoot(self):
return self.__isRoot
@@ -428,9 +428,9 @@ class H5Node(object):
:rtype: ~silx.io.url.DataUrl
"""
absolute_filename = os.path.abspath(self.local_filename)
- return silx.io.url.DataUrl(scheme="silx",
- file_path=absolute_filename,
- data_path=self.local_name)
+ return silx.io.url.DataUrl(
+ scheme="silx", file_path=absolute_filename, data_path=self.local_name
+ )
@property
def url(self):
diff --git a/src/silx/gui/hdf5/test/__init__.py b/src/silx/gui/hdf5/test/__init__.py
index 71128fb..b03339f 100644
--- a/src/silx/gui/hdf5/test/__init__.py
+++ b/src/silx/gui/hdf5/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/hdf5/test/test_hdf5.py b/src/silx/gui/hdf5/test/test_hdf5.py
index 9b1b88a..1271b48 100755
--- a/src/silx/gui/hdf5/test/test_hdf5.py
+++ b/src/silx/gui/hdf5/test/test_hdf5.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,23 +30,23 @@ __date__ = "12/03/2019"
import time
import os
-import unittest
import tempfile
import numpy
-from pkg_resources import parse_version
+from packaging.version import Version
from contextlib import contextmanager
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt
from silx.gui import hdf5
from silx.gui.utils.testutils import SignalListener
from silx.io import commonh5
+from silx.io.url import DataUrl
import weakref
import h5py
import pytest
-h5py2_9 = parse_version(h5py.version.version) >= parse_version('2.9.0')
+h5py2_9 = Version(h5py.version.version) >= Version("2.9.0")
@pytest.fixture(scope="class")
@@ -70,7 +69,6 @@ def create_NXentry(group, name):
@pytest.mark.usefixtures("useH5File")
class TestHdf5TreeModel(TestCaseQt):
-
def setUp(self):
super(TestHdf5TreeModel, self).setUp()
@@ -135,7 +133,9 @@ class TestHdf5TreeModel(TestCaseQt):
self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
model.insertFileAsync(self.filename)
index = model.index(0, 0, qt.QModelIndex())
- self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5LoadingItem.Hdf5LoadingItem)
+ self.assertIsInstance(
+ model.nodeFromIndex(index), hdf5.Hdf5LoadingItem.Hdf5LoadingItem
+ )
self.waitForPendingOperations(model)
index = model.index(0, 0, qt.QModelIndex())
self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5Item.Hdf5Item)
@@ -245,7 +245,9 @@ class TestHdf5TreeModel(TestCaseQt):
h5File = model.data(index, role=hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
model.removeIndex(index)
self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
- self.assertTrue(bool(h5File.id.valid), "The HDF5 file was unexpetedly closed")
+ self.assertTrue(
+ bool(h5File.id.valid), "The HDF5 file was unexpetedly closed"
+ )
finally:
h5File.close()
@@ -270,7 +272,12 @@ class TestHdf5TreeModel(TestCaseQt):
def getRowDataAsDict(self, model, row):
displayed = {}
- roles = [qt.Qt.DisplayRole, qt.Qt.DecorationRole, qt.Qt.ToolTipRole, qt.Qt.TextAlignmentRole]
+ roles = [
+ qt.Qt.DisplayRole,
+ qt.Qt.DecorationRole,
+ qt.Qt.ToolTipRole,
+ qt.Qt.TextAlignmentRole,
+ ]
for column in range(0, model.columnCount(qt.QModelIndex())):
index = model.index(0, column, qt.QModelIndex())
for role in roles:
@@ -287,13 +294,27 @@ class TestHdf5TreeModel(TestCaseQt):
model = hdf5.Hdf5TreeModel()
model.insertH5pyObject(h5)
displayed = self.getRowDataAsDict(model, row=0)
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock")
- self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon)
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], None)
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "File")
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock"
+ )
+ self.assertIsInstance(
+ displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], ""
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], ""
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], ""
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], None
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "File"
+ )
def testGroupData(self):
h5 = commonh5.File("/foo/bar/1.mock", "w")
@@ -303,13 +324,27 @@ class TestHdf5TreeModel(TestCaseQt):
model = hdf5.Hdf5TreeModel()
model.insertH5pyObject(d)
displayed = self.getRowDataAsDict(model, row=0)
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo")
- self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon)
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "fooo")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Group")
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo"
+ )
+ self.assertIsInstance(
+ displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], ""
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], ""
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], ""
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "fooo"
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Group"
+ )
def testDatasetData(self):
h5 = commonh5.File("/foo/bar/1.mock", "w")
@@ -319,13 +354,29 @@ class TestHdf5TreeModel(TestCaseQt):
model = hdf5.Hdf5TreeModel()
model.insertH5pyObject(d)
displayed = self.getRowDataAsDict(model, row=0)
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo")
- self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon)
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], value.dtype.name)
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "3")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "[1 2 3]")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "[1 2 3]")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Dataset")
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo"
+ )
+ self.assertIsInstance(
+ displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole],
+ value.dtype.name,
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "3"
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "[1 2 3]"
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole],
+ "[1 2 3]",
+ )
+ self.assertEqual(
+ displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Dataset"
+ )
def testDropLastAsFirst(self):
model = hdf5.Hdf5TreeModel()
@@ -366,17 +417,18 @@ class TestHdf5TreeModel(TestCaseQt):
@pytest.mark.usefixtures("useH5File")
class TestHdf5TreeModelSignals(TestCaseQt):
-
def setUp(self):
TestCaseQt.setUp(self)
self.model = hdf5.Hdf5TreeModel()
- self.h5 = h5py.File(self.filename, mode='r')
+ self.h5 = h5py.File(self.filename, mode="r")
self.model.insertH5pyObject(self.h5)
self.listener = SignalListener()
self.model.sigH5pyObjectLoaded.connect(self.listener.partial(signal="loaded"))
self.model.sigH5pyObjectRemoved.connect(self.listener.partial(signal="removed"))
- self.model.sigH5pyObjectSynchronized.connect(self.listener.partial(signal="synchronized"))
+ self.model.sigH5pyObjectSynchronized.connect(
+ self.listener.partial(signal="synchronized")
+ )
def tearDown(self):
self.signals = None
@@ -396,16 +448,28 @@ class TestHdf5TreeModelSignals(TestCaseQt):
raise RuntimeError("Still waiting for a pending operation")
def testInsert(self):
- h5 = h5py.File(self.filename, mode='r')
+ h5 = h5py.File(self.filename, mode="r")
self.model.insertH5pyObject(h5)
self.assertEqual(self.listener.callCount(), 0)
def testLoaded(self):
- self.model.insertFile(self.filename)
- self.assertEqual(self.listener.callCount(), 1)
- self.assertEqual(self.listener.karguments(argumentName="signal")[0], "loaded")
- self.assertIsNot(self.listener.arguments(callIndex=0)[0], self.h5)
- self.assertEqual(self.listener.arguments(callIndex=0)[0].filename, self.filename)
+ for data_path in [None, "/arrays/scalar"]:
+ with self.subTest(data_path=data_path):
+ url = DataUrl(file_path=self.filename, data_path=data_path)
+ insertedFilename = url.path()
+ self.model.insertFile(insertedFilename)
+ self.assertEqual(self.listener.callCount(), 1)
+ self.assertEqual(
+ self.listener.karguments(argumentName="signal")[0], "loaded"
+ )
+ self.assertIsNot(self.listener.arguments(callIndex=0)[0], self.h5)
+ self.assertEqual(
+ self.listener.arguments(callIndex=0)[0].file.filename, self.filename
+ )
+ self.assertEqual(
+ self.listener.arguments(callIndex=0)[1], insertedFilename
+ )
+ self.listener.clear()
def testRemoved(self):
self.model.removeH5pyObject(self.h5)
@@ -417,13 +481,14 @@ class TestHdf5TreeModelSignals(TestCaseQt):
self.model.synchronizeH5pyObject(self.h5)
self.waitForPendingOperations(self.model)
self.assertEqual(self.listener.callCount(), 1)
- self.assertEqual(self.listener.karguments(argumentName="signal")[0], "synchronized")
+ self.assertEqual(
+ self.listener.karguments(argumentName="signal")[0], "synchronized"
+ )
self.assertIs(self.listener.arguments(callIndex=0)[0], self.h5)
self.assertIsNot(self.listener.arguments(callIndex=0)[1], self.h5)
class TestNexusSortFilterProxyModel(TestCaseQt):
-
def getChildNames(self, model, index):
count = model.rowCount(index)
result = []
@@ -452,9 +517,15 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
"""Test NXentry with start_time"""
model = hdf5.Hdf5TreeModel()
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")]))
+ 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()
@@ -467,9 +538,15 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
"""Test NXentry with end_time"""
model = hdf5.Hdf5TreeModel()
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")]))
+ 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()
@@ -566,7 +643,7 @@ class TestNexusSortFilterProxyModel(TestCaseQt):
self.assertListEqual(names, ["100aaa", "aaa100"])
-@pytest.fixture(scope='class')
+@pytest.fixture(scope="class")
def useH5Model(request, tmpdir_factory):
# Create HDF5 files
tmp = tmpdir_factory.mktemp("test_hdf5")
@@ -581,29 +658,43 @@ def useH5Model(request, tmpdir_factory):
externalh5["/ext/vds1"] = [2, 3]
externalh5.close()
- numpy.array([0,1,10,10,2,3]).tofile(extDatFileName)
+ numpy.array([0, 1, 10, 10, 2, 3]).tofile(extDatFileName)
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_soft_link"] = h5py.SoftLink("/link/soft_link")
+ h5["link/soft_link_to_external_link"] = h5py.SoftLink("/link/external_link")
h5["link/soft_link_to_file"] = h5py.SoftLink("/")
h5["group/soft_link_relative"] = h5py.SoftLink("dataset")
h5["link/external_link"] = h5py.ExternalLink(extH5FileName, "/target/dataset")
- h5["link/external_link_to_link"] = h5py.ExternalLink(extH5FileName, "/target/link")
- h5["broken_link/external_broken_file"] = h5py.ExternalLink(extH5FileName + "_not_exists", "/target/link")
- h5["broken_link/external_broken_link"] = h5py.ExternalLink(extH5FileName, "/target/not_exists")
+ h5["link/external_link_to_soft_link"] = h5py.ExternalLink(
+ extH5FileName, "/target/link"
+ )
+ h5["broken_link/external_broken_file"] = h5py.ExternalLink(
+ extH5FileName + "_not_exists", "/target/link"
+ )
+ h5["broken_link/external_broken_link"] = h5py.ExternalLink(
+ extH5FileName, "/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")
if h5py2_9:
- layout = h5py.VirtualLayout((2,2), dtype=int)
- layout[0] = h5py.VirtualSource("base__external.h5", name="/ext/vds0", shape=(2,), dtype=int)
- layout[1] = h5py.VirtualSource("base__external.h5", name="/ext/vds1", shape=(2,), dtype=int)
+ layout = h5py.VirtualLayout((2, 2), dtype=int)
+ layout[0] = h5py.VirtualSource(
+ "base__external.h5", name="/ext/vds0", shape=(2,), dtype=int
+ )
+ layout[1] = h5py.VirtualSource(
+ "base__external.h5", name="/ext/vds1", shape=(2,), dtype=int
+ )
h5.create_group("/ext")
h5["/ext"].create_virtual_dataset("virtual", layout)
- external = [("base__external.dat", 0, 2*8), ("base__external.dat", 4*8, 2*8)]
- h5["/ext"].create_dataset("raw", shape=(2,2), dtype=int, external=external)
+ external = [
+ ("base__external.dat", 0, 2 * 8),
+ ("base__external.dat", 4 * 8, 2 * 8),
+ ]
+ h5["/ext"].create_dataset("raw", shape=(2, 2), dtype=int, external=external)
h5.close()
with h5py.File(filename, mode="r") as h5File:
@@ -616,7 +707,7 @@ def useH5Model(request, tmpdir_factory):
TestCaseQt.qWaitForDestroy(ref)
-@pytest.mark.usefixtures('useH5Model')
+@pytest.mark.usefixtures("useH5Model")
class _TestModelBase(TestCaseQt):
def getIndexFromPath(self, model, path):
"""
@@ -640,7 +731,6 @@ class _TestModelBase(TestCaseQt):
class TestH5Item(_TestModelBase):
-
def testFile(self):
path = ["base.h5"]
h5item = self.getH5ItemFromPath(self.model, path)
@@ -666,7 +756,7 @@ class TestH5Item(_TestModelBase):
self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "Soft")
def testSoftLinkToLink(self):
- path = ["base.h5", "link", "soft_link_to_link"]
+ path = ["base.h5", "link", "soft_link_to_soft_link"]
h5item = self.getH5ItemFromPath(self.model, path)
self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "Soft")
@@ -684,7 +774,7 @@ class TestH5Item(_TestModelBase):
self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "External")
def testExternalLinkToLink(self):
- path = ["base.h5", "link", "external_link_to_link"]
+ path = ["base.h5", "link", "external_link_to_soft_link"]
h5item = self.getH5ItemFromPath(self.model, path)
self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "External")
@@ -720,7 +810,14 @@ class TestH5Item(_TestModelBase):
self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "")
def testDatasetFromSoftLinkToFile(self):
- path = ["base.h5", "link", "soft_link_to_file", "link", "soft_link_to_group", "dataset"]
+ path = [
+ "base.h5",
+ "link",
+ "soft_link_to_file",
+ "link",
+ "soft_link_to_group",
+ "dataset",
+ ]
h5item = self.getH5ItemFromPath(self.model, path)
self.assertEqual(h5item.dataLink(qt.Qt.DisplayRole), "")
@@ -741,7 +838,6 @@ class TestH5Item(_TestModelBase):
class TestH5Node(_TestModelBase):
-
def getH5NodeFromPath(self, model, path):
item = self.getH5ItemFromPath(model, path)
h5node = hdf5.H5Node(item)
@@ -791,16 +887,30 @@ class TestH5Node(_TestModelBase):
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"]
+ def testSoftLinkToSoftLink(self):
+ path = ["base.h5", "link", "soft_link_to_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_to_link")
- self.assertEqual(h5node.local_name, "/link/soft_link_to_link")
+ self.assertEqual(h5node.local_basename, "soft_link_to_soft_link")
+ self.assertEqual(h5node.local_name, "/link/soft_link_to_soft_link")
+
+ def testSoftLinkToExternalLink(self):
+ path = ["base.h5", "link", "soft_link_to_external_link"]
+ h5node = self.getH5NodeFromPath(self.model, path)
+
+ with self.assertRaises(KeyError):
+ # h5py bug: #1706
+ 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, "soft_link_to_external_link")
+ self.assertEqual(h5node.local_name, "/link/soft_link_to_external_link")
def testSoftLinkRelative(self):
path = ["base.h5", "group", "soft_link_relative"]
@@ -825,19 +935,18 @@ class TestH5Node(_TestModelBase):
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"]
+ def testExternalLinkToSoftLink(self):
+ path = ["base.h5", "link", "external_link_to_soft_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")
+ self.assertEqual(h5node.local_basename, "external_link_to_soft_link")
+ self.assertEqual(h5node.local_name, "/link/external_link_to_soft_link")
def testExternalBrokenFile(self):
path = ["base.h5", "broken_link", "external_broken_file"]
@@ -897,7 +1006,14 @@ class TestH5Node(_TestModelBase):
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"]
+ 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)
@@ -905,7 +1021,9 @@ class TestH5Node(_TestModelBase):
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")
+ self.assertEqual(
+ h5node.local_name, "/link/soft_link_to_file/link/soft_link_to_group/dataset"
+ )
@pytest.mark.skipif(not h5py2_9, reason="requires h5py>=2.9")
def testExternalVirtual(self):
diff --git a/src/silx/gui/icons.py b/src/silx/gui/icons.py
index 1493b92..3e2501b 100644
--- a/src/silx/gui/icons.py
+++ b/src/silx/gui/icons.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -38,7 +37,6 @@ import weakref
from . import qt
import silx.resources
from silx.utils import weakref as silxweakref
-from silx.utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -93,7 +91,7 @@ class AbstractAnimatedIcon(qt.QObject):
"""Signal sent with a QIcon everytime the animation changed."""
def register(self, obj):
- """Register an object to the AnimatedIcon.
+ """Register an object to the AbstractAnimatedIcon.
If no object are registred, the animation is paused.
Object are stored in a weaked list.
@@ -121,7 +119,7 @@ class AbstractAnimatedIcon(qt.QObject):
return len(self.__targets)
def isRegistered(self, obj):
- """Returns true if the object is registred in the AnimatedIcon.
+ """Returns true if the object is registred in the AbstractAnimatedIcon.
:param object obj: An object
:rtype: bool
@@ -192,7 +190,7 @@ class MovieAnimatedIcon(AbstractAnimatedIcon):
def _updateState(self):
"""Update the movie play according to internal stat of the
- AnimatedIcon."""
+ MovieAnimatedIcon."""
self.__movie.setPaused(not self.hasRegistredObjects())
@@ -213,7 +211,7 @@ class MultiImageAnimatedIcon(AbstractAnimatedIcon):
self.__frames = []
for i in range(100):
try:
- frame_filename = os.sep.join((filename, ("%02d" %i)))
+ frame_filename = os.sep.join((filename, ("%02d" % i)))
frame_file = getQFile(frame_filename)
except ValueError:
break
@@ -258,22 +256,6 @@ class MultiImageAnimatedIcon(AbstractAnimatedIcon):
self.__timer.stop()
-class AnimatedIcon(MovieAnimatedIcon):
- """Store a looping QMovie to provide icons for each frames.
- Provides an event with the new icon everytime the movie frame
- is updated.
-
- It may not be available anymore for the silx release 0.6.
-
- .. deprecated:: 0.5
- Use :class:`MovieAnimatedIcon` instead.
- """
-
- @deprecated
- def __init__(self, filename, parent=None):
- MovieAnimatedIcon.__init__(self, filename, parent=parent)
-
-
def getWaitIcon():
"""Returns a cached version of the waiting AbstractAnimatedIcon.
@@ -308,7 +290,6 @@ def getAnimatedIcon(name):
key = name + "__anim"
cached_icons = getIconCache()
if key not in cached_icons:
-
qtMajorVersion = int(qt.qVersion().split(".")[0])
icon = None
@@ -416,10 +397,11 @@ def getQFile(name):
for format_ in _supported_formats:
format_ = str(format_)
- filename = silx.resources._resource_filename('%s.%s' % (name, format_),
- default_directory=os.path.join('gui', 'icons'))
+ filename = silx.resources._resource_filename(
+ "%s.%s" % (name, format_), default_directory="gui/icons"
+ )
qfile = qt.QFile(filename)
if qfile.exists():
return qfile
_logger.debug("File '%s' not found.", filename)
- raise ValueError('Not an icon name: %s' % name)
+ raise ValueError("Not an icon name: %s" % name)
diff --git a/src/silx/gui/plot/AlphaSlider.py b/src/silx/gui/plot/AlphaSlider.py
index da55b1e..8a0a711 100644
--- a/src/silx/gui/plot/AlphaSlider.py
+++ b/src/silx/gui/plot/AlphaSlider.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -97,6 +96,7 @@ class BaseAlphaSlider(qt.QSlider):
You must subclass this class and implement :meth:`getItem`.
"""
+
sigAlphaChanged = qt.Signal(float)
"""Emits the alpha value when the slider's value changes,
as a float between 0. and 1."""
@@ -120,7 +120,7 @@ class BaseAlphaSlider(qt.QSlider):
self.setEnabled(False)
else:
alpha = self.getItem().getAlpha()
- self.setValue(round(255*alpha))
+ self.setValue(round(255 * alpha))
self.valueChanged.connect(self._valueChanged)
@@ -133,8 +133,8 @@ class BaseAlphaSlider(qt.QSlider):
:rtype: :class:`silx.plot.items.Item`
"""
raise NotImplementedError(
- "BaseAlphaSlider must be subclassed to " +
- "implement getItem()")
+ "BaseAlphaSlider must be subclassed to " + "implement getItem()"
+ )
def getAlpha(self):
"""Get the opacity, as a float between 0. and 1.
@@ -142,15 +142,14 @@ class BaseAlphaSlider(qt.QSlider):
:return: Alpha value in [0., 1.]
:rtype: float
"""
- return self.value() / 255.
+ return self.value() / 255.0
def _valueChanged(self, value):
self._updateItem()
- self.sigAlphaChanged.emit(value / 255.)
+ self.sigAlphaChanged.emit(value / 255.0)
def _updateItem(self):
- """Update the item's alpha channel.
- """
+ """Update the item's alpha channel."""
item = self.getItem()
if item is not None:
item.setAlpha(self.getAlpha())
@@ -165,6 +164,7 @@ class ActiveImageAlphaSlider(BaseAlphaSlider):
See documentation of :class:`BaseAlphaSlider`
"""
+
def __init__(self, parent=None, plot=None):
"""
@@ -204,8 +204,8 @@ class NamedItemAlphaSlider(BaseAlphaSlider):
:param str legend: Legend of item whose transparency is to be
controlled.
"""
- def __init__(self, parent=None, plot=None,
- kind=None, legend=None):
+
+ def __init__(self, parent=None, plot=None, kind=None, legend=None):
self._item_legend = legend
self._item_kind = kind
@@ -235,8 +235,7 @@ class NamedItemAlphaSlider(BaseAlphaSlider):
:rtype: subclass of :class:`silx.gui.plot.items.Item`"""
if self._item_legend is None or self._item_kind is None:
return None
- return self.plot._getItem(kind=self._item_kind,
- legend=self._item_legend)
+ return self.plot._getItem(kind=self._item_kind, legend=self._item_legend)
def setLegend(self, legend):
"""Associate a different item (of the same kind) to the slider.
@@ -281,9 +280,9 @@ class NamedImageAlphaSlider(NamedItemAlphaSlider):
:param str legend: Legend of image whose transparency is to be
controlled.
"""
+
def __init__(self, parent=None, plot=None, legend=None):
- NamedItemAlphaSlider.__init__(self, parent, plot,
- kind="image", legend=legend)
+ NamedItemAlphaSlider.__init__(self, parent, plot, kind="image", legend=legend)
class NamedScatterAlphaSlider(NamedItemAlphaSlider):
@@ -295,6 +294,6 @@ class NamedScatterAlphaSlider(NamedItemAlphaSlider):
:param str legend: Legend of scatter whose transparency is to be
controlled.
"""
+
def __init__(self, parent=None, plot=None, legend=None):
- NamedItemAlphaSlider.__init__(self, parent, plot,
- kind="scatter", legend=legend)
+ NamedItemAlphaSlider.__init__(self, parent, plot, kind="scatter", legend=legend)
diff --git a/src/silx/gui/plot/ColorBar.py b/src/silx/gui/plot/ColorBar.py
index 8cafc06..ee31f25 100644
--- a/src/silx/gui/plot/ColorBar.py
+++ b/src/silx/gui/plot/ColorBar.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -70,6 +69,7 @@ class ColorBarWidget(qt.QWidget):
:param plot: PlotWidget the colorbar is attached to (optional)
:param str legend: the label to set to the colorbar
"""
+
sigVisibleChanged = qt.Signal(bool)
"""Emitted when the property `visible` have changed."""
@@ -89,12 +89,11 @@ class ColorBarWidget(qt.QWidget):
self.setLayout(qt.QHBoxLayout())
# create color scale widget
- self._colorScale = ColorScaleBar(parent=self,
- colormap=None)
+ self._colorScale = ColorScaleBar(parent=self, colormap=None)
self.layout().addWidget(self._colorScale)
# legend (is the right group)
- self.legend = _VerticalLegend('', self)
+ self.legend = _VerticalLegend("", self)
self.layout().addWidget(self.legend)
self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
@@ -119,10 +118,8 @@ class ColorBarWidget(qt.QWidget):
self._isConnected = False
plot = self.getPlot()
if plot is not None and qt_inspect.isValid(plot):
- plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
- plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChanged)
+ plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
+ plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged)
plot.sigPlotSignal.disconnect(self._defaultColormapChanged)
def _connectPlot(self):
@@ -130,8 +127,7 @@ class ColorBarWidget(qt.QWidget):
plot = self.getPlot()
if plot is not None and not self._isConnected:
activeImageLegend = plot.getActiveImage(just_legend=True)
- activeScatterLegend = plot._getActiveItem(
- kind='scatter', just_legend=True)
+ activeScatterLegend = plot.getActiveScatter(just_legend=True)
if activeImageLegend is None and activeScatterLegend is None:
# Show plot default colormap
self._syncWithDefaultColormap()
@@ -171,8 +167,7 @@ class ColorBarWidget(qt.QWidget):
The data to display or item, needed if the colormap require an autoscale
"""
self._data = data
- self.getColorScaleBar().setColormap(colormap=colormap,
- data=data)
+ self.getColorScaleBar().setColormap(colormap=colormap, data=data)
if self._colormap is not None:
self._colormap.sigChanged.disconnect(self._colormapHasChanged)
self._colormap = colormap
@@ -180,11 +175,9 @@ class ColorBarWidget(qt.QWidget):
self._colormap.sigChanged.connect(self._colormapHasChanged)
def _colormapHasChanged(self):
- """handler of the Colormap.sigChanged signal
- """
+ """handler of the Colormap.sigChanged signal"""
assert self._colormap is not None
- self.setColormap(colormap=self._colormap,
- data=self._data)
+ self.setColormap(colormap=self._colormap, data=self._data)
def setLegend(self, legend):
"""Set the legend displayed along the colorbar
@@ -221,18 +214,16 @@ class ColorBarWidget(qt.QWidget):
return
# Sync with active scatter
- scatter = plot._getActiveItem(kind='scatter')
+ scatter = plot.getActiveScatter()
- self.setColormap(colormap=scatter.getColormap(),
- data=scatter)
+ self.setColormap(colormap=scatter.getColormap(), data=scatter)
def _activeImageChanged(self, previous, legend):
"""Handle plot active image changed"""
plot = self.getPlot()
if legend is None: # No active image, try with active scatter
- activeScatterLegend = plot._getActiveItem(
- kind='scatter', just_legend=True)
+ activeScatterLegend = plot.getActiveScatter(just_legend=True)
# No more active image, use active scatter if any
self._activeScatterChanged(None, activeScatterLegend)
else:
@@ -252,11 +243,13 @@ class ColorBarWidget(qt.QWidget):
def _defaultColormapChanged(self, event):
"""Handle plot default colormap changed"""
- if event['event'] == 'defaultColormapChanged':
+ if event["event"] == "defaultColormapChanged":
plot = self.getPlot()
- if (plot is not None and
- plot.getActiveImage() is None and
- plot._getActiveItem(kind='scatter') is None):
+ if (
+ plot is not None
+ and plot.getActiveImage() is None
+ and plot.getActiveScatter() is None
+ ):
# No active item, take default colormap update into account
self._syncWithDefaultColormap()
@@ -273,8 +266,8 @@ class ColorBarWidget(qt.QWidget):
class _VerticalLegend(qt.QLabel):
- """Display vertically the given text
- """
+ """Display vertically the given text"""
+
def __init__(self, text, parent=None):
"""
@@ -334,8 +327,7 @@ 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"""
- def __init__(self, parent=None, colormap=None, data=None,
- displayTicksValues=True):
+ def __init__(self, parent=None, colormap=None, data=None, displayTicksValues=True):
super(ColorScaleBar, self).__init__(parent)
self.minVal = None
@@ -346,10 +338,9 @@ class ColorScaleBar(qt.QWidget):
self.setLayout(qt.QGridLayout())
# create the left side group (ColorScale)
- self.colorScale = _ColorScale(colormap=colormap,
- data=data,
- parent=self,
- margin=ColorScaleBar._TEXT_MARGIN)
+ self.colorScale = _ColorScale(
+ colormap=colormap, data=data, parent=self, margin=ColorScaleBar._TEXT_MARGIN
+ )
if colormap:
vmin, vmax = colormap.getColormapRange(data)
normalizer = colormap._getNormalizer()
@@ -357,12 +348,14 @@ class ColorScaleBar(qt.QWidget):
vmin, vmax = colors.DEFAULT_MIN_LIN, colors.DEFAULT_MAX_LIN
normalizer = None
- self.tickbar = _TickBar(vmin=vmin,
- vmax=vmax,
- normalizer=normalizer,
- parent=self,
- displayValues=displayTicksValues,
- margin=ColorScaleBar._TEXT_MARGIN)
+ self.tickbar = _TickBar(
+ vmin=vmin,
+ vmax=vmax,
+ normalizer=normalizer,
+ parent=self,
+ displayValues=displayTicksValues,
+ margin=ColorScaleBar._TEXT_MARGIN,
+ )
self.layout().addWidget(self.tickbar, 1, 0, 1, 1, qt.Qt.AlignRight)
self.layout().addWidget(self.colorScale, 1, 1, qt.Qt.AlignLeft)
@@ -422,9 +415,7 @@ class ColorScaleBar(qt.QWidget):
vmin, vmax = None, None
normalizer = None
- self.tickbar.update(vmin=vmin,
- vmax=vmax,
- normalizer=normalizer)
+ self.tickbar.update(vmin=vmin, vmax=vmax, normalizer=normalizer)
self._setMinMaxLabels(vmin, vmax)
def setMinMaxVisible(self, val=True):
@@ -439,24 +430,24 @@ class ColorScaleBar(qt.QWidget):
"""Update the min and max label if we are in the case of the
configuration 'minMaxValueOnly'"""
if self.minVal is None:
- text, tooltip = '', ''
+ text, tooltip = "", ""
else:
if self.minVal == 0 or 0 <= numpy.log10(abs(self.minVal)) < 7:
- text = '%.7g' % self.minVal
+ text = "%.7g" % self.minVal
else:
- text = '%.2e' % self.minVal
+ text = "%.2e" % self.minVal
tooltip = repr(self.minVal)
self._minLabel.setText(text)
self._minLabel.setToolTip(tooltip)
if self.maxVal is None:
- text, tooltip = '', ''
+ text, tooltip = "", ""
else:
if self.maxVal == 0 or 0 <= numpy.log10(abs(self.maxVal)) < 7:
- text = '%.7g' % self.maxVal
+ text = "%.7g" % self.maxVal
else:
- text = '%.2e' % self.maxVal
+ text = "%.2e" % self.maxVal
tooltip = repr(self.maxVal)
self._maxLabel.setText(text)
@@ -562,7 +553,7 @@ class _ColorScale(qt.QWidget):
if colormap is None:
return
- indices = numpy.linspace(0., 1., self._NB_CONTROL_POINTS)
+ indices = numpy.linspace(0.0, 1.0, self._NB_CONTROL_POINTS)
colors = colormap.getNColors(nbColors=self._NB_CONTROL_POINTS)
self._gradient = qt.QLinearGradient(0, 1, 0, 0)
self._gradient.setCoordinateMode(qt.QGradient.StretchToDeviceMode)
@@ -575,30 +566,39 @@ class _ColorScale(qt.QWidget):
painter = qt.QPainter(self)
if self.getColormap() is not None:
painter.setBrush(self._gradient)
- penColor = self.palette().color(qt.QPalette.Active,
- qt.QPalette.WindowText)
+ penColor = self.palette().color(qt.QPalette.Active, qt.QPalette.WindowText)
else:
- penColor = self.palette().color(qt.QPalette.Disabled,
- qt.QPalette.WindowText)
+ penColor = self.palette().color(
+ qt.QPalette.Disabled, qt.QPalette.WindowText
+ )
painter.setPen(penColor)
- painter.drawRect(qt.QRect(
- 0,
- self.margin,
- self.width() - 1,
- self.height() - 2 * self.margin - 1))
+ painter.drawRect(
+ qt.QRect(
+ 0, self.margin, self.width() - 1, self.height() - 2 * self.margin - 1
+ )
+ )
def mouseMoveEvent(self, event):
- tooltip = str(self.getValueFromRelativePosition(
- self._getRelativePosition(event.y())))
- qt.QToolTip.showText(event.globalPos(), tooltip, self)
+ tooltip = str(
+ self.getValueFromRelativePosition(
+ self._getRelativePosition(qt.getMouseEventPosition(event)[1])
+ )
+ )
+ if qt.BINDING == "PyQt5":
+ position = event.globalPos()
+ else: # Qt6
+ position = event.globalPosition().toPoint()
+ qt.QToolTip.showText(position, tooltip, self)
super(_ColorScale, self).mouseMoveEvent(event)
def _getRelativePosition(self, yPixel):
- """yPixel : pixel position into _ColorScale widget reference
- """
+ """yPixel : pixel position into _ColorScale widget reference"""
# widgets are bottom-top referencial but we display in top-bottom referential
- return 1. - (yPixel - self.margin) / float(self.height() - 2 * self.margin)
+ height = float(self.height() - 2 * self.margin)
+ if height == 0:
+ return 0.0
+ return 1.0 - (yPixel - self.margin) / height
def getValueFromRelativePosition(self, value):
"""Return the value in the colorMap from a relative position in the
@@ -611,12 +611,15 @@ class _ColorScale(qt.QWidget):
if colormap is None:
return
- value = numpy.clip(value, 0., 1.)
+ value = numpy.clip(value, 0.0, 1.0)
normalizer = colormap._getNormalizer()
- normMin, normMax = normalizer.apply([self.vmin, self.vmax], self.vmin, self.vmax)
+ normMin, normMax = normalizer.apply(
+ [self.vmin, self.vmax], self.vmin, self.vmax
+ )
return normalizer.revert(
- normMin + (normMax - normMin) * value, self.vmin, self.vmax)
+ normMin + (normMax - normMin) * value, self.vmin, self.vmax
+ )
def setMargin(self, margin):
"""Define the margin to fit with a TickBar object.
@@ -652,6 +655,7 @@ class _TickBar(qt.QWidget):
number of ticks from the tick density.
:param int margin: margin to set on the top and bottom
"""
+
_WIDTH_DISP_VAL = 45
"""widget width when displayed with ticks labels"""
_WIDTH_NO_DISP_VAL = 10
@@ -663,8 +667,16 @@ class _TickBar(qt.QWidget):
DEFAULT_TICK_DENSITY = 0.015
- def __init__(self, vmin, vmax, normalizer, parent=None, displayValues=True,
- nticks=None, margin=5):
+ def __init__(
+ self,
+ vmin,
+ vmax,
+ normalizer,
+ parent=None,
+ displayValues=True,
+ nticks=None,
+ margin=5,
+ ):
super(_TickBar, self).__init__(parent)
self.margin = margin
self._nticks = None
@@ -723,7 +735,7 @@ class _TickBar(qt.QWidget):
(nticks=None) then you can specify a ticks density to be displayed.
"""
if density < 0.0:
- raise ValueError('Density should be a positive value')
+ raise ValueError("Density should be a positive value")
self.ticksDensity = density
def computeTicks(self):
@@ -753,14 +765,16 @@ class _TickBar(qt.QWidget):
def _computeTicksLog(self, nticks):
logMin = numpy.log10(self._vmin)
logMax = numpy.log10(self._vmax)
- lowBound, highBound, spacing, self._nfrac = ticklayout.niceNumbersForLog10(logMin,
- logMax,
- nticks)
- self.ticks = numpy.power(10., numpy.arange(lowBound, highBound, spacing))
+ lowBound, highBound, spacing, self._nfrac = ticklayout.niceNumbersForLog10(
+ logMin, logMax, nticks
+ )
+ self.ticks = numpy.power(10.0, numpy.arange(lowBound, highBound, spacing))
if spacing == 1:
- self.subTicks = ticklayout.computeLogSubTicks(ticks=self.ticks,
- lowBound=numpy.power(10., lowBound),
- highBound=numpy.power(10., highBound))
+ self.subTicks = ticklayout.computeLogSubTicks(
+ ticks=self.ticks,
+ lowBound=numpy.power(10.0, lowBound),
+ highBound=numpy.power(10.0, highBound),
+ )
else:
self.subTicks = []
@@ -769,9 +783,9 @@ class _TickBar(qt.QWidget):
self.computeTicks()
def _computeTicksLin(self, nticks):
- _min, _max, _spacing, self._nfrac = ticklayout.niceNumbers(self._vmin,
- self._vmax,
- nticks)
+ _min, _max, _spacing, self._nfrac = ticklayout.niceNumbers(
+ self._vmin, self._vmax, nticks
+ )
self.ticks = numpy.arange(_min, _max, _spacing)
self.subTicks = []
@@ -794,19 +808,18 @@ class _TickBar(qt.QWidget):
self._paintTick(val, painter, majorTick=False)
def _getRelativePosition(self, val):
- """Return the relative position of val according to min and max value
- """
+ """Return the relative position of val according to min and max value"""
if self._normalizer is None:
- return 0.
+ return 0.0
normMin, normMax, normVal = self._normalizer.apply(
- [self._vmin, self._vmax, val],
- self._vmin,
- self._vmax)
+ [self._vmin, self._vmax, val], self._vmin, self._vmax
+ )
if normMin == normMax:
- return 0.
- else:
- return 1. - (normVal - normMin) / (normMax - normMin)
+ return 0.0
+ if not numpy.isfinite(normVal):
+ return 0.0
+ return 1.0 - (normVal - normMin) / (normMax - normMin)
def _paintTick(self, val, painter, majorTick=True):
"""
@@ -822,14 +835,14 @@ class _TickBar(qt.QWidget):
if majorTick is False:
lineWidth /= 2
- painter.drawLine(qt.QLine(int(self.width() - lineWidth),
- height,
- self.width(),
- height))
+ painter.drawLine(
+ qt.QLine(int(self.width() - lineWidth), height, self.width(), height)
+ )
if self.displayValues and majorTick is True:
- painter.drawText(qt.QPoint(0, int(height + fm.height() / 2)),
- self.form.format(val))
+ painter.drawText(
+ qt.QPoint(0, int(height + fm.height() / 2)), self.form.format(val)
+ )
def setDisplayType(self, disType):
"""Set the type of display we want to set for ticks labels
@@ -842,8 +855,10 @@ class _TickBar(qt.QWidget):
- 'e' for scientific display
- None to let the _TickBar guess the best display for this kind of data.
"""
- if disType not in (None, 'std', 'e'):
- raise ValueError("display type not recognized, value should be in (None, 'std', 'e'")
+ if disType not in (None, "std", "e"):
+ raise ValueError(
+ "display type not recognized, value should be in (None, 'std', 'e'"
+ )
self._forcedDisplayType = disType
def _getStandardFormat(self):
@@ -852,12 +867,14 @@ class _TickBar(qt.QWidget):
def _getFormat(self, font):
if self._forcedDisplayType is None:
return self._guessType(font)
- elif self._forcedDisplayType == 'std':
+ elif self._forcedDisplayType == "std":
return self._getStandardFormat()
- elif self._forcedDisplayType == 'e':
+ elif self._forcedDisplayType == "e":
return self._getScientificForm()
else:
- err = 'Forced type for display %s is not recognized' % self._forcedDisplayType
+ err = (
+ "Forced type for display %s is not recognized" % self._forcedDisplayType
+ )
raise ValueError(err)
def _getScientificForm(self):
diff --git a/src/silx/gui/plot/Colormap.py b/src/silx/gui/plot/Colormap.py
deleted file mode 100644
index 22fea7f..0000000
--- a/src/silx/gui/plot/Colormap.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Deprecated module providing the Colormap object
-"""
-
-__authors__ = ["T. Vincent", "H.Payno"]
-__license__ = "MIT"
-__date__ = "27/11/2020"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.Colormap",
- reason="moved",
- replacement="silx.gui.colors.Colormap",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..colors import * # noqa
diff --git a/src/silx/gui/plot/ColormapDialog.py b/src/silx/gui/plot/ColormapDialog.py
deleted file mode 100644
index 7c66cb8..0000000
--- a/src/silx/gui/plot/ColormapDialog.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Deprecated module providing ColormapDialog."""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent", "H.Payno"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.ColormapDialog",
- reason="moved",
- replacement="silx.gui.dialog.ColormapDialog",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..dialog.ColormapDialog import * # noqa
diff --git a/src/silx/gui/plot/Colors.py b/src/silx/gui/plot/Colors.py
deleted file mode 100644
index 277e104..0000000
--- a/src/silx/gui/plot/Colors.py
+++ /dev/null
@@ -1,90 +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.
-#
-# ###########################################################################*/
-"""Color conversion function, color dictionary and colormap tools."""
-
-from __future__ import absolute_import
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "14/06/2018"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.Colors",
- reason="moved",
- replacement="silx.gui.colors",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..colors import * # noqa
-
-
-@silx.utils.deprecation.deprecated(replacement='silx.gui.colors.Colormap.applyColormap')
-def applyColormapToData(data,
- name='gray',
- normalization='linear',
- autoscale=True,
- vmin=0.,
- vmax=1.,
- colors=None):
- """Apply a colormap to the data and returns the RGBA image
-
- This supports data of any dimensions (not only of dimension 2).
- The returned array will have one more dimension (with 4 entries)
- than the input data to store the RGBA channels
- corresponding to each bin in the array.
-
- :param numpy.ndarray data: The data to convert.
- :param str name: Name of the colormap (default: 'gray').
- :param str normalization: Colormap mapping: 'linear' or 'log'.
- :param bool autoscale: Whether to use data min/max (True, default)
- or [vmin, vmax] range (False).
- :param float vmin: The minimum value of the range to use if
- 'autoscale' is False.
- :param float vmax: The maximum value of the range to use if
- 'autoscale' is False.
- :param numpy.ndarray colors: Only used if name is None.
- Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
- :return: The computed RGBA image
- :rtype: numpy.ndarray of uint8
- """
- colormap = Colormap(name=name,
- normalization=normalization,
- vmin=vmin,
- vmax=vmax,
- colors=colors)
- return colormap.applyToData(data)
-
-
-@silx.utils.deprecation.deprecated(replacement='silx.gui.colors.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/src/silx/gui/plot/CompareImages.py b/src/silx/gui/plot/CompareImages.py
index 857fc79..3823ae2 100644
--- a/src/silx/gui/plot/CompareImages.py
+++ b/src/silx/gui/plot/CompareImages.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
@@ -30,505 +29,30 @@ __license__ = "MIT"
__date__ = "23/07/2018"
-import enum
import logging
import numpy
-import weakref
-import collections
import math
import silx.image.bilinear
from silx.gui import qt
from silx.gui import plot
-from silx.gui import icons
from silx.gui.colors import Colormap
from silx.gui.plot import tools
+from silx.utils.deprecation import deprecated_warning
from silx.utils.weakref import WeakMethodProxy
+from silx.gui.plot.items import Scatter
+from silx.math.colormap import normalize
-_logger = logging.getLogger(__name__)
-
-from silx.opencl import ocl
-if ocl is not None:
- try:
- from silx.opencl import sift
- except ImportError:
- # sift module is not available (e.g., in official Debian packages)
- sift = None
-else: # No OpenCL device or no pyopencl
- sift = None
-
-
-@enum.unique
-class VisualizationMode(enum.Enum):
- """Enum for each visualization mode available."""
- ONLY_A = 'a'
- ONLY_B = 'b'
- VERTICAL_LINE = 'vline'
- HORIZONTAL_LINE = 'hline'
- COMPOSITE_RED_BLUE_GRAY = "rbgchannel"
- COMPOSITE_RED_BLUE_GRAY_NEG = "rbgnegchannel"
- COMPOSITE_A_MINUS_B = "aminusb"
-
-
-@enum.unique
-class AlignmentMode(enum.Enum):
- """Enum for each alignment mode available."""
- ORIGIN = 'origin'
- CENTER = 'center'
- STRETCH = 'stretch'
- AUTO = 'auto'
-
-
-AffineTransformation = collections.namedtuple("AffineTransformation",
- ["tx", "ty", "sx", "sy", "rot"])
-"""Contains a 2D affine transformation: translation, scale and rotation"""
-
-
-class CompareImagesToolBar(qt.QToolBar):
- """ToolBar containing specific tools to custom the configuration of a
- :class:`CompareImages` widget
-
- Use :meth:`setCompareWidget` to connect this toolbar to a specific
- :class:`CompareImages` widget.
-
- :param Union[qt.QWidget,None] parent: Parent of this widget.
- """
- def __init__(self, parent=None):
- qt.QToolBar.__init__(self, parent)
-
- self.__compareWidget = None
-
- menu = qt.QMenu(self)
- self.__visualizationToolButton = qt.QToolButton(self)
- self.__visualizationToolButton.setMenu(menu)
- self.__visualizationToolButton.setPopupMode(qt.QToolButton.InstantPopup)
- self.addWidget(self.__visualizationToolButton)
- self.__visualizationGroup = qt.QActionGroup(self)
- self.__visualizationGroup.setExclusive(True)
- self.__visualizationGroup.triggered.connect(self.__visualizationModeChanged)
-
- icon = icons.getQIcon("compare-mode-a")
- action = qt.QAction(icon, "Display the first image only", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_A))
- action.setProperty("mode", VisualizationMode.ONLY_A)
- menu.addAction(action)
- self.__aModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-b")
- action = qt.QAction(icon, "Display the second image only", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_B))
- action.setProperty("mode", VisualizationMode.ONLY_B)
- menu.addAction(action)
- self.__bModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-vline")
- action = qt.QAction(icon, "Vertical compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_V))
- action.setProperty("mode", VisualizationMode.VERTICAL_LINE)
- menu.addAction(action)
- self.__vlineModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-hline")
- action = qt.QAction(icon, "Horizontal compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_H))
- action.setProperty("mode", VisualizationMode.HORIZONTAL_LINE)
- menu.addAction(action)
- self.__hlineModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-rb-channel")
- action = qt.QAction(icon, "Blue/red compare mode (additive mode)", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_C))
- action.setProperty("mode", VisualizationMode.COMPOSITE_RED_BLUE_GRAY)
- menu.addAction(action)
- self.__brChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-rbneg-channel")
- action = qt.QAction(icon, "Yellow/cyan compare mode (subtractive mode)", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
- action.setProperty("mode", VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG)
- menu.addAction(action)
- self.__ycChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-a-minus-b")
- action = qt.QAction(icon, "Raw A minus B compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
- action.setProperty("mode", VisualizationMode.COMPOSITE_A_MINUS_B)
- menu.addAction(action)
- self.__ycChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- menu = qt.QMenu(self)
- self.__alignmentToolButton = qt.QToolButton(self)
- self.__alignmentToolButton.setMenu(menu)
- self.__alignmentToolButton.setPopupMode(qt.QToolButton.InstantPopup)
- self.addWidget(self.__alignmentToolButton)
- self.__alignmentGroup = qt.QActionGroup(self)
- self.__alignmentGroup.setExclusive(True)
- self.__alignmentGroup.triggered.connect(self.__alignmentModeChanged)
-
- icon = icons.getQIcon("compare-align-origin")
- action = qt.QAction(icon, "Align images on their upper-left pixel", self)
- action.setProperty("mode", AlignmentMode.ORIGIN)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__originAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-center")
- action = qt.QAction(icon, "Center images", self)
- action.setProperty("mode", AlignmentMode.CENTER)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__centerAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-stretch")
- action = qt.QAction(icon, "Stretch the second image on the first one", self)
- action.setProperty("mode", AlignmentMode.STRETCH)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__stretchAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-auto")
- action = qt.QAction(icon, "Auto-alignment of the second image", self)
- action.setProperty("mode", AlignmentMode.AUTO)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__autoAlignAction = action
- menu.addAction(action)
- if sift is None:
- action.setEnabled(False)
- action.setToolTip("Sift module is not available")
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-keypoints")
- action = qt.QAction(icon, "Display/hide alignment keypoints", self)
- action.setCheckable(True)
- action.triggered.connect(self.__keypointVisibilityChanged)
- self.addAction(action)
- self.__displayKeypoints = action
-
- def setCompareWidget(self, widget):
- """
- Connect this tool bar to a specific :class:`CompareImages` widget.
-
- :param Union[None,CompareImages] widget: The widget to connect with.
- """
- compareWidget = self.getCompareWidget()
- if compareWidget is not None:
- compareWidget.sigConfigurationChanged.disconnect(self.__updateSelectedActions)
- compareWidget = widget
- if compareWidget is None:
- self.__compareWidget = None
- else:
- self.__compareWidget = weakref.ref(compareWidget)
- if compareWidget is not None:
- widget.sigConfigurationChanged.connect(self.__updateSelectedActions)
- self.__updateSelectedActions()
-
- def getCompareWidget(self):
- """Returns the connected widget.
-
- :rtype: CompareImages
- """
- if self.__compareWidget is None:
- return None
- else:
- return self.__compareWidget()
-
- def __updateSelectedActions(self):
- """
- Update the state of this tool bar according to the state of the
- connected :class:`CompareImages` widget.
- """
- widget = self.getCompareWidget()
- if widget is None:
- return
-
- mode = widget.getVisualizationMode()
- action = None
- for a in self.__visualizationGroup.actions():
- actionMode = a.property("mode")
- if mode == actionMode:
- action = a
- break
- old = self.__visualizationGroup.blockSignals(True)
- if action is not None:
- # Check this action
- action.setChecked(True)
- else:
- action = self.__visualizationGroup.checkedAction()
- if action is not None:
- # Uncheck this action
- action.setChecked(False)
- self.__updateVisualizationMenu()
- self.__visualizationGroup.blockSignals(old)
-
- mode = widget.getAlignmentMode()
- action = None
- for a in self.__alignmentGroup.actions():
- actionMode = a.property("mode")
- if mode == actionMode:
- action = a
- break
- old = self.__alignmentGroup.blockSignals(True)
- if action is not None:
- # Check this action
- action.setChecked(True)
- else:
- action = self.__alignmentGroup.checkedAction()
- if action is not None:
- # Uncheck this action
- action.setChecked(False)
- self.__updateAlignmentMenu()
- self.__alignmentGroup.blockSignals(old)
-
- def __visualizationModeChanged(self, selectedAction):
- """Called when user requesting changes of the visualization mode.
- """
- self.__updateVisualizationMenu()
- widget = self.getCompareWidget()
- if widget is not None:
- mode = selectedAction.property("mode")
- widget.setVisualizationMode(mode)
-
- def __updateVisualizationMenu(self):
- """Update the state of the action containing visualization menu.
- """
- selectedAction = self.__visualizationGroup.checkedAction()
- if selectedAction is not None:
- self.__visualizationToolButton.setText(selectedAction.text())
- self.__visualizationToolButton.setIcon(selectedAction.icon())
- self.__visualizationToolButton.setToolTip(selectedAction.toolTip())
- else:
- self.__visualizationToolButton.setText("")
- self.__visualizationToolButton.setIcon(qt.QIcon())
- self.__visualizationToolButton.setToolTip("")
-
- def __alignmentModeChanged(self, selectedAction):
- """Called when user requesting changes of the alignment mode.
- """
- self.__updateAlignmentMenu()
- widget = self.getCompareWidget()
- if widget is not None:
- mode = selectedAction.property("mode")
- widget.setAlignmentMode(mode)
-
- def __updateAlignmentMenu(self):
- """Update the state of the action containing alignment menu.
- """
- selectedAction = self.__alignmentGroup.checkedAction()
- if selectedAction is not None:
- self.__alignmentToolButton.setText(selectedAction.text())
- self.__alignmentToolButton.setIcon(selectedAction.icon())
- self.__alignmentToolButton.setToolTip(selectedAction.toolTip())
- else:
- self.__alignmentToolButton.setText("")
- self.__alignmentToolButton.setIcon(qt.QIcon())
- self.__alignmentToolButton.setToolTip("")
-
- def __keypointVisibilityChanged(self):
- """Called when action managing keypoints visibility changes"""
- widget = self.getCompareWidget()
- if widget is not None:
- keypointsVisible = self.__displayKeypoints.isChecked()
- widget.setKeypointsVisible(keypointsVisible)
+from .tools.compare.core import sift
+from .tools.compare.core import VisualizationMode
+from .tools.compare.core import AlignmentMode
+from .tools.compare.core import AffineTransformation
+from .tools.compare.toolbar import CompareImagesToolBar
+from .tools.compare.statusbar import CompareImagesStatusBar
+from .tools.compare.core import _CompareImageItem
-class CompareImagesStatusBar(qt.QStatusBar):
- """StatusBar containing specific information contained in a
- :class:`CompareImages` widget
-
- Use :meth:`setCompareWidget` to connect this toolbar to a specific
- :class:`CompareImages` widget.
-
- :param Union[qt.QWidget,None] parent: Parent of this widget.
- """
- def __init__(self, parent=None):
- qt.QStatusBar.__init__(self, parent)
- self.setSizeGripEnabled(False)
- self.layout().setSpacing(0)
- self.__compareWidget = None
- self._label1 = qt.QLabel(self)
- self._label1.setFrameShape(qt.QFrame.WinPanel)
- self._label1.setFrameShadow(qt.QFrame.Sunken)
- self._label2 = qt.QLabel(self)
- self._label2.setFrameShape(qt.QFrame.WinPanel)
- self._label2.setFrameShadow(qt.QFrame.Sunken)
- self._transform = qt.QLabel(self)
- self._transform.setFrameShape(qt.QFrame.WinPanel)
- self._transform.setFrameShadow(qt.QFrame.Sunken)
- self.addWidget(self._label1)
- self.addWidget(self._label2)
- self.addWidget(self._transform)
- self._pos = None
- self._updateStatusBar()
-
- def setCompareWidget(self, widget):
- """
- Connect this tool bar to a specific :class:`CompareImages` widget.
-
- :param Union[None,CompareImages] widget: The widget to connect with.
- """
- compareWidget = self.getCompareWidget()
- if compareWidget is not None:
- compareWidget.getPlot().sigPlotSignal.disconnect(self.__plotSignalReceived)
- compareWidget.sigConfigurationChanged.disconnect(self.__dataChanged)
- compareWidget = widget
- if compareWidget is None:
- self.__compareWidget = None
- else:
- self.__compareWidget = weakref.ref(compareWidget)
- if compareWidget is not None:
- compareWidget.getPlot().sigPlotSignal.connect(self.__plotSignalReceived)
- compareWidget.sigConfigurationChanged.connect(self.__dataChanged)
-
- def getCompareWidget(self):
- """Returns the connected widget.
-
- :rtype: CompareImages
- """
- if self.__compareWidget is None:
- return None
- else:
- return self.__compareWidget()
-
- def __plotSignalReceived(self, event):
- """Called when old style signals at emmited from the plot."""
- if event["event"] == "mouseMoved":
- x, y = event["x"], event["y"]
- self.__mouseMoved(x, y)
-
- def __mouseMoved(self, x, y):
- """Called when mouse move over the plot."""
- self._pos = x, y
- self._updateStatusBar()
-
- def __dataChanged(self):
- """Called when internal data from the connected widget changes."""
- self._updateStatusBar()
-
- def _formatData(self, data):
- """Format pixel of an image.
-
- It supports intensity, RGB, and RGBA.
-
- :param Union[int,float,numpy.ndarray,str]: Value of a pixel
- :rtype: str
- """
- if data is None:
- return "No data"
- if isinstance(data, (int, numpy.integer)):
- return "%d" % data
- if isinstance(data, (float, numpy.floating)):
- return "%f" % data
- if isinstance(data, numpy.ndarray):
- # RGBA value
- if data.shape == (3,):
- return "R:%d G:%d B:%d" % (data[0], data[1], data[2])
- elif data.shape == (4,):
- return "R:%d G:%d B:%d A:%d" % (data[0], data[1], data[2], data[3])
- _logger.debug("Unsupported data format %s. Cast it to string.", type(data))
- return str(data)
-
- def _updateStatusBar(self):
- """Update the content of the status bar"""
- widget = self.getCompareWidget()
- if widget is None:
- self._label1.setText("Image1: NA")
- self._label2.setText("Image2: NA")
- self._transform.setVisible(False)
- else:
- transform = widget.getTransformation()
- self._transform.setVisible(transform is not None)
- if transform is not None:
- has_notable_translation = not numpy.isclose(transform.tx, 0.0, atol=0.01) \
- or not numpy.isclose(transform.ty, 0.0, atol=0.01)
- has_notable_scale = not numpy.isclose(transform.sx, 1.0, atol=0.01) \
- or not numpy.isclose(transform.sy, 1.0, atol=0.01)
- has_notable_rotation = not numpy.isclose(transform.rot, 0.0, atol=0.01)
-
- strings = []
- if has_notable_translation:
- strings.append("Translation")
- if has_notable_scale:
- strings.append("Scale")
- if has_notable_rotation:
- strings.append("Rotation")
- if strings == []:
- has_translation = not numpy.isclose(transform.tx, 0.0) \
- or not numpy.isclose(transform.ty, 0.0)
- has_scale = not numpy.isclose(transform.sx, 1.0) \
- or not numpy.isclose(transform.sy, 1.0)
- has_rotation = not numpy.isclose(transform.rot, 0.0)
- if has_translation or has_scale or has_rotation:
- text = "No big changes"
- else:
- text = "No changes"
- else:
- text = "+".join(strings)
- self._transform.setText("Align: " + text)
-
- strings = []
- if not numpy.isclose(transform.ty, 0.0):
- strings.append("Translation x: %0.3fpx" % transform.tx)
- if not numpy.isclose(transform.ty, 0.0):
- strings.append("Translation y: %0.3fpx" % transform.ty)
- if not numpy.isclose(transform.sx, 1.0):
- strings.append("Scale x: %0.3f" % transform.sx)
- if not numpy.isclose(transform.sy, 1.0):
- strings.append("Scale y: %0.3f" % transform.sy)
- if not numpy.isclose(transform.rot, 0.0):
- strings.append("Rotation: %0.3fdeg" % (transform.rot * 180 / numpy.pi))
- if strings == []:
- text = "No transformation"
- else:
- text = "\n".join(strings)
- self._transform.setToolTip(text)
-
- if self._pos is None:
- self._label1.setText("Image1: NA")
- self._label2.setText("Image2: NA")
- else:
- data1, data2 = widget.getRawPixelData(self._pos[0], self._pos[1])
- if isinstance(data1, str):
- self._label1.setToolTip(data1)
- text1 = "NA"
- else:
- self._label1.setToolTip("")
- text1 = self._formatData(data1)
- if isinstance(data2, str):
- self._label2.setToolTip(data2)
- text2 = "NA"
- else:
- self._label2.setToolTip("")
- text2 = self._formatData(data2)
- self._label1.setText("Image1: %s" % text1)
- self._label2.setText("Image2: %s" % text2)
+_logger = logging.getLogger(__name__)
class CompareImages(qt.QMainWindow):
@@ -551,22 +75,28 @@ class CompareImages(qt.QMainWindow):
sigConfigurationChanged = qt.Signal()
"""Emitted when the configuration of the widget (visualization mode,
- alignement mode...) have changed."""
+ alignment mode...) have changed."""
def __init__(self, parent=None, backend=None):
qt.QMainWindow.__init__(self, parent)
self._resetZoomActive = True
self._colormap = Colormap()
"""Colormap shared by all modes, except the compose images (rgb image)"""
- self._colormapKeyPoints = Colormap('spring')
+ self._colormapKeyPoints = Colormap("spring")
"""Colormap used for sift keypoints"""
+ self._colormap.sigChanged.connect(self.__colormapChanged)
+
if parent is None:
- self.setWindowTitle('Compare images')
+ self.setWindowTitle("Compare images")
else:
self.setWindowFlags(qt.Qt.Widget)
self.__transformation = None
+ self.__item = _CompareImageItem()
+ self.__item.setName("_virtual")
+ self.__item.setColormap(self._colormap)
+
self.__raw1 = None
self.__raw2 = None
self.__data1 = None
@@ -575,35 +105,44 @@ class CompareImages(qt.QMainWindow):
self.__plot = plot.PlotWidget(parent=self, backend=backend)
self.__plot.setDefaultColormap(self._colormap)
- self.__plot.getXAxis().setLabel('Columns')
- self.__plot.getYAxis().setLabel('Rows')
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
+ self.__plot.getXAxis().setLabel("Columns")
+ self.__plot.getYAxis().setLabel("Rows")
+ if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
self.__plot.getYAxis().setInverted(True)
+ self.__plot.addItem(self.__item)
+ self.__plot.setActiveImage(self.__item)
self.__plot.setKeepDataAspectRatio(True)
self.__plot.sigPlotSignal.connect(self.__plotSlot)
self.__plot.setAxesDisplayed(False)
+ self.__scatter = Scatter()
+ self.__scatter.setZValue(1)
+ self.__scatter.setColormap(self._colormapKeyPoints)
+ self.__plot.addItem(self.__scatter)
+
self.setCentralWidget(self.__plot)
legend = VisualizationMode.VERTICAL_LINE.name
self.__plot.addXMarker(
- 0,
- legend=legend,
- text='',
- draggable=True,
- color='blue',
- constraint=WeakMethodProxy(self.__separatorConstraint))
+ 0,
+ legend=legend,
+ text="",
+ draggable=True,
+ color="blue",
+ constraint=WeakMethodProxy(self.__separatorConstraint),
+ )
self.__vline = self.__plot._getMarker(legend)
legend = VisualizationMode.HORIZONTAL_LINE.name
self.__plot.addYMarker(
- 0,
- legend=legend,
- text='',
- draggable=True,
- color='blue',
- constraint=WeakMethodProxy(self.__separatorConstraint))
+ 0,
+ legend=legend,
+ text="",
+ draggable=True,
+ color="blue",
+ constraint=WeakMethodProxy(self.__separatorConstraint),
+ )
self.__hline = self.__plot._getMarker(legend)
# default values
@@ -631,6 +170,26 @@ class CompareImages(qt.QMainWindow):
if self._statusBar is not None:
self.setStatusBar(self._statusBar)
+ def __getSealedColormap(self):
+ vrange = self._colormap.getColormapRange(
+ self.__item.getColormappedData(copy=False)
+ )
+ sealed = self._colormap.copy()
+ sealed.setVRange(*vrange)
+ return sealed
+
+ def __colormapChanged(self):
+ sealed = self.__getSealedColormap()
+ if self.__image1 is not None:
+ if self.__getImageMode(self.__image1.getData(copy=False)) == "intensity":
+ self.__image1.setColormap(sealed)
+ if self.__image2 is not None:
+ if self.__getImageMode(self.__image2.getData(copy=False)) == "intensity":
+ self.__image2.setColormap(sealed)
+
+ if "COMPOSITE" in self.__visualizationMode.name:
+ self.__updateData()
+
def _createStatusBar(self, plot):
self._statusBar = CompareImagesStatusBar(self)
self._statusBar.setCompareWidget(self)
@@ -645,6 +204,9 @@ class CompareImages(qt.QMainWindow):
toolBar.setCompareWidget(self)
self._compareToolBar = toolBar
+ def _getVirtualPlotItem(self):
+ return self.__item
+
def getPlot(self):
"""Returns the plot which is used to display the images.
@@ -677,10 +239,15 @@ class CompareImages(qt.QMainWindow):
It also could be a string containing information is some cases.
:rtype: Tuple(Union[int,float,numpy.ndarray,str],Union[int,float,numpy.ndarray,str])
"""
- data2 = None
alignmentMode = self.__alignmentMode
raw1, raw2 = self.__raw1, self.__raw2
- if alignmentMode == AlignmentMode.ORIGIN:
+
+ if raw1 is None or raw2 is None:
+ x1 = x
+ y1 = y
+ x2 = x
+ y2 = y
+ elif alignmentMode == AlignmentMode.ORIGIN:
x1 = x
y1 = y
x2 = x
@@ -701,22 +268,29 @@ class CompareImages(qt.QMainWindow):
x1 = x
y1 = y
# Not implemented
- data2 = "Not implemented with sift"
+ x2 = -1
+ y2 = -1
else:
- assert(False)
+ assert False
x1, y1 = int(x1), int(y1)
- if raw1 is None or y1 < 0 or y1 >= raw1.shape[0] or x1 < 0 or x1 >= raw1.shape[1]:
- data1 = None
+ x2, y2 = int(x2), int(y2)
+
+ if raw1 is None:
+ data1 = "No image A"
+ elif y1 < 0 or y1 >= raw1.shape[0] or x1 < 0 or x1 >= raw1.shape[1]:
+ data1 = ""
else:
data1 = raw1[y1, x1]
- if data2 is None:
- x2, y2 = int(x2), int(y2)
- if raw2 is None or y2 < 0 or y2 >= raw2.shape[0] or x2 < 0 or x2 >= raw2.shape[1]:
- data2 = None
- else:
- data2 = raw2[y2, x2]
+ if raw2 is None:
+ data2 = "No image B"
+ elif alignmentMode == AlignmentMode.AUTO:
+ data2 = "Not implemented with sift"
+ elif y2 < 0 or y2 >= raw2.shape[0] or x2 < 0 or x2 >= raw2.shape[1]:
+ data2 = None
+ else:
+ data2 = raw2[y2, x2]
return data1, data2
@@ -727,20 +301,31 @@ class CompareImages(qt.QMainWindow):
"""
if self.__visualizationMode == mode:
return
- previousMode = self.getVisualizationMode()
self.__visualizationMode = mode
- mode = self.getVisualizationMode()
+ self.__item.setVizualisationMode(mode)
self.__vline.setVisible(mode == VisualizationMode.VERTICAL_LINE)
self.__hline.setVisible(mode == VisualizationMode.HORIZONTAL_LINE)
- visModeRawDisplay = (VisualizationMode.ONLY_A,
- VisualizationMode.ONLY_B,
- VisualizationMode.VERTICAL_LINE,
- VisualizationMode.HORIZONTAL_LINE)
- updateColormap = not(previousMode in visModeRawDisplay and
- mode in visModeRawDisplay)
- self.__updateData(updateColormap=updateColormap)
+ self.__updateData()
self.sigConfigurationChanged.emit()
+ def centerLines(self):
+ """Center the line used to compare the 2 images."""
+ if self.__image1 is None:
+ return
+ data_range = self.__plot.getDataRange()
+
+ if data_range[0] is not None:
+ cx = (data_range[0][0] + data_range[0][1]) * 0.5
+ else:
+ cx = 0
+ if data_range[1] is not None:
+ cy = (data_range[1][0] + data_range[1][1]) * 0.5
+ else:
+ cy = 0
+ self.__vline.setPosition(cx, cy)
+ self.__hline.setPosition(cx, cy)
+ self.__updateSeparators()
+
def getVisualizationMode(self):
"""Returns the current interaction mode."""
return self.__visualizationMode
@@ -753,13 +338,17 @@ class CompareImages(qt.QMainWindow):
if self.__alignmentMode == mode:
return
self.__alignmentMode = mode
- self.__updateData(updateColormap=False)
+ self.__updateData()
self.sigConfigurationChanged.emit()
def getAlignmentMode(self):
"""Returns the current selected alignemnt mode."""
return self.__alignmentMode
+ def getKeypointsVisible(self):
+ """Returns true if the keypoints are displayed"""
+ return self.__keypointsVisible
+
def setKeypointsVisible(self, isVisible):
"""Set keypoints visibility.
@@ -777,16 +366,16 @@ class CompareImages(qt.QMainWindow):
def __plotSlot(self, event):
"""Handle events from the plot"""
- if event['event'] in ('markerMoving', 'markerMoved'):
+ if event["event"] in ("markerMoving", "markerMoved"):
mode = self.getVisualizationMode()
legend = mode.name
- if event['label'] == legend:
+ if event["label"] == legend:
if mode == VisualizationMode.VERTICAL_LINE:
- value = int(float(str(event['xdata'])))
+ value = int(float(str(event["xdata"])))
elif mode == VisualizationMode.HORIZONTAL_LINE:
- value = int(float(str(event['ydata'])))
+ value = int(float(str(event["ydata"])))
else:
- assert(False)
+ assert False
if self.__previousSeparatorPosition != value:
self.__separatorMoved(value)
self.__previousSeparatorPosition = value
@@ -808,8 +397,7 @@ class CompareImages(qt.QMainWindow):
return x, y
def __updateSeparators(self):
- """Redraw images according to the current state of the separators.
- """
+ """Redraw images according to the current state of the separators."""
mode = self.getVisualizationMode()
if mode == VisualizationMode.VERTICAL_LINE:
pos = self.__vline.getXPosition()
@@ -821,7 +409,8 @@ class CompareImages(qt.QMainWindow):
self.__previousSeparatorPosition = pos
else:
self.__image1.setOrigin((0, 0))
- self.__image2.setOrigin((0, 0))
+ if self.__image2 is not None:
+ self.__image2.setOrigin((0, 0))
def __separatorMoved(self, pos):
"""Called when vertical or horizontal separators have moved.
@@ -841,8 +430,9 @@ class CompareImages(qt.QMainWindow):
data1 = self.__data1[:, 0:pos]
data2 = self.__data2[:, pos:]
self.__image1.setData(data1, copy=False)
- self.__image2.setData(data2, copy=False)
- self.__image2.setOrigin((pos, 0))
+ if self.__image2 is not None:
+ self.__image2.setData(data2, copy=False)
+ self.__image2.setOrigin((pos, 0))
elif mode == VisualizationMode.HORIZONTAL_LINE:
pos = int(pos)
if pos <= 0:
@@ -852,150 +442,209 @@ class CompareImages(qt.QMainWindow):
data1 = self.__data1[0:pos, :]
data2 = self.__data2[pos:, :]
self.__image1.setData(data1, copy=False)
- self.__image2.setData(data2, copy=False)
- self.__image2.setOrigin((0, pos))
+ if self.__image2 is not None:
+ self.__image2.setData(data2, copy=False)
+ self.__image2.setOrigin((0, pos))
else:
- assert(False)
+ assert False
- def setData(self, image1, image2, updateColormap=True):
+ def clear(self):
+ self.setData(None, None)
+
+ def setData(self, image1, image2, updateColormap="deprecated"):
"""Set images to compare.
Images can contains floating-point or integer values, or RGB and RGBA
values, but should have comparable intensities.
RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
+ of unsigned integer 8-bits or floating-points between 0.0 to 1.0.
:param numpy.ndarray image1: The first image
:param numpy.ndarray image2: The second image
"""
+ if updateColormap != "deprecated":
+ deprecated_warning(
+ "Argument", "setData's updateColormap argument", since_version="2.0.0"
+ )
+
self.__raw1 = image1
self.__raw2 = image2
- self.__updateData(updateColormap=updateColormap)
+ self.__updateData()
if self.isAutoResetZoom():
self.__plot.resetZoom()
- def setImage1(self, image1, updateColormap=True):
+ def setImage1(self, image1, updateColormap="deprecated"):
"""Set image1 to be compared.
Images can contains floating-point or integer values, or RGB and RGBA
values, but should have comparable intensities.
RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
+ of unsigned integer 8-bits or floating-points between 0.0 to 1.0.
:param numpy.ndarray image1: The first image
"""
+ if updateColormap != "deprecated":
+ deprecated_warning(
+ "Argument", "setImage1's updateColormap argument", since_version="2.0.0"
+ )
+
self.__raw1 = image1
- self.__updateData(updateColormap=updateColormap)
+ self.__updateData()
if self.isAutoResetZoom():
self.__plot.resetZoom()
- def setImage2(self, image2, updateColormap=True):
+ def setImage2(self, image2, updateColormap="deprecated"):
"""Set image2 to be compared.
Images can contains floating-point or integer values, or RGB and RGBA
values, but should have comparable intensities.
RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
+ of unsigned integer 8-bits or floating-points between 0.0 to 1.0.
:param numpy.ndarray image2: The second image
"""
+ if updateColormap != "deprecated":
+ deprecated_warning(
+ "Argument", "setImage2's updateColormap argument", since_version="2.0.0"
+ )
+
self.__raw2 = image2
- self.__updateData(updateColormap=updateColormap)
+ self.__updateData()
if self.isAutoResetZoom():
self.__plot.resetZoom()
def __updateKeyPoints(self):
- """Update the displayed keypoints using cached keypoints.
- """
- if self.__keypointsVisible:
+ """Update the displayed keypoints using cached keypoints."""
+ if self.__keypointsVisible and self.__matching_keypoints:
data = self.__matching_keypoints
else:
data = [], [], []
- self.__plot.addScatter(x=data[0],
- y=data[1],
- z=1,
- value=data[2],
- colormap=self._colormapKeyPoints,
- legend="keypoints")
-
- def __updateData(self, updateColormap):
+ self.__scatter.setData(x=data[0], y=data[1], value=data[2])
+
+ def __updateData(self):
"""Compute aligned image when the alignment mode changes.
This function cache input images which are used when
vertical/horizontal separators moves.
"""
raw1, raw2 = self.__raw1, self.__raw2
- if raw1 is None or raw2 is None:
- return
alignmentMode = self.getAlignmentMode()
self.__transformation = None
- if alignmentMode == AlignmentMode.ORIGIN:
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size, transparent=True)
- data2 = self.__createMarginImage(raw2, size, transparent=True)
- self.__matching_keypoints = [0.0], [0.0], [1.0]
- elif alignmentMode == AlignmentMode.CENTER:
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size, transparent=True, center=True)
- data2 = self.__createMarginImage(raw2, size, transparent=True, center=True)
- self.__matching_keypoints = ([data1.shape[1] // 2],
- [data1.shape[0] // 2],
- [1.0])
- elif alignmentMode == AlignmentMode.STRETCH:
- data1 = raw1
- data2 = self.__rescaleImage(raw2, data1.shape)
- self.__matching_keypoints = ([0, data1.shape[1], data1.shape[1], 0],
- [0, 0, data1.shape[0], data1.shape[0]],
- [1.0, 1.0, 1.0, 1.0])
- elif alignmentMode == AlignmentMode.AUTO:
- # TODO: sift implementation do not support RGBA images
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size)
- data2 = self.__createMarginImage(raw2, size)
- self.__matching_keypoints = [0.0], [0.0], [1.0]
- try:
- data1, data2 = self.__createSiftData(data1, data2)
- if data2 is None:
- raise ValueError("Unexpected None value")
- except Exception as e:
- # TODO: Display it on the GUI
- _logger.error(e)
- self.__setDefaultAlignmentMode()
- return
+ if raw1 is None or raw2 is None:
+ # No need to realign the 2 images
+ # But create a dummy image when there is None for simplification
+ if raw1 is None:
+ data1 = numpy.empty((0, 0))
+ else:
+ data1 = raw1
+ if raw2 is None:
+ data2 = numpy.empty((0, 0))
+ else:
+ data2 = raw2
+ self.__matching_keypoints = None
else:
- assert(False)
+ if alignmentMode == AlignmentMode.ORIGIN:
+ yy = max(raw1.shape[0], raw2.shape[0])
+ xx = max(raw1.shape[1], raw2.shape[1])
+ size = yy, xx
+ data1 = self.__createMarginImage(raw1, size, transparent=True)
+ data2 = self.__createMarginImage(raw2, size, transparent=True)
+ self.__matching_keypoints = [0.0], [0.0], [1.0]
+ elif alignmentMode == AlignmentMode.CENTER:
+ yy = max(raw1.shape[0], raw2.shape[0])
+ xx = max(raw1.shape[1], raw2.shape[1])
+ size = yy, xx
+ data1 = self.__createMarginImage(
+ raw1, size, transparent=True, center=True
+ )
+ data2 = self.__createMarginImage(
+ raw2, size, transparent=True, center=True
+ )
+ self.__matching_keypoints = (
+ [data1.shape[1] // 2],
+ [data1.shape[0] // 2],
+ [1.0],
+ )
+ elif alignmentMode == AlignmentMode.STRETCH:
+ data1 = raw1
+ data2 = self.__rescaleImage(raw2, data1.shape)
+ self.__matching_keypoints = (
+ [0, data1.shape[1], data1.shape[1], 0],
+ [0, 0, data1.shape[0], data1.shape[0]],
+ [1.0, 1.0, 1.0, 1.0],
+ )
+ elif alignmentMode == AlignmentMode.AUTO:
+ # TODO: sift implementation do not support RGBA images
+ yy = max(raw1.shape[0], raw2.shape[0])
+ xx = max(raw1.shape[1], raw2.shape[1])
+ size = yy, xx
+ data1 = self.__createMarginImage(raw1, size)
+ data2 = self.__createMarginImage(raw2, size)
+ self.__matching_keypoints = [0.0], [0.0], [1.0]
+ try:
+ data1, data2 = self.__createSiftData(data1, data2)
+ if data2 is None:
+ raise ValueError("Unexpected None value")
+ except Exception as e:
+ # TODO: Display it on the GUI
+ _logger.error(e)
+ self.__setDefaultAlignmentMode()
+ return
+ else:
+ assert False
+
+ self.__item.setImageData1(data1)
+ self.__item.setImageData2(data2)
mode = self.getVisualizationMode()
if mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
+ data1 = self.__composeRgbImage(data1, data2, mode)
+ data2 = None
elif mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
+ data1 = self.__composeRgbImage(data1, data2, mode)
+ data2 = None
elif mode == VisualizationMode.COMPOSITE_A_MINUS_B:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
+ data1 = self.__composeAMinusBImage(data1, data2)
+ data2 = None
elif mode == VisualizationMode.ONLY_A:
- data2 = numpy.empty((0, 0))
+ data2 = None
elif mode == VisualizationMode.ONLY_B:
data1 = numpy.empty((0, 0))
self.__data1, self.__data2 = data1, data2
- self.__plot.addImage(data1, z=0, legend="image1", resetzoom=False)
- self.__plot.addImage(data2, z=0, legend="image2", resetzoom=False)
+
+ colormap = self.__getSealedColormap()
+ mode1 = self.__getImageMode(self.__data1)
+ if mode1 == "intensity":
+ colormap1 = colormap
+ else:
+ colormap1 = None
+ self.__plot.addImage(
+ data1, z=0, legend="image1", resetzoom=False, colormap=colormap1
+ )
self.__image1 = self.__plot.getImage("image1")
- self.__image2 = self.__plot.getImage("image2")
+
+ if data2 is not None:
+ mode2 = self.__getImageMode(data2)
+ if mode2 == "intensity":
+ colormap2 = colormap
+ else:
+ colormap2 = None
+ self.__plot.addImage(
+ data2, z=0, legend="image2", resetzoom=False, colormap=colormap2
+ )
+ self.__image2 = self.__plot.getImage("image2")
+ self.__image2.setVisible(True)
+ else:
+ if self.__image2 is not None:
+ self.__image2.setVisible(False)
+ self.__image2 = None
+ self.__data2 = numpy.empty((0, 0))
self.__updateKeyPoints()
# Set the separator into the middle
@@ -1005,27 +654,6 @@ class CompareImages(qt.QMainWindow):
value = self.__data1.shape[0] // 2
self.__hline.setPosition(0, value)
self.__updateSeparators()
- if updateColormap:
- self.__updateColormap()
-
- def __updateColormap(self):
- # TODO: The colormap histogram will still be wrong
- mode1 = self.__getImageMode(self.__data1)
- mode2 = self.__getImageMode(self.__data2)
- if mode1 == "intensity" and mode1 == mode2:
- if self.__data1.size == 0:
- vmin = self.__data2.min()
- vmax = self.__data2.max()
- elif self.__data2.size == 0:
- vmin = self.__data1.min()
- vmax = self.__data1.max()
- else:
- vmin = min(self.__data1.min(), self.__data2.min())
- vmax = max(self.__data1.max(), self.__data2.max())
- colormap = self.getColormap()
- colormap.setVRange(vmin=vmin, vmax=vmax)
- self.__image1.setColormap(colormap)
- self.__image2.setColormap(colormap)
def __getImageMode(self, image):
"""Returns a value identifying the way the image is stored in the
@@ -1061,62 +689,117 @@ class CompareImages(qt.QMainWindow):
data[:, :, c] = self.__rescaleArray(image[:, :, c], shape)
return data
- def __composeImage(self, data1, data2, mode):
+ def __composeRgbImage(self, data1, data2, mode):
"""Returns an RBG image containing composition of data1 and data2 in 2
different channels
+ A data image of a size of 0 is considered as missing. This does not
+ interrupt the processing.
+
:param numpy.ndarray data1: First image
:param numpy.ndarray data1: Second image
:param VisualizationMode mode: Composition mode.
:rtype: numpy.ndarray
"""
- assert(data1.shape[0:2] == data2.shape[0:2])
- if mode == VisualizationMode.COMPOSITE_A_MINUS_B:
- # TODO: this calculation has no interest of generating a 'composed'
- # rgb image, this could be moved in an other function or doc
- # should be modified
- _type = data1.dtype
- result = data1.astype(numpy.float64) - data2.astype(numpy.float64)
- return result
- mode1 = self.__getImageMode(data1)
- if mode1 in ["rgb", "rgba"]:
- intensity1 = self.__luminosityImage(data1)
- vmin1, vmax1 = 0.0, 1.0
+ if data1.size != 0 and data2.size != 0:
+ assert data1.shape[0:2] == data2.shape[0:2]
+
+ sealed = self.__getSealedColormap()
+ vmin, vmax = sealed.getVRange()
+
+ if data1.size == 0:
+ intensity1 = numpy.zeros(data2.shape[0:2])
else:
- intensity1 = data1
- vmin1, vmax1 = data1.min(), data1.max()
+ mode1 = self.__getImageMode(data1)
+ if mode1 in ["rgb", "rgba"]:
+ intensity1 = self.__luminosityImage(data1)
+ else:
+ intensity1 = data1
- mode2 = self.__getImageMode(data2)
- if mode2 in ["rgb", "rgba"]:
- intensity2 = self.__luminosityImage(data2)
- vmin2, vmax2 = 0.0, 1.0
+ if data2.size == 0:
+ intensity2 = numpy.zeros(data1.shape[0:2])
else:
- intensity2 = data2
- vmin2, vmax2 = data2.min(), data2.max()
+ mode2 = self.__getImageMode(data2)
+ if mode2 in ["rgb", "rgba"]:
+ intensity2 = self.__luminosityImage(data2)
+ else:
+ intensity2 = data2
- vmin, vmax = min(vmin1, vmin2) * 1.0, max(vmax1, vmax2) * 1.0
- shape = data1.shape
+ shape = intensity1.shape
result = numpy.empty((shape[0], shape[1], 3), dtype=numpy.uint8)
- a = (intensity1 - vmin) * (1.0 / (vmax - vmin)) * 255.0
- b = (intensity2 - vmin) * (1.0 / (vmax - vmin)) * 255.0
+ a, _, _ = normalize(
+ intensity1,
+ norm=sealed.getNormalization(),
+ autoscale=sealed.getAutoscaleMode(),
+ vmin=sealed.getVMin(),
+ vmax=sealed.getVMax(),
+ gamma=sealed.getGammaNormalizationParameter(),
+ )
+ b, _, _ = normalize(
+ intensity2,
+ norm=sealed.getNormalization(),
+ autoscale=sealed.getAutoscaleMode(),
+ vmin=sealed.getVMin(),
+ vmax=sealed.getVMax(),
+ gamma=sealed.getGammaNormalizationParameter(),
+ )
if mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY:
result[:, :, 0] = a
- result[:, :, 1] = (a + b) / 2
+ result[:, :, 1] = a // 2 + b // 2
result[:, :, 2] = b
elif mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG:
result[:, :, 0] = 255 - b
- result[:, :, 1] = 255 - (a + b) / 2
+ result[:, :, 1] = 255 - (a // 2 + b // 2)
result[:, :, 2] = 255 - a
return result
- def __luminosityImage(self, image):
+ def __composeAMinusBImage(self, data1, data2):
+ """Returns an intensity image containing the composition of `A-B`.
+
+ A data image of a size of 0 is considered as missing. This does not
+ interrupt the processing.
+
+ :param numpy.ndarray data1: First image
+ :param numpy.ndarray data1: Second image
+ :rtype: numpy.ndarray
+ """
+ if data1.size != 0 and data2.size != 0:
+ assert data1.shape[0:2] == data2.shape[0:2]
+
+ data1 = self.__asIntensityImage(data1)
+ data2 = self.__asIntensityImage(data2)
+ if data1.size == 0:
+ result = data2
+ elif data2.size == 0:
+ result = data1
+ else:
+ result = data1.astype(numpy.float32) - data2.astype(numpy.float32)
+ return result
+
+ def __asIntensityImage(self, image: numpy.ndarray):
+ """Returns an intensity image.
+
+ If the image use a single channel, it will be returned as it is.
+
+ If the image is an RBG(A) image, the luminosity (0..1) is extracted and
+ returned. The alpha channel is ignored.
+
+ :rtype: numpy.ndarray
+ """
+ mode = self.__getImageMode(image)
+ if mode in ["rgb", "rgba"]:
+ return self.__luminosityImage(image)
+ return image
+
+ def __luminosityImage(self, image: numpy.ndarray):
"""Returns the luminosity channel from an RBG(A) image.
+
The alpha channel is ignored.
:rtype: numpy.ndarray
"""
mode = self.__getImageMode(image)
- assert(mode in ["rgb", "rgba"])
+ assert mode in ["rgb", "rgba"]
is_uint8 = image.dtype.type == numpy.uint8
# luminosity
image = 0.21 * image[..., 0] + 0.72 * image[..., 1] + 0.07 * image[..., 2]
@@ -1129,8 +812,10 @@ class CompareImages(qt.QMainWindow):
:rtype: numpy.ndarray
"""
- y, x = numpy.ogrid[:shape[0], :shape[1]]
- y, x = y * 1.0 * (image.shape[0] - 1) / (shape[0] - 1), x * 1.0 * (image.shape[1] - 1) / (shape[1] - 1)
+ y, x = numpy.ogrid[: shape[0], : shape[1]]
+ y, x = y * 1.0 * (image.shape[0] - 1) / (shape[0] - 1), x * 1.0 * (
+ image.shape[1] - 1
+ ) / (shape[1] - 1)
b = silx.image.bilinear.BilinearImage(image)
# TODO: could be optimized using strides
x2d = numpy.zeros_like(y) + x
@@ -1143,8 +828,8 @@ class CompareImages(qt.QMainWindow):
:rtype: numpy.ndarray
"""
- assert(image.shape[0] <= size[0])
- assert(image.shape[1] <= size[1])
+ assert image.shape[0] <= size[0]
+ assert image.shape[1] <= size[1]
if image.shape == size:
return image
mode = self.__getImageMode(image)
@@ -1157,7 +842,7 @@ class CompareImages(qt.QMainWindow):
if mode == "intensity":
data = numpy.zeros(size, dtype=image.dtype)
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1]] = image
+ data[pos0 : pos0 + image.shape[0], pos1 : pos1 + image.shape[1]] = image
# TODO: It is maybe possible to put NaN on the margin
else:
if transparent:
@@ -1165,9 +850,13 @@ class CompareImages(qt.QMainWindow):
else:
data = numpy.zeros((size[0], size[1], 3), dtype=numpy.uint8)
depth = min(data.shape[2], image.shape[2])
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1], 0:depth] = image[:, :, 0:depth]
+ data[
+ pos0 : pos0 + image.shape[0], pos1 : pos1 + image.shape[1], 0:depth
+ ] = image[:, :, 0:depth]
if transparent and depth == 3:
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1], 3] = 255
+ data[
+ pos0 : pos0 + image.shape[0], pos1 : pos1 + image.shape[1], 3
+ ] = 255
return data
def __toAffineTransformation(self, sift_result):
@@ -1191,7 +880,7 @@ class CompareImages(qt.QMainWindow):
return AffineTransformation(tx, ty, sx, sy, rot)
def getTransformation(self):
- """Retuns the affine transformation applied to the second image to align
+ """Returns the affine transformation applied to the second image to align
it to the first image.
This result is only valid for sift alignment.
@@ -1220,9 +909,11 @@ class CompareImages(qt.QMainWindow):
_logger.info("Number of Keypoints within image 1: %i" % keypoints.size)
_logger.info(" within image 2: %i" % second_keypoints.size)
- self.__matching_keypoints = (match[:].x[:, 0],
- match[:].y[:, 0],
- match[:].scale[:, 0])
+ self.__matching_keypoints = (
+ match[:].x[:, 0],
+ match[:].y[:, 0],
+ match[:].scale[:, 0],
+ )
matching_keypoints = match.shape[0]
_logger.info("Matching keypoints: %i" % matching_keypoints)
if matching_keypoints == 0:
@@ -1242,6 +933,10 @@ class CompareImages(qt.QMainWindow):
self.__transformation = self.__toAffineTransformation(result)
return data1, data2
+ def resetZoom(self, dataMargins=None):
+ """Reset the plot limits to the bounds of the data and redraw the plot."""
+ self.__plot.resetZoom(dataMargins)
+
def setAutoResetZoom(self, activate=True):
"""
diff --git a/src/silx/gui/plot/ComplexImageView.py b/src/silx/gui/plot/ComplexImageView.py
index 4eee3b0..654a1c1 100644
--- a/src/silx/gui/plot/ComplexImageView.py
+++ b/src/silx/gui/plot/ComplexImageView.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,18 +27,14 @@ The :class:`ComplexImageView` widget is dedicated to visualize a single 2D datas
of complex data.
"""
-from __future__ import absolute_import
-
__authors__ = ["Vincent Favre-Nicolin", "T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
import logging
-import collections
import numpy
-from ...utils.deprecation import deprecated
from .. import qt, icons
from .PlotWindow import Plot2D
from . import items
@@ -51,6 +46,7 @@ _logger = logging.getLogger(__name__)
# Widgets
+
class _AmplitudeRangeDialog(qt.QDialog):
"""QDialog asking for the amplitude range to display."""
@@ -60,12 +56,9 @@ class _AmplitudeRangeDialog(qt.QDialog):
It provides the new range as a 2-tuple: (max, delta)
"""
- def __init__(self,
- parent=None,
- amplitudeRange=None,
- displayedRange=(None, 2)):
+ def __init__(self, parent=None, amplitudeRange=None, displayedRange=(None, 2)):
super(_AmplitudeRangeDialog, self).__init__(parent)
- self.setWindowTitle('Set Displayed Amplitude Range')
+ self.setWindowTitle("Set Displayed Amplitude Range")
if amplitudeRange is not None:
amplitudeRange = min(amplitudeRange), max(amplitudeRange)
@@ -77,25 +70,24 @@ class _AmplitudeRangeDialog(qt.QDialog):
if self._amplitudeRange is not None:
min_, max_ = self._amplitudeRange
- layout.addRow(
- qt.QLabel('Data Amplitude Range: [%g, %g]' % (min_, max_)))
+ layout.addRow(qt.QLabel("Data Amplitude Range: [%g, %g]" % (min_, max_)))
self._maxLineEdit = FloatEdit(parent=self)
- self._maxLineEdit.validator().setBottom(0.)
+ self._maxLineEdit.validator().setBottom(0.0)
self._maxLineEdit.setAlignment(qt.Qt.AlignRight)
self._maxLineEdit.editingFinished.connect(self._rangeUpdated)
- layout.addRow('Displayed Max.:', self._maxLineEdit)
+ layout.addRow("Displayed Max.:", self._maxLineEdit)
- self._autoscale = qt.QCheckBox('autoscale')
+ self._autoscale = qt.QCheckBox("autoscale")
self._autoscale.toggled.connect(self._autoscaleCheckBoxToggled)
- layout.addRow('', self._autoscale)
+ layout.addRow("", self._autoscale)
self._deltaLineEdit = FloatEdit(parent=self)
- self._deltaLineEdit.validator().setBottom(1.)
+ self._deltaLineEdit.validator().setBottom(1.0)
self._deltaLineEdit.setAlignment(qt.Qt.AlignRight)
self._deltaLineEdit.editingFinished.connect(self._rangeUpdated)
- layout.addRow('Displayed delta (log10 unit):', self._deltaLineEdit)
+ layout.addRow("Displayed delta (log10 unit):", self._deltaLineEdit)
buttons = qt.QDialogButtonBox(self)
buttons.addButton(qt.QDialogButtonBox.Ok)
@@ -110,8 +102,7 @@ class _AmplitudeRangeDialog(qt.QDialog):
self.rejected.connect(self._handleRejected)
def _resetDialogToDefault(self):
- """Set Widgets of the dialog from range information
- """
+ """Set Widgets of the dialog from range information"""
max_, delta = self._defaultDisplayedRange
if max_ is not None: # Not in autoscale
@@ -119,7 +110,7 @@ class _AmplitudeRangeDialog(qt.QDialog):
elif self._amplitudeRange is not None: # Autoscale with data
displayedMax = self._amplitudeRange[1]
else: # Autoscale without data
- displayedMax = ''
+ displayedMax = ""
if displayedMax == "":
self._maxLineEdit.setText("")
else:
@@ -152,7 +143,7 @@ class _AmplitudeRangeDialog(qt.QDialog):
"""Handle autoscale checkbox state changes"""
if checked: # Use default values
if self._amplitudeRange is None:
- max_ = ''
+ max_ = ""
else:
max_ = self._amplitudeRange[1]
if max_ == "":
@@ -170,21 +161,31 @@ class _ComplexDataToolButton(qt.QToolButton):
:param plot: The :class:`ComplexImageView` to control
"""
- _MODES = collections.OrderedDict([
- (ImageComplexData.ComplexMode.ABSOLUTE, ('math-amplitude', 'Amplitude')),
- (ImageComplexData.ComplexMode.SQUARE_AMPLITUDE,
- ('math-square-amplitude', 'Square amplitude')),
- (ImageComplexData.ComplexMode.PHASE, ('math-phase', 'Phase')),
- (ImageComplexData.ComplexMode.REAL, ('math-real', 'Real part')),
- (ImageComplexData.ComplexMode.IMAGINARY,
- ('math-imaginary', 'Imaginary part')),
- (ImageComplexData.ComplexMode.AMPLITUDE_PHASE,
- ('math-phase-color', 'Amplitude and Phase')),
- (ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE,
- ('math-phase-color-log', 'Log10(Amp.) and Phase'))
- ])
-
- _RANGE_DIALOG_TEXT = 'Set Amplitude Range...'
+ _MODES = dict(
+ [
+ (ImageComplexData.ComplexMode.ABSOLUTE, ("math-amplitude", "Amplitude")),
+ (
+ ImageComplexData.ComplexMode.SQUARE_AMPLITUDE,
+ ("math-square-amplitude", "Square amplitude"),
+ ),
+ (ImageComplexData.ComplexMode.PHASE, ("math-phase", "Phase")),
+ (ImageComplexData.ComplexMode.REAL, ("math-real", "Real part")),
+ (
+ ImageComplexData.ComplexMode.IMAGINARY,
+ ("math-imaginary", "Imaginary part"),
+ ),
+ (
+ ImageComplexData.ComplexMode.AMPLITUDE_PHASE,
+ ("math-phase-color", "Amplitude and Phase"),
+ ),
+ (
+ ImageComplexData.ComplexMode.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)
@@ -210,16 +211,16 @@ class _ComplexDataToolButton(qt.QToolButton):
self.setPopupMode(qt.QToolButton.InstantPopup)
self._modeChanged(self._plot2DComplex.getComplexMode())
- self._plot2DComplex.sigVisualizationModeChanged.connect(
- self._modeChanged)
+ self._plot2DComplex.sigVisualizationModeChanged.connect(self._modeChanged)
def _modeChanged(self, mode):
"""Handle change of visualization modes"""
icon, text = self._MODES[mode]
self.setIcon(icons.getQIcon(icon))
- self.setToolTip('Display the ' + text.lower())
+ self.setToolTip("Display the " + text.lower())
self._rangeDialogAction.setEnabled(
- mode == ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE)
+ mode == ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE
+ )
def _triggered(self, action):
"""Handle triggering of menu actions"""
@@ -239,7 +240,8 @@ class _ComplexDataToolButton(qt.QToolButton):
dialog = _AmplitudeRangeDialog(
parent=self,
amplitudeRange=dataRange,
- displayedRange=self._plot2DComplex._getAmplitudeRangeInfo())
+ displayedRange=self._plot2DComplex._getAmplitudeRangeInfo(),
+ )
dialog.sigRangeChanged.connect(self._rangeChanged)
dialog.exec()
dialog.sigRangeChanged.disconnect(self._rangeChanged)
@@ -275,7 +277,7 @@ class ComplexImageView(qt.QWidget):
def __init__(self, parent=None):
super(ComplexImageView, self).__init__(parent)
if parent is None:
- self.setWindowTitle('ComplexImageView')
+ self.setWindowTitle("ComplexImageView")
self._plot2D = Plot2D(self)
@@ -287,14 +289,13 @@ class ComplexImageView(qt.QWidget):
# Create and add image to the plot
self._plotImage = ImageComplexData()
- self._plotImage.setName('__ComplexImageView__complex_image__')
+ self._plotImage.setName("__ComplexImageView__complex_image__")
self._plotImage.sigItemChanged.connect(self._itemChanged)
self._plot2D.addItem(self._plotImage)
- self._plot2D.setActiveImage(self._plotImage.getName())
+ self._plot2D.setActiveImage(self._plotImage)
- toolBar = qt.QToolBar('Complex', self)
- toolBar.addWidget(
- _ComplexDataToolButton(parent=self, plot=self))
+ toolBar = qt.QToolBar("Complex", self)
+ toolBar.addWidget(_ComplexDataToolButton(parent=self, plot=self))
self._plot2D.insertToolBar(self._plot2D.getProfileToolbar(), toolBar)
@@ -347,8 +348,10 @@ class ComplexImageView(qt.QWidget):
:rtype: numpy.ndarray of float with 2 dims or RGBA image (uint8).
"""
mode = self.getComplexMode()
- if mode in (self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
+ if mode in (
+ self.ComplexMode.AMPLITUDE_PHASE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ ):
return self._plotImage.getRgbaImageData(copy=copy)
else:
return self._plotImage.getData(copy=copy)
@@ -357,19 +360,6 @@ class ComplexImageView(qt.QWidget):
Mode = ComplexMode
- @classmethod
- @deprecated(replacement='supportedComplexModes', since_version='0.11.0')
- def getSupportedVisualizationModes(cls):
- return cls.supportedComplexModes()
-
- @deprecated(replacement='setComplexMode', since_version='0.11.0')
- def setVisualizationMode(self, mode):
- return self.setComplexMode(mode)
-
- @deprecated(replacement='getComplexMode', since_version='0.11.0')
- def getVisualizationMode(self):
- return self.getComplexMode()
-
# Image item proxy
@staticmethod
@@ -493,7 +483,7 @@ class ComplexImageView(qt.QWidget):
:rtype: :class:`.items.Axis`
"""
- return self.getPlot().getYAxis(axis='left')
+ return self.getPlot().getYAxis(axis="left")
def getGraphTitle(self):
"""Return the plot main title as a str."""
diff --git a/src/silx/gui/plot/CurvesROIWidget.py b/src/silx/gui/plot/CurvesROIWidget.py
index 132d398..bd47da0 100644
--- a/src/silx/gui/plot/CurvesROIWidget.py
+++ b/src/silx/gui/plot/CurvesROIWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,14 +32,12 @@ __authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
__date__ = "13/03/2018"
-from collections import OrderedDict
import logging
import os
import sys
import functools
import numpy
from silx.io import dictdump
-from silx.utils import deprecation
from silx.utils.weakref import WeakMethodProxy
from silx.utils.proxy import docstring
from .. import icons, qt
@@ -108,8 +105,7 @@ class CurvesROIWidget(qt.QWidget):
layout.addWidget(self.headerLabel)
widgetAllCheckbox = qt.QWidget(parent=self)
- self._showAllCheckBox = qt.QCheckBox("show all ROI",
- parent=widgetAllCheckbox)
+ self._showAllCheckBox = qt.QCheckBox("show all ROI", parent=widgetAllCheckbox)
widgetAllCheckbox.setLayout(qt.QHBoxLayout())
spacer = qt.QWidget(parent=widgetAllCheckbox)
spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
@@ -133,14 +129,15 @@ class CurvesROIWidget(qt.QWidget):
self.addButton = qt.QPushButton(hbox)
self.addButton.setText("Add ROI")
- self.addButton.setToolTip('Create a new ROI')
+ self.addButton.setToolTip("Create a new ROI")
self.delButton = qt.QPushButton(hbox)
self.delButton.setText("Delete ROI")
- self.addButton.setToolTip('Remove the selected ROI')
+ self.addButton.setToolTip("Remove the selected ROI")
self.resetButton = qt.QPushButton(hbox)
self.resetButton.setText("Reset")
- self.addButton.setToolTip('Clear all created ROIs. We only let the '
- 'default ROI')
+ self.addButton.setToolTip(
+ "Clear all created ROIs. We only let the " "default ROI"
+ )
hboxlayout.addWidget(self.addButton)
hboxlayout.addWidget(self.delButton)
@@ -150,10 +147,10 @@ class CurvesROIWidget(qt.QWidget):
self.loadButton = qt.QPushButton(hbox)
self.loadButton.setText("Load")
- self.loadButton.setToolTip('Load ROIs from a .ini file')
+ self.loadButton.setToolTip("Load ROIs from a .ini file")
self.saveButton = qt.QPushButton(hbox)
self.saveButton.setText("Save")
- self.loadButton.setToolTip('Save ROIs to a .ini file')
+ self.loadButton.setToolTip("Save ROIs to a .ini file")
hboxlayout.addWidget(self.loadButton)
hboxlayout.addWidget(self.saveButton)
layout.setStretchFactor(self.headerLabel, 0)
@@ -175,8 +172,8 @@ class CurvesROIWidget(qt.QWidget):
self._isConnected = False # True if connected to plot signals
self._isInit = False
- # expose API
- self.getROIListAndDict = self.roiTable.getROIListAndDict
+ def getROIListAndDict(self):
+ return self.roiTable.getROIListAndDict()
def getPlotWidget(self):
"""Returns the associated PlotWidget or None
@@ -211,6 +208,7 @@ class CurvesROIWidget(qt.QWidget):
def _add(self):
"""Add button clicked handler"""
+
def getNextRoiName():
rois = self.roiTable.getRois(order=None)
roisNames = []
@@ -225,6 +223,7 @@ class CurvesROIWidget(qt.QWidget):
i += 1
newroi = "newroi %d" % i
return newroi
+
roi = ROI(name=getNextRoiName())
if roi.getName() == "ICR":
@@ -243,9 +242,9 @@ class CurvesROIWidget(qt.QWidget):
# back compatibility pymca roi signals
ddict = {}
- ddict['event'] = "AddROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
+ ddict["event"] = "AddROI"
+ ddict["roilist"] = self.roiTable.roidict.values()
+ ddict["roidict"] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
# end back compatibility pymca roi signals
@@ -255,9 +254,9 @@ class CurvesROIWidget(qt.QWidget):
# back compatibility pymca roi signals
ddict = {}
- ddict['event'] = "DelROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
+ ddict["event"] = "DelROI"
+ ddict["roilist"] = self.roiTable.roidict.values()
+ ddict["roidict"] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
# end back compatibility pymca roi signals
@@ -270,17 +269,16 @@ class CurvesROIWidget(qt.QWidget):
# back compatibility pymca roi signals
ddict = {}
- ddict['event'] = "ResetROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
+ ddict["event"] = "ResetROI"
+ ddict["roilist"] = self.roiTable.roidict.values()
+ ddict["roidict"] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
# end back compatibility pymca roi signals
def _load(self):
"""Load button clicked handler"""
dialog = qt.QFileDialog(self)
- dialog.setNameFilters(
- ['INI File *.ini', 'JSON File *.json', 'All *.*'])
+ dialog.setNameFilters(["INI File *.ini", "JSON File *.json", "All *.*"])
dialog.setFileMode(qt.QFileDialog.ExistingFile)
dialog.setDirectory(self.roiFileDir)
if not dialog.exec():
@@ -296,9 +294,9 @@ class CurvesROIWidget(qt.QWidget):
# back compatibility pymca roi signals
ddict = {}
- ddict['event'] = "LoadROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
+ ddict["event"] = "LoadROI"
+ ddict["roilist"] = self.roiTable.roidict.values()
+ ddict["roidict"] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
# end back compatibility pymca roi signals
@@ -312,7 +310,7 @@ class CurvesROIWidget(qt.QWidget):
def _save(self):
"""Save button clicked handler"""
dialog = qt.QFileDialog(self)
- dialog.setNameFilters(['INI File *.ini', 'JSON File *.json'])
+ dialog.setNameFilters(["INI File *.ini", "JSON File *.json"])
dialog.setFileMode(qt.QFileDialog.AnyFile)
dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
dialog.setDirectory(self.roiFileDir)
@@ -321,7 +319,7 @@ class CurvesROIWidget(qt.QWidget):
return
outputFile = dialog.selectedFiles()[0]
- extension = '.' + dialog.selectedNameFilter().split('.')[-1]
+ extension = "." + dialog.selectedNameFilter().split(".")[-1]
dialog.close()
if not outputFile.endswith(extension):
@@ -346,16 +344,10 @@ class CurvesROIWidget(qt.QWidget):
"""
self.roiTable.save(filename)
- def setHeader(self, text='ROIs'):
+ def setHeader(self, text="ROIs"):
"""Set the header text of this widget"""
self.headerLabel.setText("<b>%s<\b>" % text)
- @deprecation.deprecated(replacement="calculateRois",
- reason="CamelCase convention",
- since_version="0.7")
- def calculateROIs(self, *args, **kw):
- self.calculateRois(*args, **kw)
-
def calculateRois(self, roiList=None, roiDict=None):
"""Compute ROI information"""
return self.roiTable.calculateRois()
@@ -368,7 +360,7 @@ class CurvesROIWidget(qt.QWidget):
plot = self.getPlotWidget()
curves = () if plot is None else plot.getAllCurves()
if not curves:
- return 1.0, 1.0, 100., 100.
+ return 1.0, 1.0, 100.0, 100.0
xmin, ymin = None, None
xmax, ymax = None, None
@@ -421,12 +413,12 @@ class CurvesROIWidget(qt.QWidget):
def _emitCurrentROISignal(self):
ddict = {}
- ddict['event'] = "currentROISignal"
+ ddict["event"] = "currentROISignal"
if self.roiTable.activeRoi is not None:
- ddict['ROI'] = self.roiTable.activeRoi.toDict()
- ddict['current'] = self.roiTable.activeRoi.getName()
+ ddict["ROI"] = self.roiTable.activeRoi.toDict()
+ ddict["current"] = self.roiTable.activeRoi.getName()
else:
- ddict['current'] = None
+ ddict["current"] = None
if self.__lastSigROISignal != ddict:
self.__lastSigROISignal = ddict
@@ -441,13 +433,14 @@ class _FloatItem(qt.QTableWidgetItem):
"""
Simple QTableWidgetItem overloading the < operator to deal with ordering
"""
+
def __init__(self):
qt.QTableWidgetItem.__init__(self, type=qt.QTableWidgetItem.Type)
def __lt__(self, other):
- if self.text() in ('', ROITable.INFO_NOT_FOUND):
+ if self.text() in ("", ROITable.INFO_NOT_FOUND):
return False
- if other.text() in ('', ROITable.INFO_NOT_FOUND):
+ if other.text() in ("", ROITable.INFO_NOT_FOUND):
return True
return float(self.text()) < float(other.text())
@@ -465,21 +458,23 @@ class ROITable(TableWidget):
"""Signal emitted when the active roi changed or when the value of the
active roi are changing"""
- COLUMNS_INDEX = OrderedDict([
- ('ID', 0),
- ('ROI', 1),
- ('Type', 2),
- ('From', 3),
- ('To', 4),
- ('Raw Counts', 5),
- ('Net Counts', 6),
- ('Raw Area', 7),
- ('Net Area', 8),
- ])
+ COLUMNS_INDEX = dict(
+ [
+ ("ID", 0),
+ ("ROI", 1),
+ ("Type", 2),
+ ("From", 3),
+ ("To", 4),
+ ("Raw Counts", 5),
+ ("Net Counts", 6),
+ ("Raw Area", 7),
+ ("Net Area", 8),
+ ]
+ )
COLUMNS = list(COLUMNS_INDEX.keys())
- INFO_NOT_FOUND = '????????'
+ INFO_NOT_FOUND = "????????"
def __init__(self, parent=None, plot=None, rois=None):
super(ROITable, self).__init__(parent)
@@ -530,26 +525,32 @@ class ROITable(TableWidget):
header = self.horizontalHeader()
header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
self.sortByColumn(0, qt.Qt.AscendingOrder)
- self.hideColumn(self.COLUMNS_INDEX['ID'])
+ self.hideColumn(self.COLUMNS_INDEX["ID"])
def setPlot(self, plot):
self.clear()
self.plot = plot
def __setTooltip(self):
- self.horizontalHeaderItem(self.COLUMNS_INDEX['ROI']).setToolTip(
- 'Region of interest identifier')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Type']).setToolTip(
- 'Type of the ROI')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['From']).setToolTip(
- 'X-value of the min point')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['To']).setToolTip(
- 'X-value of the max point')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Raw Counts']).setToolTip(
- 'Estimation of the integral between y=0 and the selected curve')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Net Counts']).setToolTip(
- 'Estimation of the integral between the segment [maxPt, minPt] '
- 'and the selected curve')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["ROI"]).setToolTip(
+ "Region of interest identifier"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["Type"]).setToolTip(
+ "Type of the ROI"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["From"]).setToolTip(
+ "X-value of the min point"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["To"]).setToolTip(
+ "X-value of the max point"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["Raw Counts"]).setToolTip(
+ "Estimation of the integral between y=0 and the selected curve"
+ )
+ self.horizontalHeaderItem(self.COLUMNS_INDEX["Net Counts"]).setToolTip(
+ "Estimation of the integral between the segment [maxPt, minPt] "
+ "and the selected curve"
+ )
def setRois(self, rois, order=None):
"""Set the ROIs by providing a dictionary of ROI information.
@@ -566,7 +567,7 @@ class ROITable(TableWidget):
:param str order: Field used for ordering the ROIs.
One of "from", "to", "type".
None (default) for no ordering, or same order as specified
- in parameter ``roidict`` if provided as an OrderedDict.
+ in parameter ``rois`` if provided as a dict.
"""
assert order in [None, "from", "to", "type"]
self.clear()
@@ -577,7 +578,7 @@ class ROITable(TableWidget):
if isinstance(roi, ROI):
_roi = roi
else:
- roi['name'] = roiName
+ roi["name"] = roiName
_roi = ROI._fromDict(roi)
self.addRoi(_roi)
else:
@@ -592,12 +593,11 @@ class ROITable(TableWidget):
:param :class:`ROI` roi: roi to add to the table
"""
assert isinstance(roi, ROI)
- self._getItem(name='ID', row=None, roi=roi)
+ self._getItem(name="ID", row=None, roi=roi)
self._roiDict[roi.getID()] = roi
self._markersHandler.add(roi, _RoiMarkerHandler(roi, self.plot))
self._updateRoiInfo(roi.getID())
- callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
- roi.getID())
+ callback = functools.partial(WeakMethodProxy(self._updateRoiInfo), roi.getID())
roi.sigChanged.connect(callback)
# set it as the active one
self.setActiveRoi(roi)
@@ -610,7 +610,7 @@ class ROITable(TableWidget):
if item:
return item
else:
- if name == 'ID':
+ if name == "ID":
assert roi
if roi.getID() in self._roiToItems:
return self._roiToItems[roi.getID()]
@@ -618,41 +618,47 @@ class ROITable(TableWidget):
# create a new row
row = self.rowCount()
self.setRowCount(self.rowCount() + 1)
- item = qt.QTableWidgetItem(str(roi.getID()),
- type=qt.QTableWidgetItem.Type)
+ item = qt.QTableWidgetItem(
+ str(roi.getID()), type=qt.QTableWidgetItem.Type
+ )
self._roiToItems[roi.getID()] = item
- elif name == 'ROI':
- item = qt.QTableWidgetItem(roi.getName() if roi else '',
- type=qt.QTableWidgetItem.Type)
- if roi.getName().upper() in ('ICR', 'DEFAULT'):
+ elif name == "ROI":
+ item = qt.QTableWidgetItem(
+ roi.getName() if roi else "", type=qt.QTableWidgetItem.Type
+ )
+ if roi.getName().upper() in ("ICR", "DEFAULT"):
item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
else:
- item.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- elif name == 'Type':
+ item.setFlags(
+ qt.Qt.ItemIsSelectable
+ | qt.Qt.ItemIsEnabled
+ | qt.Qt.ItemIsEditable
+ )
+ elif name == "Type":
item = qt.QTableWidgetItem(type=qt.QTableWidgetItem.Type)
item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
- elif name in ('To', 'From'):
+ elif name in ("To", "From"):
item = _FloatItem()
- if roi.getName().upper() in ('ICR', 'DEFAULT'):
+ if roi.getName().upper() in ("ICR", "DEFAULT"):
item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
else:
- item.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- elif name in ('Raw Counts', 'Net Counts', 'Raw Area', 'Net Area'):
+ item.setFlags(
+ qt.Qt.ItemIsSelectable
+ | qt.Qt.ItemIsEnabled
+ | qt.Qt.ItemIsEditable
+ )
+ elif name in ("Raw Counts", "Net Counts", "Raw Area", "Net Area"):
item = _FloatItem()
item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
else:
- raise ValueError('item type not recognized')
+ raise ValueError("item type not recognized")
self.setItem(row, self.COLUMNS_INDEX[name], item)
return item
def _itemChanged(self, item):
def getRoi():
- IDItem = self.item(item.row(), self.COLUMNS_INDEX['ID'])
+ IDItem = self.item(item.row(), self.COLUMNS_INDEX["ID"])
assert IDItem
id = int(IDItem.text())
assert id in self._roiDict
@@ -664,21 +670,21 @@ class ROITable(TableWidget):
self.activeROIChanged.emit()
self._userIsEditingRoi = True
- if item.column() in (self.COLUMNS_INDEX['To'], self.COLUMNS_INDEX['From']):
+ if item.column() in (self.COLUMNS_INDEX["To"], self.COLUMNS_INDEX["From"]):
roi = getRoi()
- if item.text() not in ('', self.INFO_NOT_FOUND):
+ if item.text() not in ("", self.INFO_NOT_FOUND):
try:
value = float(item.text())
except ValueError:
value = 0
changed = False
- if item.column() == self.COLUMNS_INDEX['To']:
+ if item.column() == self.COLUMNS_INDEX["To"]:
if value != roi.getTo():
roi.setTo(value)
changed = True
else:
- assert(item.column() == self.COLUMNS_INDEX['From'])
+ assert item.column() == self.COLUMNS_INDEX["From"]
if value != roi.getFrom():
roi.setFrom(value)
changed = True
@@ -686,7 +692,7 @@ class ROITable(TableWidget):
self._updateMarker(roi.getName())
signalChanged(roi)
- if item.column() is self.COLUMNS_INDEX['ROI']:
+ if item.column() is self.COLUMNS_INDEX["ROI"]:
roi = getRoi()
if roi.getName() != item.text():
roi.setName(item.text())
@@ -706,7 +712,7 @@ class ROITable(TableWidget):
roiToRm = set()
for item in activeItems:
row = item.row()
- itemID = self.item(row, self.COLUMNS_INDEX['ID'])
+ itemID = self.item(row, self.COLUMNS_INDEX["ID"])
roiToRm.add(self._roiDict[int(itemID.text())])
[self.removeROI(roi) for roi in roiToRm]
self.blockSignals(old)
@@ -727,8 +733,9 @@ class ROITable(TableWidget):
del self._roiDict[roi.getID()]
self._markersHandler.remove(roi)
- callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
- roi.getID())
+ callback = functools.partial(
+ WeakMethodProxy(self._updateRoiInfo), roi.getID()
+ )
roi.sigChanged.connect(callback)
def setActiveRoi(self, roi):
@@ -770,42 +777,42 @@ class ROITable(TableWidget):
roi.setTo(max)
roi.blockSignals(False)
- itemID = self._getItem(name='ID', roi=roi, row=None)
- itemName = self._getItem(name='ROI', row=itemID.row(), roi=roi)
+ itemID = self._getItem(name="ID", roi=roi, row=None)
+ itemName = self._getItem(name="ROI", row=itemID.row(), roi=roi)
itemName.setText(roi.getName())
- itemType = self._getItem(name='Type', row=itemID.row(), roi=roi)
+ itemType = self._getItem(name="Type", row=itemID.row(), roi=roi)
itemType.setText(roi.getType() or self.INFO_NOT_FOUND)
- itemFrom = self._getItem(name='From', row=itemID.row(), roi=roi)
- fromdata = str(roi.getFrom()) if roi.getFrom() is not None else self.INFO_NOT_FOUND
+ itemFrom = self._getItem(name="From", row=itemID.row(), roi=roi)
+ fromdata = (
+ str(roi.getFrom()) if roi.getFrom() is not None else self.INFO_NOT_FOUND
+ )
itemFrom.setText(fromdata)
- itemTo = self._getItem(name='To', row=itemID.row(), roi=roi)
+ itemTo = self._getItem(name="To", row=itemID.row(), roi=roi)
todata = str(roi.getTo()) if roi.getTo() is not None else self.INFO_NOT_FOUND
itemTo.setText(todata)
rawCounts, netCounts = roi.computeRawAndNetCounts(
- curve=self.plot.getActiveCurve(just_legend=False))
- itemRawCounts = self._getItem(name='Raw Counts', row=itemID.row(),
- roi=roi)
+ curve=self.plot.getActiveCurve(just_legend=False)
+ )
+ itemRawCounts = self._getItem(name="Raw Counts", row=itemID.row(), roi=roi)
rawCounts = str(rawCounts) if rawCounts is not None else self.INFO_NOT_FOUND
itemRawCounts.setText(rawCounts)
- itemNetCounts = self._getItem(name='Net Counts', row=itemID.row(),
- roi=roi)
+ itemNetCounts = self._getItem(name="Net Counts", row=itemID.row(), roi=roi)
netCounts = str(netCounts) if netCounts is not None else self.INFO_NOT_FOUND
itemNetCounts.setText(netCounts)
rawArea, netArea = roi.computeRawAndNetArea(
- curve=self.plot.getActiveCurve(just_legend=False))
- itemRawArea = self._getItem(name='Raw Area', row=itemID.row(),
- roi=roi)
+ curve=self.plot.getActiveCurve(just_legend=False)
+ )
+ itemRawArea = self._getItem(name="Raw Area", row=itemID.row(), roi=roi)
rawArea = str(rawArea) if rawArea is not None else self.INFO_NOT_FOUND
itemRawArea.setText(rawArea)
- itemNetArea = self._getItem(name='Net Area', row=itemID.row(),
- roi=roi)
+ itemNetArea = self._getItem(name="Net Area", row=itemID.row(), roi=roi)
netArea = str(netArea) if netArea is not None else self.INFO_NOT_FOUND
itemNetArea.setText(netArea)
@@ -814,49 +821,23 @@ class ROITable(TableWidget):
def currentChanged(self, current, previous):
if previous and current.row() != previous.row() and current.row() >= 0:
- roiItem = self.item(current.row(),
- self.COLUMNS_INDEX['ID'])
+ roiItem = self.item(current.row(), self.COLUMNS_INDEX["ID"])
assert roiItem
self.setActiveRoi(self._roiDict[int(roiItem.text())])
self._markersHandler.updateAllMarkers()
qt.QTableWidget.currentChanged(self, current, previous)
- @deprecation.deprecated(reason="Removed",
- replacement="roidict and roidict.values()",
- since_version="0.10.0")
- def getROIListAndDict(self):
- """
-
- :return: the list of roi objects and the dictionary of roi name to roi
- object.
- """
- roidict = self._roiDict
- return list(roidict.values()), roidict
-
- def calculateRois(self, roiList=None, roiDict=None):
- """
- Update values of all registred rois (raw and net counts in particular)
-
- :param roiList: deprecated parameter
- :param roiDict: deprecated parameter
- """
- if roiDict:
- deprecation.deprecated_warning(name='roiDict', type_='Parameter',
- reason='Unused parameter',
- since_version="0.10.0")
- if roiList:
- deprecation.deprecated_warning(name='roiList', type_='Parameter',
- reason='Unused parameter',
- since_version="0.10.0")
-
+ def calculateRois(self):
+ """Update values of all registred rois (raw and net counts in particular)"""
for roiID in self._roiDict:
self._updateRoiInfo(roiID)
def _updateMarker(self, roiID):
"""Make sure the marker of the given roi name is updated"""
- if self._showAllMarkers or (self.activeRoi
- and self.activeRoi.getName() == roiID):
+ if self._showAllMarkers or (
+ self.activeRoi and self.activeRoi.getName() == roiID
+ ):
self._updateMarkers()
def _updateMarkers(self):
@@ -866,7 +847,9 @@ class ROITable(TableWidget):
if not self.activeRoi or not self.plot:
return
assert isinstance(self.activeRoi, ROI)
- markerHandler = self._markersHandler.getMarkerHandler(self.activeRoi.getID())
+ markerHandler = self._markersHandler.getMarkerHandler(
+ self.activeRoi.getID()
+ )
if markerHandler is not None:
markerHandler.updateMarkers()
@@ -885,12 +868,16 @@ class ROITable(TableWidget):
if order is None or order.lower() == "none":
ordered_roilist = list(self._roiDict.values())
- res = OrderedDict([(roi.getName(), self._roiDict[roi.getID()]) for roi in ordered_roilist])
+ res = dict(
+ [(roi.getName(), self._roiDict[roi.getID()]) for roi in ordered_roilist]
+ )
else:
assert order in ["from", "to", "type", "netcounts", "rawcounts"]
- ordered_roilist = sorted(self._roiDict.keys(),
- key=lambda roi_id: self._roiDict[roi_id].get(order))
- res = OrderedDict([(roi.getName(), self._roiDict[id]) for id in ordered_roilist])
+ ordered_roilist = sorted(
+ self._roiDict.keys(),
+ key=lambda roi_id: self._roiDict[roi_id].get(order),
+ )
+ res = dict([(roi.getName(), self._roiDict[id]) for id in ordered_roilist])
return res
@@ -905,7 +892,7 @@ class ROITable(TableWidget):
for roiID, roi in self._roiDict.items():
roilist.append(roi.toDict())
roidict[roi.getName()] = roi.toDict()
- datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
+ datadict = {"ROI": {"roilist": roilist, "roidict": roidict}}
dictdump.dump(datadict, filename)
def load(self, filename):
@@ -918,9 +905,9 @@ class ROITable(TableWidget):
rois = []
# Remove rawcounts and netcounts from ROIs
- for roiDict in roisDict['ROI']['roidict'].values():
- roiDict.pop('rawcounts', None)
- roiDict.pop('netcounts', None)
+ for roiDict in roisDict["ROI"]["roidict"].values():
+ roiDict.pop("rawcounts", None)
+ roiDict.pop("netcounts", None)
rois.append(ROI._fromDict(roiDict))
self.setRois(rois)
@@ -947,14 +934,13 @@ class ROITable(TableWidget):
def _handleROIMarkerEvent(self, ddict):
"""Handle plot signals related to marker events."""
- if ddict['event'] == 'markerMoved':
- label = ddict['label']
+ if ddict["event"] == "markerMoved":
+ label = ddict["label"]
roiID = self._markersHandler.getRoiID(markerID=label)
if roiID is not None:
# avoid several emission of sigROISignal
old = self.blockSignals(True)
- self._markersHandler.changePosition(markerID=label,
- x=ddict['x'])
+ self._markersHandler.changePosition(markerID=label, x=ddict["x"])
self.blockSignals(old)
self._updateRoiInfo(roiID)
@@ -995,11 +981,11 @@ class ROITable(TableWidget):
should be visible.
"""
if visible is True:
- self.showColumn(self.COLUMNS_INDEX['Raw Counts'])
- self.showColumn(self.COLUMNS_INDEX['Net Counts'])
+ self.showColumn(self.COLUMNS_INDEX["Raw Counts"])
+ self.showColumn(self.COLUMNS_INDEX["Net Counts"])
else:
- self.hideColumn(self.COLUMNS_INDEX['Raw Counts'])
- self.hideColumn(self.COLUMNS_INDEX['Net Counts'])
+ self.hideColumn(self.COLUMNS_INDEX["Raw Counts"])
+ self.hideColumn(self.COLUMNS_INDEX["Net Counts"])
def setAreaVisible(self, visible):
"""
@@ -1009,11 +995,11 @@ class ROITable(TableWidget):
should be visible.
"""
if visible is True:
- self.showColumn(self.COLUMNS_INDEX['Raw Area'])
- self.showColumn(self.COLUMNS_INDEX['Net Area'])
+ self.showColumn(self.COLUMNS_INDEX["Raw Area"])
+ self.showColumn(self.COLUMNS_INDEX["Net Area"])
else:
- self.hideColumn(self.COLUMNS_INDEX['Raw Area'])
- self.hideColumn(self.COLUMNS_INDEX['Net Area'])
+ self.hideColumn(self.COLUMNS_INDEX["Raw Area"])
+ self.hideColumn(self.COLUMNS_INDEX["Net Area"])
def fillFromROIDict(self, roilist=(), roidict=None, currentroi=None):
"""
@@ -1074,7 +1060,7 @@ class ROI(_RegionOfInterestBase):
self._fromdata = fromdata
self._todata = todata
- self._type = type_ or 'Default'
+ self._type = type_ or "Default"
self.sigItemChanged.connect(self.__itemChanged)
@@ -1151,27 +1137,27 @@ class ROI(_RegionOfInterestBase):
:return: dict containing the roi parameters
"""
ddict = {
- 'type': self._type,
- 'name': self.getName(),
- 'from': self._fromdata,
- 'to': self._todata,
+ "type": self._type,
+ "name": self.getName(),
+ "from": self._fromdata,
+ "to": self._todata,
}
- if hasattr(self, '_extraInfo'):
+ if hasattr(self, "_extraInfo"):
ddict.update(self._extraInfo)
return ddict
@staticmethod
def _fromDict(dic):
- assert 'name' in dic
- roi = ROI(name=dic['name'])
+ assert "name" in dic
+ roi = ROI(name=dic["name"])
roi._extraInfo = {}
for key in dic:
- if key == 'from':
- roi.setFrom(dic['from'])
- elif key == 'to':
- roi.setTo(dic['to'])
- elif key == 'type':
- roi.setType(dic['type'])
+ if key == "from":
+ roi.setFrom(dic["from"])
+ elif key == "to":
+ roi.setTo(dic["to"])
+ elif key == "type":
+ roi.setType(dic["type"])
else:
roi._extraInfo[key] = dic[key]
@@ -1182,7 +1168,7 @@ class ROI(_RegionOfInterestBase):
:return: True if the ROI is the `ICR`
"""
- return self.getName() == 'ICR'
+ return self.getName() == "ICR"
def computeRawAndNetCounts(self, curve):
"""Compute the Raw and net counts in the ROI for the given curve.
@@ -1207,8 +1193,7 @@ class ROI(_RegionOfInterestBase):
x = curve.getXData(copy=False)
y = curve.getYData(copy=False)
- idx = numpy.nonzero((self._fromdata <= x) &
- (x <= self._todata))[0]
+ idx = numpy.nonzero((self._fromdata <= x) & (x <= self._todata))[0]
if len(idx):
xw = x[idx]
yw = y[idx]
@@ -1216,10 +1201,9 @@ class ROI(_RegionOfInterestBase):
deltaX = xw[-1] - xw[0]
deltaY = yw[-1] - yw[0]
if deltaX > 0.0:
- slope = (deltaY / deltaX)
+ slope = deltaY / deltaX
background = yw[0] + slope * (xw - xw[0])
- netCounts = (rawCounts -
- background.sum(dtype=numpy.float64))
+ netCounts = rawCounts - background.sum(dtype=numpy.float64)
else:
netCounts = 0.0
else:
@@ -1275,6 +1259,7 @@ class _RoiMarkerManager(object):
"""
Deal with all the ROI markers
"""
+
def __init__(self):
self._roiMarkerHandlers = {}
self._middleROIMarkerFlag = False
@@ -1294,7 +1279,7 @@ class _RoiMarkerManager(object):
assert isinstance(roi, ROI)
assert isinstance(markersHandler, _RoiMarkerHandler)
if roi.getID() in self._roiMarkerHandlers:
- raise ValueError('roi with the same ID already existing')
+ raise ValueError("roi with the same ID already existing")
else:
self._roiMarkerHandlers[roi.getID()] = markersHandler
@@ -1324,25 +1309,30 @@ class _RoiMarkerManager(object):
def changePosition(self, markerID, x):
markerHandler = self.getMarker(markerID)
if markerHandler is None:
- raise ValueError('Marker %s not register' % markerID)
+ raise ValueError("Marker %s not register" % markerID)
markerHandler.changePosition(markerID=markerID, x=x)
def updateMarker(self, markerID):
markerHandler = self.getMarker(markerID)
if markerHandler is None:
- raise ValueError('Marker %s not register' % markerID)
+ raise ValueError("Marker %s not register" % markerID)
roiID = self.getRoiID(markerID)
- visible = (self._activeRoi and self._activeRoi.getID() == roiID) or self._showAllMarkers is True
+ visible = (
+ self._activeRoi and self._activeRoi.getID() == roiID
+ ) or self._showAllMarkers is True
markerHandler.setVisible(visible)
markerHandler.updateAllMarkers()
def updateRoiMarkers(self, roiID):
if roiID in self._roiMarkerHandlers:
- visible = ((self._activeRoi and self._activeRoi.getID() == roiID)
- or self._showAllMarkers is True)
+ visible = (
+ self._activeRoi and self._activeRoi.getID() == roiID
+ ) or self._showAllMarkers is True
_roi = self._roiMarkerHandlers[roiID]._roi()
if _roi and not _roi.isICR():
- self._roiMarkerHandlers[roiID].showMiddleMarker(self._middleROIMarkerFlag)
+ self._roiMarkerHandlers[roiID].showMiddleMarker(
+ self._middleROIMarkerFlag
+ )
self._roiMarkerHandlers[roiID].setVisible(visible)
self._roiMarkerHandlers[roiID].updateMarkers()
@@ -1373,8 +1363,11 @@ class _RoiMarkerManager(object):
def getVisibleRois(self):
res = {}
for roiID, roiHandler in self._roiMarkerHandlers.items():
- markers = (roiHandler.getMarker('min'), roiHandler.getMarker('max'),
- roiHandler.getMarker('middle'))
+ markers = (
+ roiHandler.getMarker("min"),
+ roiHandler.getMarker("max"),
+ roiHandler.getMarker("middle"),
+ )
for marker in markers:
if marker.isVisible():
if roiID not in res:
@@ -1385,6 +1378,7 @@ class _RoiMarkerManager(object):
class _RoiMarkerHandler(object):
"""Used to deal with ROI markers used in ROITable"""
+
def __init__(self, roi, plot):
assert roi and isinstance(roi, ROI)
assert plot
@@ -1392,7 +1386,7 @@ class _RoiMarkerHandler(object):
self._roi = weakref.ref(roi)
self._plot = weakref.ref(plot)
self._draggable = False if roi.isICR() else True
- self._color = 'black' if roi.isICR() else 'blue'
+ self._color = "black" if roi.isICR() else "blue"
self._displayMidMarker = False
self._visible = True
@@ -1406,9 +1400,9 @@ class _RoiMarkerHandler(object):
def clear(self):
if self.plot and self.roi:
- self.plot.removeMarker(self._markerID('min'))
- self.plot.removeMarker(self._markerID('max'))
- self.plot.removeMarker(self._markerID('middle'))
+ self.plot.removeMarker(self._markerID("min"))
+ self.plot.removeMarker(self._markerID("max"))
+ self.plot.removeMarker(self._markerID("middle"))
@property
def roi(self):
@@ -1424,7 +1418,7 @@ class _RoiMarkerHandler(object):
_logger.warning("ROI is not draggable. Won't display middle marker")
return
self._displayMidMarker = visible
- self.getMarker('middle').setVisible(self._displayMidMarker)
+ self.getMarker("middle").setVisible(self._displayMidMarker)
def updateMarkers(self):
if self.roi is None:
@@ -1434,54 +1428,56 @@ class _RoiMarkerHandler(object):
self._updateMiddleMarkerPos()
def _updateMinMarkerPos(self):
- self.getMarker('min').setPosition(x=self.roi.getFrom(), y=None)
- self.getMarker('min').setVisible(self._visible)
+ self.getMarker("min").setPosition(x=self.roi.getFrom(), y=None)
+ self.getMarker("min").setVisible(self._visible)
def _updateMaxMarkerPos(self):
- self.getMarker('max').setPosition(x=self.roi.getTo(), y=None)
- self.getMarker('max').setVisible(self._visible)
+ self.getMarker("max").setPosition(x=self.roi.getTo(), y=None)
+ self.getMarker("max").setVisible(self._visible)
def _updateMiddleMarkerPos(self):
- self.getMarker('middle').setPosition(x=self.roi.getMiddle(), y=None)
- self.getMarker('middle').setVisible(self._displayMidMarker and self._visible)
+ self.getMarker("middle").setPosition(x=self.roi.getMiddle(), y=None)
+ self.getMarker("middle").setVisible(self._displayMidMarker and self._visible)
def getMarker(self, markerType):
if self.plot is None:
return None
- assert markerType in ('min', 'max', 'middle')
+ assert markerType in ("min", "max", "middle")
if self.plot._getMarker(self._markerID(markerType)) is None:
assert self.roi
- if markerType == 'min':
+ if markerType == "min":
val = self.roi.getFrom()
- elif markerType == 'max':
+ elif markerType == "max":
val = self.roi.getTo()
else:
val = self.roi.getMiddle()
_color = self._color
- if markerType == 'middle':
- _color = 'yellow'
- self.plot.addXMarker(val,
- legend=self._markerID(markerType),
- text=self.getMarkerName(markerType),
- color=_color,
- draggable=self.draggable)
+ if markerType == "middle":
+ _color = "yellow"
+ self.plot.addXMarker(
+ val,
+ legend=self._markerID(markerType),
+ text=self.getMarkerName(markerType),
+ color=_color,
+ draggable=self.draggable,
+ )
return self.plot._getMarker(self._markerID(markerType))
def _markerID(self, markerType):
- assert markerType in ('min', 'max', 'middle')
+ assert markerType in ("min", "max", "middle")
assert self.roi
- return '_'.join((str(self.roi.getID()), markerType))
+ return "_".join((str(self.roi.getID()), markerType))
def getMarkerName(self, markerType):
- assert markerType in ('min', 'max', 'middle')
+ assert markerType in ("min", "max", "middle")
assert self.roi
- return ' '.join((self.roi.getName(), markerType))
+ return " ".join((self.roi.getName(), markerType))
def updateTexts(self):
- self.getMarker('min').setText(self.getMarkerName('min'))
- self.getMarker('max').setText(self.getMarkerName('max'))
- self.getMarker('middle').setText(self.getMarkerName('middle'))
+ self.getMarker("min").setText(self.getMarkerName("min"))
+ self.getMarker("max").setText(self.getMarkerName("max"))
+ self.getMarker("middle").setText(self.getMarkerName("middle"))
def changePosition(self, markerID, x):
assert self.hasMarker(markerID)
@@ -1489,10 +1485,10 @@ class _RoiMarkerHandler(object):
assert markerType is not None
if self.roi is None:
return
- if markerType == 'min':
+ if markerType == "min":
self.roi.setFrom(x)
self._updateMiddleMarkerPos()
- elif markerType == 'max':
+ elif markerType == "max":
self.roi.setTo(x)
self._updateMiddleMarkerPos()
else:
@@ -1503,17 +1499,19 @@ class _RoiMarkerHandler(object):
self._updateMaxMarkerPos()
def hasMarker(self, marker):
- return marker in (self._markerID('min'),
- self._markerID('max'),
- self._markerID('middle'))
+ return marker in (
+ self._markerID("min"),
+ self._markerID("max"),
+ self._markerID("middle"),
+ )
def _getMarkerType(self, markerID):
- if markerID.endswith('_min'):
- return 'min'
- elif markerID.endswith('_max'):
- return 'max'
- elif markerID.endswith('_middle'):
- return 'middle'
+ if markerID.endswith("_min"):
+ return "min"
+ elif markerID.endswith("_max"):
+ return "max"
+ elif markerID.endswith("_middle"):
+ return "middle"
else:
return None
@@ -1527,6 +1525,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
:param plot: :class:`.PlotWindow` instance on which to operate
:param name: See :class:`QDockWidget`
"""
+
sigROISignal = qt.Signal(object)
"""Deprecated signal for backward compatibility with silx < 0.7.
Prefer connecting directly to :attr:`CurvesRoiWidget.sigRoiSignal`
@@ -1565,17 +1564,9 @@ class CurvesROIDockWidget(qt.QDockWidget):
See :class:`QMainWindow`.
"""
action = super(CurvesROIDockWidget, self).toggleViewAction()
- action.setIcon(icons.getQIcon('plot-roi'))
+ action.setIcon(icons.getQIcon("plot-roi"))
return action
- def showEvent(self, event):
- """Make sure this widget is raised when it is shown
- (when it is first created as a tab in PlotWindow or when it is shown
- again after hiding).
- """
- self.raise_()
- qt.QDockWidget.showEvent(self, event)
-
@property
def currentROI(self):
return self.roiWidget.currentRoi
diff --git a/src/silx/gui/plot/ImageStack.py b/src/silx/gui/plot/ImageStack.py
index 1588a31..175d6e4 100644
--- a/src/silx/gui/plot/ImageStack.py
+++ b/src/silx/gui/plot/ImageStack.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2020-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2020-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,118 +23,35 @@
# ###########################################################################*/
"""Image stack view with data prefetch capabilty."""
+from __future__ import annotations
+
__authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "04/03/2019"
-from silx.gui import icons, qt
+from silx.gui import qt
from silx.gui.plot import Plot2D
-from silx.gui.utils import concurrent
from silx.io.url import DataUrl
from silx.io.utils import get_data
-from collections import OrderedDict
from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
-import time
-import threading
+from silx.gui.widgets.UrlList import UrlList
+from silx.gui.utils import blockSignals
+from silx.utils.deprecation import deprecated
+
import typing
import logging
+from silx.gui.widgets.WaitingOverlay import WaitingOverlay
+from collections.abc import Iterable
_logger = logging.getLogger(__name__)
-class _PlotWithWaitingLabel(qt.QWidget):
- """Image plot widget with an overlay 'waiting' status.
- """
-
- class AnimationThread(threading.Thread):
- def __init__(self, label):
- self.running = True
- self._label = label
- self.animated_icon = icons.getWaitIcon()
- self.animated_icon.register(self._label)
- super(_PlotWithWaitingLabel.AnimationThread, self).__init__()
-
- def run(self):
- while self.running:
- time.sleep(0.05)
- icon = self.animated_icon.currentIcon()
- self.future_result = concurrent.submitToQtMainThread(
- self._label.setPixmap, icon.pixmap(30, state=qt.QIcon.On))
-
- def stop(self):
- """Stop the update thread"""
- if self.running:
- self.animated_icon.unregister(self._label)
- self.running = False
- self.join(2)
-
- def __init__(self, parent):
- super(_PlotWithWaitingLabel, self).__init__(parent=parent)
- self._autoResetZoom = True
- layout = qt.QStackedLayout(self)
- layout.setStackingMode(qt.QStackedLayout.StackAll)
-
- self._waiting_label = qt.QLabel(parent=self)
- self._waiting_label.setAlignment(qt.Qt.AlignHCenter | qt.Qt.AlignVCenter)
- layout.addWidget(self._waiting_label)
-
- self._plot = Plot2D(parent=self)
- layout.addWidget(self._plot)
-
- self.updateThread = _PlotWithWaitingLabel.AnimationThread(self._waiting_label)
- self.updateThread.start()
-
- def close(self) -> bool:
- super(_PlotWithWaitingLabel, self).close()
- self.stopUpdateThread()
-
- def stopUpdateThread(self):
- self.updateThread.stop()
-
- def setAutoResetZoom(self, reset):
- """
- Should we reset the zoom when adding an image (eq. when browsing)
-
- :param bool reset:
- """
- self._autoResetZoom = reset
- if self._autoResetZoom:
- self._plot.resetZoom()
-
- def isAutoResetZoom(self):
- """
-
- :return: True if a reset is done when the image change
- :rtype: bool
- """
- return self._autoResetZoom
-
- def setWaiting(self, activate=True):
- if activate is True:
- self._plot.clear()
- self._waiting_label.show()
- else:
- self._waiting_label.hide()
-
- def setData(self, data):
- self.setWaiting(activate=False)
- self._plot.addImage(data=data, resetzoom=self._autoResetZoom)
-
- def clear(self):
- self._plot.clear()
- self.setWaiting(False)
-
- def getPlotWidget(self):
- return self._plot
-
-
class _HorizontalSlider(HorizontalSliderWithBrowser):
-
sigCurrentUrlIndexChanged = qt.Signal(int)
def __init__(self, parent):
- super(_HorizontalSlider, self).__init__(parent=parent)
+ super().__init__(parent=parent)
# connect signal / slot
self.valueChanged.connect(self._urlChanged)
@@ -147,67 +63,23 @@ class _HorizontalSlider(HorizontalSliderWithBrowser):
self.sigCurrentUrlIndexChanged.emit(value)
-class UrlList(qt.QWidget):
- """List of URLs the user to select an URL"""
-
- sigCurrentUrlChanged = qt.Signal(str)
- """Signal emitted when the active/current url change"""
-
- def __init__(self, parent=None):
- super(UrlList, self).__init__(parent)
- self.setLayout(qt.QVBoxLayout())
- self.layout().setSpacing(0)
- self.layout().setContentsMargins(0, 0, 0, 0)
- self._listWidget = qt.QListWidget(parent=self)
- self.layout().addWidget(self._listWidget)
-
- # connect signal / Slot
- self._listWidget.currentItemChanged.connect(self._notifyCurrentUrlChanged)
-
- # expose API
- self.currentItem = self._listWidget.currentItem
-
- def setUrls(self, urls: list) -> None:
- url_names = []
- [url_names.append(url.path()) for url in urls]
- self._listWidget.addItems(url_names)
-
- def _notifyCurrentUrlChanged(self, current, previous):
- if current is None:
- pass
- else:
- self.sigCurrentUrlChanged.emit(current.text())
-
- def setUrl(self, url: DataUrl) -> None:
- assert isinstance(url, DataUrl)
- sel_items = self._listWidget.findItems(url.path(), qt.Qt.MatchExactly)
- if sel_items is None:
- _logger.warning(url.path(), ' is not registered in the list.')
- elif len(sel_items) > 0:
- item = sel_items[0]
- self._listWidget.setCurrentItem(item)
- self.sigCurrentUrlChanged.emit(item.text())
-
- def clear(self):
- self._listWidget.clear()
-
-
class _ToggleableUrlSelectionTable(qt.QWidget):
-
_BUTTON_ICON = qt.QStyle.SP_ToolBarHorizontalExtensionButton # noqa
sigCurrentUrlChanged = qt.Signal(str)
"""Signal emitted when the active/current url change"""
+ sigUrlRemoved = qt.Signal(str)
+
def __init__(self, parent=None) -> None:
- qt.QWidget.__init__(self, parent)
+ super().__init__(parent)
self.setLayout(qt.QGridLayout())
self._toggleButton = qt.QPushButton(parent=self)
self.layout().addWidget(self._toggleButton, 0, 2, 1, 1)
- self._toggleButton.setSizePolicy(qt.QSizePolicy.Fixed,
- qt.QSizePolicy.Fixed)
+ self._toggleButton.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
self._urlsTable = UrlList(parent=self)
+
self.layout().addWidget(self._urlsTable, 1, 1, 1, 2)
# set up
@@ -215,12 +87,8 @@ class _ToggleableUrlSelectionTable(qt.QWidget):
# Signal / slot connection
self._toggleButton.clicked.connect(self.toggleUrlSelectionTable)
- self._urlsTable.sigCurrentUrlChanged.connect(self._propagateSignal)
-
- # expose API
- self.setUrls = self._urlsTable.setUrls
- self.setUrl = self._urlsTable.setUrl
- self.currentItem = self._urlsTable.currentItem
+ self._urlsTable.sigCurrentUrlChanged.connect(self.sigCurrentUrlChanged)
+ self._urlsTable.sigUrlRemoved.connect(self.sigUrlRemoved)
def toggleUrlSelectionTable(self):
visible = not self.urlSelectionTableIsVisible()
@@ -237,21 +105,36 @@ class _ToggleableUrlSelectionTable(qt.QWidget):
self._toggleButton.setIcon(icon)
def urlSelectionTableIsVisible(self):
- return self._urlsTable.isVisible()
-
- def _propagateSignal(self, url):
- self.sigCurrentUrlChanged.emit(url)
+ return self._urlsTable.isVisibleTo(self)
def clear(self):
self._urlsTable.clear()
+ # expose UrlList API
+ @deprecated(replacement="addUrls", since_version="2.0")
+ def setUrls(self, urls: Iterable[DataUrl]):
+ self._urlsTable.addUrls(urls=urls)
+
+ def addUrls(self, urls: Iterable[DataUrl]):
+ self._urlsTable.addUrls(urls=urls)
+
+ def setUrl(self, url: typing.Optional[DataUrl]):
+ self._urlsTable.setUrl(url=url)
+
+ def removeUrl(self, url: str):
+ self._urlsTable.removeUrl(url)
+
+ def currentItem(self):
+ return self._urlsTable.currentItem()
+
class UrlLoader(qt.QThread):
"""
Thread use to load DataUrl
"""
+
def __init__(self, parent, url):
- super(UrlLoader, self).__init__(parent=parent)
+ super().__init__(parent=parent)
assert isinstance(url, DataUrl)
self.url = url
self.data = None
@@ -278,17 +161,21 @@ class ImageStack(qt.QMainWindow):
"""Signal emitted when the current url change"""
def __init__(self, parent=None) -> None:
- super(ImageStack, self).__init__(parent)
+ super().__init__(parent)
self.__n_prefetch = ImageStack.N_PRELOAD
self._loadingThreads = []
self.setWindowFlags(qt.Qt.Widget)
self._current_url = None
self._url_loader = UrlLoader
"class to instantiate for loading urls"
+ self._autoResetZoom = True
# main widget
- self._plot = _PlotWithWaitingLabel(parent=self)
+ self._plot = Plot2D(parent=self)
self._plot.setAttribute(qt.Qt.WA_DeleteOnClose, True)
+ self._waitingOverlay = WaitingOverlay(self._plot)
+ self._waitingOverlay.setIconSize(qt.QSize(30, 30))
+ self._waitingOverlay.hide()
self.setWindowTitle("Image stack")
self.setCentralWidget(self._plot)
@@ -309,12 +196,14 @@ class ImageStack(qt.QMainWindow):
# connect signal / slot
self._urlsTable.sigCurrentUrlChanged.connect(self.setCurrentUrl)
+ self._urlsTable.sigUrlRemoved.connect(self.removeUrl)
self._slider.sigCurrentUrlIndexChanged.connect(self.setCurrentUrlIndex)
def close(self) -> bool:
self._freeLoadingThreads()
+ self._waitingOverlay.close()
self._plot.close()
- super(ImageStack, self).close()
+ super().close()
def setUrlLoaderClass(self, urlLoader: typing.Type[UrlLoader]) -> None:
"""
@@ -347,14 +236,14 @@ class ImageStack(qt.QMainWindow):
:return: PlotWidget contained in this window
:rtype: Plot2D
"""
- return self._plot.getPlotWidget()
+ return self._plot
def reset(self) -> None:
"""Clear the plot and remove any link to url"""
self._freeLoadingThreads()
self._urls = None
self._urlIndexes = None
- self._urlData = OrderedDict({})
+ self._urlData = {}
self._current_url = None
self._plot.clear()
self._urlsTable.clear()
@@ -397,7 +286,8 @@ class ImageStack(qt.QMainWindow):
if url in self._urlIndexes:
self._urlData[url] = sender.data
if self.getCurrentUrl().path() == url:
- self._plot.setData(self._urlData[url])
+ self._waitingOverlay.setVisible(False)
+ self._plot.addImage(self._urlData[url], resetzoom=self._autoResetZoom)
if sender in self._loadingThreads:
self._loadingThreads.remove(sender)
self.sigLoaded.emit(url)
@@ -422,6 +312,29 @@ class ImageStack(qt.QMainWindow):
"""
return self.__n_prefetch
+ def setUrlsEditable(self, editable: bool):
+ self._urlsTable._urlsTable.setEditable(editable)
+ if editable:
+ selection_mode = qt.QAbstractItemView.ExtendedSelection
+ else:
+ selection_mode = qt.QAbstractItemView.SingleSelection
+ self._urlsTable._urlsTable.setSelectionMode(selection_mode)
+
+ @staticmethod
+ def createUrlIndexes(urls: tuple):
+ indexes = {}
+ for index, url in enumerate(urls):
+ assert isinstance(
+ url, DataUrl
+ ), f"url is expected to be a DataUrl. Get {type(url)}"
+ indexes[index] = url
+ return indexes
+
+ def _resetSlider(self):
+ with blockSignals(self._slider):
+ self._slider.setMinimum(0)
+ self._slider.setMaximum(len(self._urls) - 1)
+
def setUrls(self, urls: list) -> None:
"""list of urls within an index. Warning: urls should contain an image
compatible with the silx.gui.plot.Plot class
@@ -430,26 +343,16 @@ class ImageStack(qt.QMainWindow):
(position in the stack), value is the DataUrl
:type: list
"""
- def createUrlIndexes():
- indexes = OrderedDict()
- for index, url in enumerate(urls):
- indexes[index] = url
- return indexes
-
- urls_with_indexes = createUrlIndexes()
+ urls_with_indexes = self.createUrlIndexes(urls=urls)
urlsToIndex = self._urlsToIndex(urls_with_indexes)
self.reset()
self._urls = urls_with_indexes
self._urlIndexes = urlsToIndex
- old_url_table = self._urlsTable.blockSignals(True)
- self._urlsTable.setUrls(urls=list(self._urls.values()))
- self._urlsTable.blockSignals(old_url_table)
+ with blockSignals(self._urlsTable):
+ self._urlsTable.addUrls(urls=list(self._urls.values()))
- old_slider = self._slider.blockSignals(True)
- self._slider.setMinimum(0)
- self._slider.setMaximum(len(self._urls) - 1)
- self._slider.blockSignals(old_slider)
+ self._resetSlider()
if self.getCurrentUrl() in self._urls:
self.setCurrentUrl(self.getCurrentUrl())
@@ -458,6 +361,35 @@ class ImageStack(qt.QMainWindow):
first_url = self._urls[list(self._urls.keys())[0]]
self.setCurrentUrl(first_url)
+ def removeUrl(self, url: str) -> None:
+ """
+ Remove provided URL from the table
+
+ :param url: URL as str
+ """
+ # remove the given urls from self._urls and self._urlIndexes
+ if not isinstance(url, str):
+ raise TypeError("url is expected to be the str representation of the url")
+
+ # try to get reset the url displayed
+ current_url = self.getCurrentUrl()
+ with blockSignals(self._urlsTable):
+ self._urlsTable.removeUrl(url)
+ # update urls
+ urls_with_indexes = self.createUrlIndexes(
+ filter(
+ lambda a: a.path() != url,
+ self._urls.values(),
+ )
+ )
+ urlsToIndex = self._urlsToIndex(urls_with_indexes)
+ self._urls = urls_with_indexes
+ self._urlIndexes = urlsToIndex
+ self._resetSlider()
+
+ if current_url != url:
+ self.setCurrentUrl(current_url)
+
def getUrls(self) -> tuple:
"""
@@ -556,41 +488,46 @@ class ImageStack(qt.QMainWindow):
if self._urls is None:
return
elif index >= len(self._urls):
- raise ValueError('requested index out of bounds')
+ raise ValueError("requested index out of bounds")
else:
return self.setCurrentUrl(self._urls[index])
- def setCurrentUrl(self, url: typing.Union[DataUrl, str]) -> None:
+ def setCurrentUrl(self, url: typing.Optional[typing.Union[DataUrl, str]]) -> None:
"""
Define the url to be displayed
:param url: url to be displayed
:type: DataUrl
+ :raises KeyError: raised if the url is not know
"""
- assert isinstance(url, (DataUrl, str))
- if isinstance(url, str):
+ assert isinstance(url, (DataUrl, str, type(None)))
+ if url == "":
+ url = None
+ elif isinstance(url, str):
url = DataUrl(path=url)
- if url != self._current_url:
+ if url is not None and url != self._current_url:
self._current_url = url
self.sigCurrentUrlChanged.emit(url.path())
- old_url_table = self._urlsTable.blockSignals(True)
- old_slider = self._slider.blockSignals(True)
-
- self._urlsTable.setUrl(url)
- self._slider.setUrlIndex(self._urlIndexes[url.path()])
- if self._current_url is None:
- self._plot.clear()
- else:
- if self._current_url.path() in self._urlData:
- self._plot.setData(self._urlData[url.path()])
- else:
- self._load(url)
- self._notifyLoading()
- self._preFetch(self._getNNextUrls(self.__n_prefetch, url))
- self._preFetch(self._getNPreviousUrls(self.__n_prefetch, url))
- self._urlsTable.blockSignals(old_url_table)
- self._slider.blockSignals(old_slider)
+ with blockSignals(self._urlsTable):
+ with blockSignals(self._slider):
+ self._urlsTable.setUrl(url)
+ if url is not None:
+ self._slider.setUrlIndex(self._urlIndexes[url.path()])
+ if self._current_url is None:
+ self._plot.clear()
+ else:
+ if self._current_url.path() in self._urlData:
+ self._waitingOverlay.setVisible(False)
+ self._plot.addImage(
+ self._urlData[url.path()], resetzoom=self._autoResetZoom
+ )
+ else:
+ self._plot.clear()
+ self._load(url)
+ self._waitingOverlay.setVisible(True)
+ self._preFetch(self._getNNextUrls(self.__n_prefetch, url))
+ self._preFetch(self._getNPreviousUrls(self.__n_prefetch, url))
def getCurrentUrl(self) -> typing.Union[None, DataUrl]:
"""
@@ -619,17 +556,15 @@ class ImageStack(qt.QMainWindow):
res[url.path()] = index
return res
- def _notifyLoading(self):
- """display a simple image of loading..."""
- self._plot.setWaiting(activate=True)
-
def setAutoResetZoom(self, reset):
"""
Should we reset the zoom when adding an image (eq. when browsing)
:param bool reset:
"""
- self._plot.setAutoResetZoom(reset)
+ self._autoResetZoom = reset
+ if self._autoResetZoom:
+ self._plot.resetZoom()
def isAutoResetZoom(self) -> bool:
"""
@@ -637,4 +572,12 @@ class ImageStack(qt.QMainWindow):
:return: True if a reset is done when the image change
:rtype: bool
"""
- return self._plot.isAutoResetZoom()
+ return self._autoResetZoom
+
+ def getWaiterOverlay(self):
+ """
+
+ :return: Return the instance of `WaitingOverlay` used to display if processing or not
+ :rtype: WaitingOverlay
+ """
+ return self._waitingOverlay
diff --git a/src/silx/gui/plot/ImageView.py b/src/silx/gui/plot/ImageView.py
index f8b830a..eaca42b 100644
--- a/src/silx/gui/plot/ImageView.py
+++ b/src/silx/gui/plot/ImageView.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 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
@@ -37,9 +36,6 @@ Basic usage of :class:`ImageView` is through the following methods:
For an example of use, see `imageview.py` in :ref:`sample-code`.
"""
-from __future__ import division
-
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "26/04/2018"
@@ -67,19 +63,27 @@ from .tools.RadarView import RadarView
from .utils.axis import SyncAxes
from ..utils import blockSignals
from . import _utils
-from .tools.profile import manager
from .tools.profile import rois
from .actions import PlotAction
_logger = logging.getLogger(__name__)
-ProfileSumResult = collections.namedtuple("ProfileResult",
- ["dataXRange", "dataYRange",
- 'histoH', 'histoHRange',
- 'histoV', 'histoVRange',
- "xCoords", "xData",
- "yCoords", "yData"])
+ProfileSumResult = collections.namedtuple(
+ "ProfileResult",
+ [
+ "dataXRange",
+ "dataYRange",
+ "histoH",
+ "histoHRange",
+ "histoV",
+ "histoVRange",
+ "xCoords",
+ "xData",
+ "yCoords",
+ "yData",
+ ],
+)
def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
@@ -107,8 +111,7 @@ def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
yMin = int((yMin - origin[1]) / scale[1])
yMax = int((yMax - origin[1]) / scale[1])
- if (xMin >= width or xMax < 0 or
- yMin >= height or yMax < 0):
+ if xMin >= width or xMax < 0 or yMin >= height or yMax < 0:
return None
# The image is at least partly in the plot area
@@ -119,14 +122,15 @@ def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
subsetYMax = (height if yMax >= height else yMax) + 1
if cache is not None:
- if ((subsetXMin, subsetXMax) == cache.dataXRange and
- (subsetYMin, subsetYMax) == cache.dataYRange):
+ if (subsetXMin, subsetXMax) == cache.dataXRange and (
+ subsetYMin,
+ subsetYMax,
+ ) == cache.dataYRange:
# The visible area of data is the same
return cache
# Rebuild histograms for visible area
- visibleData = data[subsetYMin:subsetYMax,
- subsetXMin:subsetXMax]
+ visibleData = data[subsetYMin:subsetYMax, subsetXMin:subsetXMax]
histoHVisibleData = numpy.nansum(visibleData, axis=0)
histoVVisibleData = numpy.nansum(visibleData, axis=1)
histoHMin = numpy.nanmin(histoHVisibleData)
@@ -155,7 +159,8 @@ def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
xCoords=xCoords,
xData=xData,
yCoords=yCoords,
- yData=yData)
+ yData=yData,
+ )
return result
@@ -181,8 +186,8 @@ class _SideHistogram(PlotWidget):
def _plotEvents(self, eventDict):
"""Callback for horizontal histogram plot events."""
- if eventDict['event'] == 'mouseMoved':
- self.sigMouseMoved.emit(eventDict['x'], eventDict['y'])
+ if eventDict["event"] == "mouseMoved":
+ self.sigMouseMoved.emit(eventDict["x"], eventDict["y"])
def setProfileColor(self, color):
self._color = color
@@ -222,13 +227,13 @@ class _SideHistogram(PlotWidget):
profileSum = self.__profileSum
try:
- self.removeCurve('profile')
+ self.removeCurve("profile")
except Exception:
pass
if profileSum is None:
try:
- self.removeCurve('profilesum')
+ self.removeCurve("profilesum")
except Exception:
pass
return
@@ -240,13 +245,17 @@ class _SideHistogram(PlotWidget):
else:
assert False
- self.addCurve(xx, yy,
- xlabel='', ylabel='',
- legend="profilesum",
- color=self._color,
- linestyle='-',
- selectable=False,
- resetzoom=False)
+ self.addCurve(
+ xx,
+ yy,
+ xlabel="",
+ ylabel="",
+ legend="profilesum",
+ color=self._color,
+ linestyle="-",
+ selectable=False,
+ resetzoom=False,
+ )
self.__updateLimits()
@@ -258,13 +267,13 @@ class _SideHistogram(PlotWidget):
profile = self.__profile
try:
- self.removeCurve('profilesum')
+ self.removeCurve("profilesum")
except Exception:
pass
if profile is None:
try:
- self.removeCurve('profile')
+ self.removeCurve("profile")
except Exception:
pass
self.setProfileSum(self.__profileSum)
@@ -277,11 +286,7 @@ class _SideHistogram(PlotWidget):
else:
assert False
- self.addCurve(xx,
- yy,
- legend="profile",
- color=self._roiColor,
- resetzoom=False)
+ self.addCurve(xx, yy, legend="profile", color=self._roiColor, resetzoom=False)
self.__updateLimits()
@@ -303,9 +308,13 @@ class _SideHistogram(PlotWidget):
# Tune the result using the data margins
margins = self.getDataMargins()
if self._direction == qt.Qt.Horizontal:
- _, _, vMin, vMax = _utils.addMarginsToLimits(margins, False, False, 0, 0, vMin, vMax)
+ _, _, vMin, vMax = _utils.addMarginsToLimits(
+ margins, False, False, 0, 0, vMin, vMax
+ )
elif self._direction == qt.Qt.Vertical:
- vMin, vMax, _, _ = _utils.addMarginsToLimits(margins, False, False, vMin, vMax, 0, 0)
+ vMin, vMax, _, _ = _utils.addMarginsToLimits(
+ margins, False, False, vMin, vMax, 0, 0
+ )
else:
assert False
@@ -329,10 +338,14 @@ class ShowSideHistogramsAction(PlotAction):
def __init__(self, plot, parent=None):
super(ShowSideHistogramsAction, self).__init__(
- plot, icon='side-histograms', text='Show/hide side histograms',
- tooltip='Show/hide side histogram',
+ plot,
+ icon="side-histograms",
+ text="Show/hide side histograms",
+ tooltip="Show/hide side histogram",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
def _actionTriggered(self, checked=False):
if self.plot.isSideHistogramDisplayed() != checked:
@@ -353,25 +366,33 @@ class AggregationModeAction(qt.QWidgetAction):
filterAction.setText("No filter")
filterAction.setCheckable(True)
filterAction.setChecked(True)
- filterAction.setProperty("aggregation", items.ImageDataAggregated.Aggregation.NONE)
+ filterAction.setProperty(
+ "aggregation", items.ImageDataAggregated.Aggregation.NONE
+ )
densityNoFilterAction = filterAction
filterAction = qt.QAction(self)
filterAction.setText("Max filter")
filterAction.setCheckable(True)
- filterAction.setProperty("aggregation", items.ImageDataAggregated.Aggregation.MAX)
+ filterAction.setProperty(
+ "aggregation", items.ImageDataAggregated.Aggregation.MAX
+ )
densityMaxFilterAction = filterAction
filterAction = qt.QAction(self)
filterAction.setText("Mean filter")
filterAction.setCheckable(True)
- filterAction.setProperty("aggregation", items.ImageDataAggregated.Aggregation.MEAN)
+ filterAction.setProperty(
+ "aggregation", items.ImageDataAggregated.Aggregation.MEAN
+ )
densityMeanFilterAction = filterAction
filterAction = qt.QAction(self)
filterAction.setText("Min filter")
filterAction.setCheckable(True)
- filterAction.setProperty("aggregation", items.ImageDataAggregated.Aggregation.MIN)
+ filterAction.setProperty(
+ "aggregation", items.ImageDataAggregated.Aggregation.MIN
+ )
densityMinFilterAction = filterAction
densityGroup = qt.QActionGroup(self)
@@ -432,7 +453,7 @@ class ImageView(PlotWindow):
:type backend: str or :class:`BackendBase.BackendBase`
"""
- HISTOGRAMS_COLOR = 'blue'
+ HISTOGRAMS_COLOR = "blue"
"""Color to use for the side histograms."""
HISTOGRAMS_HEIGHT = 200
@@ -456,26 +477,37 @@ class ImageView(PlotWindow):
class ProfileWindowBehavior(Enum):
"""ImageView's profile window behavior options"""
- POPUP = 'popup'
+ POPUP = "popup"
"""All profiles are displayed in pop-up windows"""
- EMBEDDED = 'embedded'
+ EMBEDDED = "embedded"
"""Horizontal, vertical and cross profiles are displayed in
sides widgets, others are displayed in pop-up windows.
"""
def __init__(self, parent=None, backend=None):
- self._imageLegend = '__ImageView__image' + str(id(self))
+ self._imageLegend = "__ImageView__image" + str(id(self))
self._cache = None # Store currently visible data information
- super(ImageView, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=False,
- logScale=False, grid=False,
- curveStyle=False, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=False,
- roi=False, mask=True)
+ super(ImageView, self).__init__(
+ parent=parent,
+ backend=backend,
+ resetzoom=True,
+ autoScale=False,
+ logScale=False,
+ grid=False,
+ curveStyle=False,
+ colormap=True,
+ aspectRatio=True,
+ yInverted=True,
+ copy=True,
+ save=True,
+ print_=True,
+ control=False,
+ position=False,
+ roi=False,
+ mask=True,
+ )
# Enable mask synchronisation to use it in profiles
maskToolsWidget = self.getMaskToolsDockWidget().widget()
@@ -485,12 +517,14 @@ class ImageView(PlotWindow):
self.__showSideHistogramsAction.setChecked(True)
self.__aggregationModeAction = AggregationModeAction(self)
- self.__aggregationModeAction.sigAggregationModeChanged.connect(self._aggregationModeChanged)
+ self.__aggregationModeAction.sigAggregationModeChanged.connect(
+ self._aggregationModeChanged
+ )
if parent is None:
- self.setWindowTitle('ImageView')
+ self.setWindowTitle("ImageView")
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
+ if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
self.getYAxis().setInverted(True)
self._initWidgets(backend)
@@ -505,26 +539,32 @@ class ImageView(PlotWindow):
def _initWidgets(self, backend):
"""Set-up layout and plots."""
- self._histoHPlot = _SideHistogram(backend=backend, parent=self, direction=qt.Qt.Horizontal)
+ self._histoHPlot = _SideHistogram(
+ backend=backend, parent=self, direction=qt.Qt.Horizontal
+ )
widgetHandle = self._histoHPlot.getWidgetHandle()
widgetHandle.setMinimumHeight(self.HISTOGRAMS_HEIGHT)
widgetHandle.setMaximumHeight(self.HISTOGRAMS_HEIGHT)
- self._histoHPlot.setInteractiveMode('zoom')
- self._histoHPlot.setDataMargins(0., 0., 0.1, 0.1)
+ self._histoHPlot.setInteractiveMode("zoom")
+ self._histoHPlot.setDataMargins(0.0, 0.0, 0.1, 0.1)
self._histoHPlot.sigMouseMoved.connect(self._mouseMovedOnHistoH)
self._histoHPlot.setProfileColor(self.HISTOGRAMS_COLOR)
- self._histoVPlot = _SideHistogram(backend=backend, parent=self, direction=qt.Qt.Vertical)
+ self._histoVPlot = _SideHistogram(
+ backend=backend, parent=self, direction=qt.Qt.Vertical
+ )
widgetHandle = self._histoVPlot.getWidgetHandle()
widgetHandle.setMinimumWidth(self.HISTOGRAMS_HEIGHT)
widgetHandle.setMaximumWidth(self.HISTOGRAMS_HEIGHT)
- self._histoVPlot.setInteractiveMode('zoom')
- self._histoVPlot.setDataMargins(0.1, 0.1, 0., 0.)
+ # Trick to align the histogram to the main plot
+ self._histoVPlot.setGraphTitle(" ")
+ self._histoVPlot.setInteractiveMode("zoom")
+ self._histoVPlot.setDataMargins(0.1, 0.1, 0.0, 0.0)
self._histoVPlot.sigMouseMoved.connect(self._mouseMovedOnHistoV)
self._histoVPlot.setProfileColor(self.HISTOGRAMS_COLOR)
self.setPanWithArrowKeys(True)
- self.setInteractiveMode('zoom') # Color set in setColormap
+ self.setInteractiveMode("zoom") # Color set in setColormap
self.sigPlotSignal.connect(self._imagePlotCB)
self.sigActiveImageChanged.connect(self._activeImageChangedSlot)
@@ -608,7 +648,7 @@ class ImageView(PlotWindow):
def isSideHistogramDisplayed(self):
"""True if the side histograms are displayed"""
- return self._histoHPlot.isVisible()
+ return self._histoHPlot.isVisibleTo(self)
def _updateHistograms(self):
"""Update histograms content using current active image."""
@@ -629,7 +669,7 @@ class ImageView(PlotWindow):
def _imagePlotCB(self, eventDict):
"""Callback for imageView plot events."""
- if eventDict['event'] == 'mouseMoved':
+ if eventDict["event"] == "mouseMoved":
activeImage = self.getActiveImage()
if activeImage is not None:
data = activeImage.getData(copy=False)
@@ -638,16 +678,14 @@ class ImageView(PlotWindow):
# Get corresponding coordinate in image
origin = activeImage.getOrigin()
scale = activeImage.getScale()
- if (eventDict['x'] >= origin[0] and
- eventDict['y'] >= origin[1]):
- x = int((eventDict['x'] - origin[0]) / scale[0])
- y = int((eventDict['y'] - origin[1]) / scale[1])
+ if eventDict["x"] >= origin[0] and eventDict["y"] >= origin[1]:
+ x = int((eventDict["x"] - origin[0]) / scale[0])
+ y = int((eventDict["y"] - origin[1]) / scale[1])
if x >= 0 and x < width and y >= 0 and y < height:
- self.valueChanged.emit(float(x), float(y),
- data[y][x])
+ self.valueChanged.emit(float(x), float(y), data[y][x])
- elif eventDict['event'] == 'limitsChanged':
+ elif eventDict["event"] == "limitsChanged":
self._updateHistograms()
def _mouseMovedOnHistoH(self, x, y):
@@ -667,9 +705,10 @@ class ImageView(PlotWindow):
column = int((x - minValue) / xScale)
if column >= 0 and column < data.shape[0]:
self.valueChanged.emit(
- float('nan'),
+ float("nan"),
float(column + self._cache.dataXRange[0]),
- data[column])
+ data[column],
+ )
def _mouseMovedOnHistoV(self, x, y):
if self._cache is None:
@@ -688,9 +727,8 @@ class ImageView(PlotWindow):
row = int((y - minValue) / yScale)
if row >= 0 and row < data.shape[0]:
self.valueChanged.emit(
- float(row + self._cache.dataYRange[0]),
- float('nan'),
- data[row])
+ float(row + self._cache.dataYRange[0]), float("nan"), data[row]
+ )
def _activeImageChangedSlot(self, previous, legend):
"""Handle Plot active image change.
@@ -737,7 +775,7 @@ class ImageView(PlotWindow):
return self.__profileWindowBehavior
def getProfileToolBar(self):
- """"Returns profile tools attached to this plot.
+ """Returns profile tools attached to this plot.
:rtype: silx.gui.plot.PlotTools.ProfileToolBar
"""
@@ -761,18 +799,20 @@ class ImageView(PlotWindow):
:return: The histogram and its extent as a dict or None.
:rtype: dict
"""
- assert axis in ('x', 'y')
+ assert axis in ("x", "y")
if self._cache is None:
return None
else:
- if axis == 'x':
+ if axis == "x":
return dict(
data=numpy.array(self._cache.histoH, copy=True),
- extent=self._cache.dataXRange)
+ extent=self._cache.dataXRange,
+ )
else:
return dict(
data=numpy.array(self._cache.histoV, copy=True),
- extent=(self._cache.dataYRange))
+ extent=(self._cache.dataYRange),
+ )
def radarView(self):
"""Get the lower right radarView widget."""
@@ -799,8 +839,15 @@ class ImageView(PlotWindow):
"""
return self.getDefaultColormap()
- def setColormap(self, colormap=None, normalization=None,
- autoscale=None, vmin=None, vmax=None, colors=None):
+ def setColormap(
+ self,
+ colormap=None,
+ normalization=None,
+ autoscale=None,
+ vmin=None,
+ vmax=None,
+ colors=None,
+ ):
"""Set the default colormap and update active image.
Parameters that are not provided are taken from the current colormap.
@@ -872,10 +919,17 @@ class ImageView(PlotWindow):
cmap.setColormapLUT(colors)
cursorColor = cursorColorForColormap(cmap.getName())
- self.setInteractiveMode('zoom', color=cursorColor)
-
- def setImage(self, image, origin=(0, 0), scale=(1., 1.),
- copy=True, reset=None, resetzoom=True):
+ self.setInteractiveMode("zoom", color=cursorColor)
+
+ def setImage(
+ self,
+ image,
+ origin=(0, 0),
+ scale=(1.0, 1.0),
+ copy=True,
+ reset=None,
+ resetzoom=True,
+ ):
"""Set the image to display.
:param image: A 2D array representing the image or None to empty plot.
@@ -905,12 +959,12 @@ class ImageView(PlotWindow):
assert scale[1] > 0
if image is None:
- self.remove(self._imageLegend, kind='image')
+ self.remove(self._imageLegend, kind="image")
return
- data = numpy.array(image, order='C', copy=copy)
+ data = numpy.array(image, order="C", copy=copy)
if data.size == 0:
- self.remove(self._imageLegend, kind='image')
+ self.remove(self._imageLegend, kind="image")
return
assert data.ndim == 2 or (data.ndim == 3 and data.shape[2] in (3, 4))
@@ -921,11 +975,14 @@ class ImageView(PlotWindow):
aggregation = items.ImageDataAggregated.Aggregation.NONE
if aggregation is items.ImageDataAggregated.Aggregation.NONE:
- self.addImage(data,
- legend=self._imageLegend,
- origin=origin, scale=scale,
- colormap=self.getColormap(),
- resetzoom=False)
+ self.addImage(
+ data,
+ legend=self._imageLegend,
+ origin=origin,
+ scale=scale,
+ colormap=self.getColormap(),
+ resetzoom=False,
+ )
else:
item = self._getItem("image", self._imageLegend)
if isinstance(item, items.ImageDataAggregated):
@@ -958,31 +1015,33 @@ class ImageView(PlotWindow):
# ImageViewMainWindow #########################################################
+
class ImageViewMainWindow(ImageView):
""":class:`ImageView` with additional toolbars
Adds extra toolbar and a status bar to :class:`ImageView`.
"""
+
def __init__(self, parent=None, backend=None):
self._dataInfo = None
super(ImageViewMainWindow, self).__init__(parent, backend)
self.setWindowFlags(qt.Qt.Window)
- self.getXAxis().setLabel('X')
- self.getYAxis().setLabel('Y')
- self.setGraphTitle('Image')
+ self.getXAxis().setLabel("X")
+ self.getYAxis().setLabel("Y")
+ self.setGraphTitle("Image")
# Add toolbars and status bar
self.addToolBar(qt.Qt.BottomToolBarArea, LimitsToolBar(plot=self))
- menu = self.menuBar().addMenu('File')
+ menu = self.menuBar().addMenu("File")
menu.addAction(self.getOutputToolBar().getSaveAction())
menu.addAction(self.getOutputToolBar().getPrintAction())
menu.addSeparator()
- action = menu.addAction('Quit')
+ action = menu.addAction("Quit")
action.triggered[bool].connect(qt.QApplication.instance().quit)
- menu = self.menuBar().addMenu('Edit')
+ menu = self.menuBar().addMenu("Edit")
menu.addAction(self.getOutputToolBar().getCopyAction())
menu.addSeparator()
menu.addAction(self.getResetZoomAction())
@@ -991,7 +1050,7 @@ class ImageViewMainWindow(ImageView):
menu.addAction(actions.control.YAxisInvertedAction(self, self))
menu.addAction(self.getShowSideHistogramsAction())
- self.__profileMenu = self.menuBar().addMenu('Profile')
+ self.__profileMenu = self.menuBar().addMenu("Profile")
self.__updateProfileMenu()
# Connect to ImageView's signal
@@ -1011,7 +1070,12 @@ class ImageViewMainWindow(ImageView):
try:
if isinstance(value, numpy.ndarray):
if len(value) == 4:
- return "RGBA: %.3g, %.3g, %.3g, %.3g" % (value[0], value[1], value[2], value[3])
+ return "RGBA: %.3g, %.3g, %.3g, %.3g" % (
+ value[0],
+ value[1],
+ value[2],
+ value[3],
+ )
elif len(value) == 3:
return "RGB: %.3g, %.3g, %.3g" % (value[0], value[1], value[2])
else:
@@ -1024,14 +1088,14 @@ class ImageViewMainWindow(ImageView):
def _statusBarSlot(self, row, column, value):
"""Update status bar with coordinates/value from plots."""
if numpy.isnan(row):
- msg = 'Column: %d, Sum: %g' % (int(column), value)
+ msg = "Column: %d, Sum: %g" % (int(column), value)
elif numpy.isnan(column):
- msg = 'Row: %d, Sum: %g' % (int(row), value)
+ msg = "Row: %d, Sum: %g" % (int(row), value)
else:
msg_value = self._formatValueToString(value)
- msg = 'Position: (%d, %d), %s' % (int(row), int(column), msg_value)
+ msg = "Position: (%d, %d), %s" % (int(row), int(column), msg_value)
if self._dataInfo is not None:
- msg = self._dataInfo + ', ' + msg
+ msg = self._dataInfo + ", " + msg
self.statusBar().showMessage(msg)
@@ -1042,10 +1106,10 @@ class ImageViewMainWindow(ImageView):
@docstring(ImageView)
def setImage(self, image, *args, **kwargs):
- if hasattr(image, 'dtype') and hasattr(image, 'shape'):
+ if hasattr(image, "dtype") and hasattr(image, "shape"):
assert image.ndim == 2 or (image.ndim == 3 and image.shape[2] in (3, 4))
height, width = image.shape[0:2]
- dataInfo = 'Data: %dx%d (%s)' % (width, height, str(image.dtype))
+ dataInfo = "Data: %dx%d (%s)" % (width, height, str(image.dtype))
else:
dataInfo = None
diff --git a/src/silx/gui/plot/Interaction.py b/src/silx/gui/plot/Interaction.py
index 6213889..2d8bf63 100644
--- a/src/silx/gui/plot/Interaction.py
+++ b/src/silx/gui/plot/Interaction.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
@@ -85,6 +84,7 @@ import weakref
# state machine ###############################################################
+
class State(object):
"""Base class for the states of a state machine.
@@ -143,6 +143,7 @@ class State(object):
"""
pass
+
class StateMachine(object):
"""State machine controller.
@@ -185,7 +186,7 @@ class StateMachine(object):
:param str eventName: Name of the event to handle
:returns: The return value of the handler or None
"""
- handlerName = 'on' + eventName[0].upper() + eventName[1:]
+ handlerName = "on" + eventName[0].upper() + eventName[1:]
try:
handler = getattr(self.state, handlerName)
except AttributeError:
@@ -205,13 +206,13 @@ class StateMachine(object):
# clickOrDrag #################################################################
-LEFT_BTN = 'left'
+LEFT_BTN = "left"
"""Left mouse button."""
-RIGHT_BTN = 'right'
+RIGHT_BTN = "right"
"""Right mouse button."""
-MIDDLE_BTN = 'middle'
+MIDDLE_BTN = "middle"
"""Middle mouse button."""
@@ -225,15 +226,15 @@ class ClickOrDrag(StateMachine):
:param Set[str] dragButtons: Set of buttons that provides drag interaction
"""
- DRAG_THRESHOLD_SQUARE_DIST = 5 ** 2
+ DRAG_THRESHOLD_SQUARE_DIST = 5**2
class Idle(State):
def onPress(self, x, y, btn):
if btn in self.machine.dragButtons:
- self.goto('clickOrDrag', x, y, btn)
+ self.goto("clickOrDrag", x, y, btn)
return True
elif btn in self.machine.clickButtons:
- self.goto('click', x, y, btn)
+ self.goto("click", x, y, btn)
return True
class Click(State):
@@ -245,12 +246,12 @@ class ClickOrDrag(StateMachine):
dx2 = (x - self.initPos[0]) ** 2
dy2 = (y - self.initPos[1]) ** 2
if (dx2 + dy2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('idle')
+ self.goto("idle")
def onRelease(self, x, y, btn):
if btn == self.button:
self.machine.click(x, y, btn)
- self.goto('idle')
+ self.goto("idle")
class ClickOrDrag(State):
def enterState(self, x, y, btn):
@@ -261,13 +262,13 @@ class ClickOrDrag(StateMachine):
dx2 = (x - self.initPos[0]) ** 2
dy2 = (y - self.initPos[1]) ** 2
if (dx2 + dy2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('drag', self.initPos, (x, y), self.button)
+ self.goto("drag", self.initPos, (x, y), self.button)
def onRelease(self, x, y, btn):
if btn == self.button:
if btn in self.machine.clickButtons:
self.machine.click(x, y, btn)
- self.goto('idle')
+ self.goto("idle")
class Drag(State):
def enterState(self, initPos, curPos, btn):
@@ -282,26 +283,27 @@ class ClickOrDrag(StateMachine):
def onRelease(self, x, y, btn):
if btn == self.button:
self.machine.endDrag(self.initPos, (x, y), btn)
- self.goto('idle')
+ self.goto("idle")
- def __init__(self,
- clickButtons=(LEFT_BTN, RIGHT_BTN),
- dragButtons=(LEFT_BTN,)):
+ def __init__(self, clickButtons=(LEFT_BTN, RIGHT_BTN), dragButtons=(LEFT_BTN,)):
states = {
- 'idle': self.Idle,
- 'click': self.Click,
- 'clickOrDrag': self.ClickOrDrag,
- 'drag': self.Drag
+ "idle": self.Idle,
+ "click": self.Click,
+ "clickOrDrag": self.ClickOrDrag,
+ "drag": self.Drag,
}
self.__clickButtons = set(clickButtons)
self.__dragButtons = set(dragButtons)
- super(ClickOrDrag, self).__init__(states, 'idle')
+ super(ClickOrDrag, self).__init__(states, "idle")
- clickButtons = property(lambda self: self.__clickButtons,
- doc="Buttons with click interaction (Set[int])")
+ clickButtons = property(
+ lambda self: self.__clickButtons,
+ doc="Buttons with click interaction (Set[int])",
+ )
- dragButtons = property(lambda self: self.__dragButtons,
- doc="Buttons with drag interaction (Set[int])")
+ dragButtons = property(
+ lambda self: self.__dragButtons, doc="Buttons with drag interaction (Set[int])"
+ )
def click(self, x, y, btn):
"""Called upon a button supporting click.
diff --git a/src/silx/gui/plot/ItemsSelectionDialog.py b/src/silx/gui/plot/ItemsSelectionDialog.py
index c0504b0..b4e4f9e 100644
--- a/src/silx/gui/plot/ItemsSelectionDialog.py
+++ b/src/silx/gui/plot/ItemsSelectionDialog.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -44,6 +43,7 @@ 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):
@@ -88,8 +88,10 @@ class KindsSelector(qt.QListWidget):
def selectAll(self):
"""Select all available kinds."""
- if self.selectionMode() in [qt.QAbstractItemView.SingleSelection,
- qt.QAbstractItemView.NoSelection]:
+ 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)
@@ -103,6 +105,7 @@ class PlotItemsSelector(qt.QTableWidget):
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")
@@ -132,8 +135,9 @@ class PlotItemsSelector(qt.QTableWidget):
: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))
+ raise KeyError(
+ "Illegal plot item kinds: %s" % set(kinds) - set(PlotWidget.ITEM_KINDS)
+ )
self.plot_item_kinds = kinds
self.updatePlotItems()
@@ -200,6 +204,7 @@ class ItemsSelectionDialog(qt.QDialog):
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")
@@ -212,7 +217,8 @@ class ItemsSelectionDialog(qt.QDialog):
self.kind_selector = KindsSelector(self)
self.kind_selector.setToolTip(
- "select one or more item kinds to show them in the item list")
+ "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")
@@ -262,25 +268,26 @@ class ItemsSelectionDialog(qt.QDialog):
: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.")
+ 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.")
+ "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.")
+ "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.")
+ "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.")
+ raise ValueError("The NoSelection mode is not allowed " "in this context.")
self.item_selector.setSelectionMode(mode)
diff --git a/src/silx/gui/plot/LegendSelector.py b/src/silx/gui/plot/LegendSelector.py
index d439387..22348fb 100755
--- a/src/silx/gui/plot/LegendSelector.py
+++ b/src/silx/gui/plot/LegendSelector.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -40,6 +39,7 @@ import numpy
from .. import qt, colors
from ..widgets.LegendIconWidget import LegendIconWidget
from . import items
+from ...utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -87,11 +87,10 @@ class LegendIcon(LegendIconWidget):
self._update()
def _update(self):
- """Update widget according to current curve state.
- """
+ """Update widget according to current curve state."""
curve = self.getCurve()
if curve is None:
- _logger.error('Curve no more exists')
+ _logger.error("Curve no more exists")
self.setEnabled(False)
return
@@ -105,11 +104,10 @@ class LegendIcon(LegendIconWidget):
color = style.getColor()
if numpy.array(color, copy=False).ndim != 1:
# array of colors, use transparent black
- color = 0., 0., 0., 0.
+ color = 0.0, 0.0, 0.0, 0.0
color = colors.rgba(color) # Make sure it is float in [0, 1]
alpha = curve.getAlpha()
- color = qt.QColor.fromRgbF(
- color[0], color[1], color[2], color[3] * alpha)
+ color = qt.QColor.fromRgbF(color[0], color[1], color[2], color[3] * alpha)
self.setLineColor(color)
self.setSymbolColor(color)
self.update() # TODO this should not be needed
@@ -119,15 +117,17 @@ class LegendIcon(LegendIconWidget):
:param event: Kind of change
"""
- if event in (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SYMBOL,
- items.ItemChangedType.SYMBOL_SIZE,
- items.ItemChangedType.LINE_WIDTH,
- items.ItemChangedType.LINE_STYLE,
- items.ItemChangedType.COLOR,
- items.ItemChangedType.ALPHA,
- items.ItemChangedType.HIGHLIGHTED,
- items.ItemChangedType.HIGHLIGHTED_STYLE):
+ if event in (
+ items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.SYMBOL,
+ items.ItemChangedType.SYMBOL_SIZE,
+ items.ItemChangedType.LINE_WIDTH,
+ items.ItemChangedType.LINE_STYLE,
+ items.ItemChangedType.COLOR,
+ items.ItemChangedType.ALPHA,
+ items.ItemChangedType.HIGHLIGHTED,
+ items.ItemChangedType.HIGHLIGHTED_STYLE,
+ ):
self._update()
@@ -143,12 +143,14 @@ class LegendModel(qt.QAbstractListModel):
- symbol
- visibility of the symbols
"""
+
iconColorRole = qt.Qt.UserRole + 0
iconLineWidthRole = qt.Qt.UserRole + 1
iconLineStyleRole = qt.Qt.UserRole + 2
showLineRole = qt.Qt.UserRole + 3
iconSymbolRole = qt.Qt.UserRole + 4
showSymbolRole = qt.Qt.UserRole + 5
+ itemRole = qt.Qt.UserRole + 6
def __init__(self, legendList=None, parent=None):
super(LegendModel, self).__init__(parent)
@@ -160,16 +162,14 @@ class LegendModel(qt.QAbstractListModel):
def __getitem__(self, idx):
if idx >= len(self.legendList):
- raise IndexError('list index out of range')
+ raise IndexError("list index out of range")
return self.legendList[idx]
def rowCount(self, modelIndex=None):
return len(self.legendList)
def flags(self, index):
- return (qt.Qt.ItemIsEditable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsSelectable)
+ return qt.Qt.ItemIsEditable | qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable
def data(self, modelIndex, role):
if modelIndex.isValid:
@@ -177,7 +177,7 @@ class LegendModel(qt.QAbstractListModel):
else:
return None
if idx >= len(self.legendList):
- raise IndexError('list index out of range')
+ raise IndexError("list index out of range")
item = self.legendList[idx]
isActive = item[1].get("active", False)
@@ -187,7 +187,7 @@ class LegendModel(qt.QAbstractListModel):
return legend
elif role == qt.Qt.SizeHintRole:
# size = qt.QSize(200,50)
- _logger.warning('LegendModel -- size hint role not implemented')
+ _logger.warning("LegendModel -- size hint role not implemented")
return qt.QSize()
elif role == qt.Qt.TextAlignmentRole:
alignment = qt.Qt.AlignVCenter | qt.Qt.AlignLeft
@@ -195,7 +195,7 @@ class LegendModel(qt.QAbstractListModel):
elif role == qt.Qt.BackgroundRole:
# Background color, must be QBrush
if isActive:
- brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.Highlight)
+ brush = self._palette.brush(qt.QPalette.Active, qt.QPalette.Highlight)
elif idx % 2:
brush = qt.QBrush(qt.QColor(240, 240, 240))
else:
@@ -204,28 +204,32 @@ class LegendModel(qt.QAbstractListModel):
elif role == qt.Qt.ForegroundRole:
# ForegroundRole color, must be QBrush
if isActive:
- brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.HighlightedText)
+ brush = self._palette.brush(
+ qt.QPalette.Active, qt.QPalette.HighlightedText
+ )
else:
- brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.WindowText)
+ brush = self._palette.brush(qt.QPalette.Active, qt.QPalette.WindowText)
return brush
elif role == qt.Qt.CheckStateRole:
return bool(item[2]) # item[2] == True
elif role == qt.Qt.ToolTipRole or role == qt.Qt.StatusTipRole:
- return ''
+ return ""
elif role == self.iconColorRole:
- return item[1]['color']
+ return item[1]["color"]
elif role == self.iconLineWidthRole:
- return item[1]['linewidth']
+ return item[1]["linewidth"]
elif role == self.iconLineStyleRole:
- return item[1]['linestyle']
+ return item[1]["linestyle"]
elif role == self.iconSymbolRole:
- return item[1]['symbol']
+ return item[1]["symbol"]
elif role == self.showLineRole:
return item[3]
elif role == self.showSymbolRole:
return item[4]
+ elif role == self.itemRole:
+ return item[5]
else:
- _logger.info('Unkown role requested: %s', str(role))
+ _logger.info("Unkown role requested: %s", str(role))
return None
def setData(self, modelIndex, value, role):
@@ -235,8 +239,7 @@ class LegendModel(qt.QAbstractListModel):
return None
if idx >= len(self.legendList):
# raise IndexError('list index out of range')
- _logger.warning(
- 'setData -- List index out of range, idx: %d', idx)
+ _logger.warning("setData -- List index out of range, idx: %d", idx)
return None
item = self.legendList[idx]
@@ -245,22 +248,25 @@ class LegendModel(qt.QAbstractListModel):
# Set legend
item[0] = str(value)
elif role == self.iconColorRole:
- item[1]['color'] = qt.QColor(value)
+ item[1]["color"] = qt.QColor(value)
elif role == self.iconLineWidthRole:
- item[1]['linewidth'] = int(value)
+ item[1]["linewidth"] = int(value)
elif role == self.iconLineStyleRole:
- item[1]['linestyle'] = str(value)
+ item[1]["linestyle"] = value
elif role == self.iconSymbolRole:
- item[1]['symbol'] = str(value)
+ item[1]["symbol"] = str(value)
elif role == qt.Qt.CheckStateRole:
item[2] = value
elif role == self.showLineRole:
item[3] = value
elif role == self.showSymbolRole:
item[4] = value
+ elif role == self.itemRole:
+ item[5] = value
except ValueError:
- _logger.warning('Conversion failed:\n\tvalue: %s\n\trole: %s',
- str(value), str(role))
+ _logger.warning(
+ "Conversion failed:\n\tvalue: %s\n\trole: %s", str(value), str(role)
+ )
# Can that be right? Read docs again..
self.dataChanged.emit(modelIndex, modelIndex)
return True
@@ -273,44 +279,45 @@ class LegendModel(qt.QAbstractListModel):
"""
modelIndex = self.createIndex(row, 0)
count = len(llist)
- super(LegendModel, self).beginInsertRows(modelIndex,
- row,
- row + count)
+ super(LegendModel, self).beginInsertRows(modelIndex, row, row + count)
head = self.legendList[0:row]
tail = self.legendList[row:]
new = []
- for (legend, icon) in llist:
- linestyle = icon.get('linestyle', None)
+ for legend, icon in llist:
+ linestyle = icon.get("linestyle", None)
if LegendIconWidget.isEmptyLineStyle(linestyle):
# Curve had no line, give it one and hide it
# So when toggle line, it will display a solid line
showLine = False
- icon['linestyle'] = '-'
+ icon["linestyle"] = "-"
else:
showLine = True
- symbol = icon.get('symbol', None)
+ symbol = icon.get("symbol", None)
if LegendIconWidget.isEmptySymbol(symbol):
# Curve had no symbol, give it one and hide it
# So when toggle symbol, it will display 'o'
showSymbol = False
- icon['symbol'] = 'o'
+ icon["symbol"] = "o"
else:
showSymbol = True
- selected = icon.get('selected', True)
- item = [legend,
- icon,
- selected,
- showLine,
- showSymbol]
+ selected = icon.get("selected", True)
+ item = [
+ legend,
+ icon,
+ selected,
+ showLine,
+ showSymbol,
+ icon.get("item", None),
+ ]
new.append(item)
self.legendList = head + new + tail
super(LegendModel, self).endInsertRows()
return True
def insertRows(self, row, count, modelIndex=qt.QModelIndex()):
- raise NotImplementedError('Use LegendModel.insertLegendList instead')
+ raise NotImplementedError("Use LegendModel.insertLegendList instead")
def removeRow(self, row):
return self.removeRows(row, 1)
@@ -321,14 +328,13 @@ class LegendModel(qt.QAbstractListModel):
# Nothing to do..
return True
if row < 0 or row >= length:
- raise IndexError('Index out of range -- ' +
- 'idx: %d, len: %d' % (row, length))
+ raise IndexError(
+ "Index out of range -- " + "idx: %d, len: %d" % (row, length)
+ )
if count == 0:
return False
- super(LegendModel, self).beginRemoveRows(modelIndex,
- row,
- row + count)
- del(self.legendList[row:row + count])
+ super(LegendModel, self).beginRemoveRows(modelIndex, row, row + count)
+ del self.legendList[row : row + count]
super(LegendModel, self).endRemoveRows()
return True
@@ -339,8 +345,7 @@ class LegendModel(qt.QAbstractListModel):
:type editor: QWidget
"""
if event not in self.eventList:
- raise ValueError('setEditor -- Event must be in %s' %
- str(self.eventList))
+ raise ValueError("setEditor -- Event must be in %s" % str(self.eventList))
self.editorDict[event] = editor
@@ -381,12 +386,11 @@ class LegendListItemWidget(qt.QItemDelegate):
iconSize = self.icon.sizeHint()
# Calculate icon position
x = rect.left() + 2
- y = rect.top() + int(.5 * (rect.height() - iconSize.height()))
+ y = rect.top() + int(0.5 * (rect.height() - iconSize.height()))
iconRect = qt.QRect(qt.QPoint(x, y), iconSize)
# Calculate label rectangle
- legendSize = qt.QSize(rect.width() - iconSize.width() - 30,
- rect.height())
+ legendSize = qt.QSize(rect.width() - iconSize.width() - 30, rect.height())
# Calculate label position
x = rect.left() + iconRect.width()
y = rect.top()
@@ -444,8 +448,7 @@ class LegendListItemWidget(qt.QItemDelegate):
else:
checkState = qt.Qt.Unchecked
- self.drawCheck(
- painter, qt.QStyleOptionViewItem(), chBoxRect, checkState)
+ self.drawCheck(painter, qt.QStyleOptionViewItem(), chBoxRect, checkState)
painter.restore()
@@ -454,7 +457,11 @@ class LegendListItemWidget(qt.QItemDelegate):
# Mouse events are sent to editorEvent()
# even if they don't start editing of the item.
if event.button() == qt.Qt.RightButton and self.contextMenu:
- self.contextMenu.exec(event.globalPos(), modelIndex)
+ if qt.BINDING == "PyQt5":
+ position = event.globalPos()
+ else: # Qt6
+ position = event.globalPosition().toPoint()
+ self.contextMenu.exec(position, modelIndex)
return True
elif event.button() == qt.Qt.LeftButton:
# Check if checkbox was clicked
@@ -462,26 +469,29 @@ class LegendListItemWidget(qt.QItemDelegate):
cbRect = self.cbDict[idx]
if cbRect.contains(event.pos()):
# Toggle checkbox
- model.setData(modelIndex,
- not modelIndex.data(qt.Qt.CheckStateRole),
- qt.Qt.CheckStateRole)
+ model.setData(
+ modelIndex,
+ not modelIndex.data(qt.Qt.CheckStateRole),
+ qt.Qt.CheckStateRole,
+ )
event.ignore()
return True
else:
return super(LegendListItemWidget, self).editorEvent(
- event, model, option, modelIndex)
+ event, model, option, modelIndex
+ )
def createEditor(self, parent, option, idx):
- _logger.info('### Editor request ###')
+ _logger.info("### Editor request ###")
def sizeHint(self, option, idx):
# return qt.QSize(68,24)
iconSize = self.icon.sizeHint()
legendSize = self.legend.sizeHint()
checkboxSize = self.checkbox.sizeHint()
- height = max([iconSize.height(),
- legendSize.height(),
- checkboxSize.height()]) + 4
+ height = (
+ max([iconSize.height(), legendSize.height(), checkboxSize.height()]) + 4
+ )
width = iconSize.width() + legendSize.width() + checkboxSize.width()
return qt.QSize(width, height)
@@ -492,9 +502,9 @@ class LegendListView(qt.QListView):
sigLegendSignal = qt.Signal(object)
"""Signal emitting a dict when an action is triggered by the user."""
- __mouseClickedEvent = 'mouseClicked'
- __checkBoxClickedEvent = 'checkBoxClicked'
- __legendClickedEvent = 'legendClicked'
+ __mouseClickedEvent = "mouseClicked"
+ __checkBoxClickedEvent = "checkBoxClicked"
+ __legendClickedEvent = "legendClicked"
def __init__(self, parent=None, model=None, contextMenu=None):
super(LegendListView, self).__init__(parent)
@@ -540,47 +550,55 @@ class LegendListView(qt.QListView):
model.setData(modelIndex, new_legend, qt.Qt.DisplayRole)
color = modelIndex.data(LegendModel.iconColorRole)
- new_color = icon.get('color', None)
+ new_color = icon.get("color", None)
if new_color != color:
model.setData(modelIndex, new_color, LegendModel.iconColorRole)
linewidth = modelIndex.data(LegendModel.iconLineWidthRole)
- new_linewidth = icon.get('linewidth', 1.0)
+ new_linewidth = icon.get("linewidth", 1.0)
if new_linewidth != linewidth:
- model.setData(modelIndex, new_linewidth, LegendModel.iconLineWidthRole)
+ model.setData(
+ modelIndex, new_linewidth, LegendModel.iconLineWidthRole
+ )
linestyle = modelIndex.data(LegendModel.iconLineStyleRole)
- new_linestyle = icon.get('linestyle', None)
+ new_linestyle = icon.get("linestyle", None)
visible = not LegendIconWidget.isEmptyLineStyle(new_linestyle)
model.setData(modelIndex, visible, LegendModel.showLineRole)
if new_linestyle != linestyle:
- model.setData(modelIndex, new_linestyle, LegendModel.iconLineStyleRole)
+ model.setData(
+ modelIndex, new_linestyle, LegendModel.iconLineStyleRole
+ )
symbol = modelIndex.data(LegendModel.iconSymbolRole)
- new_symbol = icon.get('symbol', None)
+ new_symbol = icon.get("symbol", None)
visible = not LegendIconWidget.isEmptySymbol(new_symbol)
model.setData(modelIndex, visible, LegendModel.showSymbolRole)
if new_symbol != symbol:
model.setData(modelIndex, new_symbol, LegendModel.iconSymbolRole)
selected = modelIndex.data(qt.Qt.CheckStateRole)
- new_selected = icon.get('selected', True)
+ new_selected = icon.get("selected", True)
if new_selected != selected:
model.setData(modelIndex, new_selected, qt.Qt.CheckStateRole)
- _logger.debug('LegendListView.setLegendList(legendList) finished')
+
+ item = modelIndex.data(LegendModel.itemRole)
+ newItem = icon.get("item", None)
+ if item is not newItem:
+ model.setData(modelIndex, newItem, LegendModel.itemRole)
+ _logger.debug("LegendListView.setLegendList(legendList) finished")
def clear(self):
model = self.model()
model.removeRows(0, model.rowCount())
- _logger.debug('LegendListView.clear() finished')
+ _logger.debug("LegendListView.clear() finished")
def setContextMenu(self, contextMenu=None):
delegate = self.itemDelegate()
if isinstance(delegate, LegendListItemWidget) and self.model():
if contextMenu is None:
delegate.contextMenu = LegendListContextMenu(self.model())
- delegate.contextMenu.sigContextMenu.connect(
- self._contextMenuSlot)
+ delegate.contextMenu.sigContextMenu.connect(self._contextMenuSlot)
else:
delegate.contextMenu = contextMenu
@@ -633,12 +651,11 @@ class LegendListView(qt.QListView):
:param QModelIndex modelIndex: index of the clicked item
"""
- _logger.debug('self._handleMouseClick called')
- if self.__lastButton not in [qt.Qt.LeftButton,
- qt.Qt.RightButton]:
+ _logger.debug("self._handleMouseClick called")
+ if self.__lastButton not in [qt.Qt.LeftButton, qt.Qt.RightButton]:
return
if not modelIndex.isValid():
- _logger.debug('_handleMouseClick -- Invalid QModelIndex')
+ _logger.debug("_handleMouseClick -- Invalid QModelIndex")
return
# model = self.model()
idx = modelIndex.row()
@@ -654,30 +671,29 @@ class LegendListView(qt.QListView):
# TODO: Check for doubleclicks on legend/icon and spawn editors
ddict = {
- 'legend': str(modelIndex.data(qt.Qt.DisplayRole)),
- 'icon': {
- 'linewidth': str(modelIndex.data(
- LegendModel.iconLineWidthRole)),
- 'linestyle': str(modelIndex.data(
- LegendModel.iconLineStyleRole)),
- 'symbol': str(modelIndex.data(LegendModel.iconSymbolRole))
+ "legend": str(modelIndex.data(qt.Qt.DisplayRole)),
+ "icon": {
+ "linewidth": str(modelIndex.data(LegendModel.iconLineWidthRole)),
+ "linestyle": modelIndex.data(LegendModel.iconLineStyleRole),
+ "symbol": str(modelIndex.data(LegendModel.iconSymbolRole)),
},
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data())
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
}
if self.__lastButton == qt.Qt.RightButton:
- _logger.debug('Right clicked')
- ddict['button'] = "right"
- ddict['event'] = self.__mouseClickedEvent
+ _logger.debug("Right clicked")
+ ddict["button"] = "right"
+ ddict["event"] = self.__mouseClickedEvent
elif cbClicked:
- _logger.debug('CheckBox clicked')
- ddict['button'] = "left"
- ddict['event'] = self.__checkBoxClickedEvent
+ _logger.debug("CheckBox clicked")
+ ddict["button"] = "left"
+ ddict["event"] = self.__checkBoxClickedEvent
else:
- _logger.debug('Legend clicked')
- ddict['button'] = "left"
- ddict['event'] = self.__legendClickedEvent
- _logger.debug(' idx: %d\n ddict: %s', idx, str(ddict))
+ _logger.debug("Legend clicked")
+ ddict["button"] = "left"
+ ddict["event"] = self.__legendClickedEvent
+ _logger.debug(" idx: %d\n ddict: %s", idx, str(ddict))
self.sigLegendSignal.emit(ddict)
@@ -691,29 +707,26 @@ class LegendListContextMenu(qt.QMenu):
super(LegendListContextMenu, self).__init__(parent=None)
self.model = model
- self.addAction('Set Active', self.setActiveAction)
- self.addAction('Map to left', self.mapToLeftAction)
- self.addAction('Map to right', self.mapToRightAction)
+ self.addAction("Set Active", self.setActiveAction)
+ self.addAction("Map to left", self.mapToLeftAction)
+ self.addAction("Map to right", self.mapToRightAction)
- self._pointsAction = self.addAction(
- 'Points', self.togglePointsAction)
+ self._pointsAction = self.addAction("Points", self.togglePointsAction)
self._pointsAction.setCheckable(True)
- self._linesAction = self.addAction('Lines', self.toggleLinesAction)
+ self._linesAction = self.addAction("Lines", self.toggleLinesAction)
self._linesAction.setCheckable(True)
- self.addAction('Remove curve', self.removeItemAction)
- self.addAction('Rename curve', self.renameItemAction)
+ self.addAction("Remove curve", self.removeItemAction)
+ self.addAction("Rename curve", self.renameItemAction)
def exec(self, pos, idx):
self.__currentIdx = idx
# Set checkable action state
modelIndex = self.currentIdx()
- self._pointsAction.setChecked(
- modelIndex.data(LegendModel.showSymbolRole))
- self._linesAction.setChecked(
- modelIndex.data(LegendModel.showLineRole))
+ self._pointsAction.setChecked(modelIndex.data(LegendModel.showSymbolRole))
+ self._linesAction.setChecked(modelIndex.data(LegendModel.showLineRole))
super(LegendListContextMenu, self).popup(pos)
@@ -724,55 +737,59 @@ class LegendListContextMenu(qt.QMenu):
return self.__currentIdx
def mapToLeftAction(self):
- _logger.debug('LegendListContextMenu.mapToLeftAction called')
+ _logger.debug("LegendListContextMenu.mapToLeftAction called")
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "mapToLeft"
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "mapToLeft",
}
self.sigContextMenu.emit(ddict)
def mapToRightAction(self):
- _logger.debug('LegendListContextMenu.mapToRightAction called')
+ _logger.debug("LegendListContextMenu.mapToRightAction called")
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "mapToRight"
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "mapToRight",
}
self.sigContextMenu.emit(ddict)
def removeItemAction(self):
- _logger.debug('LegendListContextMenu.removeCurveAction called')
+ _logger.debug("LegendListContextMenu.removeCurveAction called")
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "removeCurve"
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "removeCurve",
}
self.model.removeRow(modelIndex.row())
self.sigContextMenu.emit(ddict)
def renameItemAction(self):
- _logger.debug('LegendListContextMenu.renameCurveAction called')
+ _logger.debug("LegendListContextMenu.renameCurveAction called")
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "renameCurve"
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "renameCurve",
}
self.sigContextMenu.emit(ddict)
@@ -780,17 +797,18 @@ class LegendListContextMenu(qt.QMenu):
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "type": str(modelIndex.data()),
}
linestyle = modelIndex.data(LegendModel.iconLineStyleRole)
visible = not modelIndex.data(LegendModel.showLineRole)
- _logger.debug('toggleLinesAction -- lines visible: %s', str(visible))
- ddict['event'] = "toggleLine"
- ddict['line'] = visible
- ddict['linestyle'] = linestyle if visible else ''
+ _logger.debug("toggleLinesAction -- lines visible: %s", str(visible))
+ ddict["event"] = "toggleLine"
+ ddict["line"] = visible
+ ddict["linestyle"] = linestyle if visible else ""
self.model.setData(modelIndex, visible, LegendModel.showLineRole)
self.sigContextMenu.emit(ddict)
@@ -798,33 +816,34 @@ class LegendListContextMenu(qt.QMenu):
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
}
flag = modelIndex.data(LegendModel.showSymbolRole)
symbol = modelIndex.data(LegendModel.iconSymbolRole)
visible = not flag or LegendIconWidget.isEmptySymbol(symbol)
- _logger.debug(
- 'togglePointsAction -- Symbols visible: %s', str(visible))
+ _logger.debug("togglePointsAction -- Symbols visible: %s", str(visible))
- ddict['event'] = "togglePoints"
- ddict['points'] = visible
- ddict['symbol'] = symbol if visible else ''
+ ddict["event"] = "togglePoints"
+ ddict["points"] = visible
+ ddict["symbol"] = symbol if visible else ""
self.model.setData(modelIndex, visible, LegendModel.showSymbolRole)
self.sigContextMenu.emit(ddict)
def setActiveAction(self):
modelIndex = self.currentIdx()
legend = str(modelIndex.data(qt.Qt.DisplayRole))
- _logger.debug('setActiveAction -- active curve: %s', legend)
+ _logger.debug("setActiveAction -- active curve: %s", legend)
ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "setActiveCurve",
+ "legend": legend,
+ "label": legend,
+ "selected": modelIndex.data(qt.Qt.CheckStateRole),
+ "type": str(modelIndex.data()),
+ "item": modelIndex.data(LegendModel.itemRole),
+ "event": "setActiveCurve",
}
self.sigContextMenu.emit(ddict)
@@ -843,10 +862,10 @@ class RenameCurveDialog(qt.QDialog):
self.hboxLayout = qt.QHBoxLayout(self.hbox)
self.hboxLayout.addStretch(1)
self.okButton = qt.QPushButton(self.hbox)
- self.okButton.setText('OK')
+ self.okButton.setText("OK")
self.hboxLayout.addWidget(self.okButton)
self.cancelButton = qt.QPushButton(self.hbox)
- self.cancelButton.setText('Cancel')
+ self.cancelButton.setText("Cancel")
self.hboxLayout.addWidget(self.cancelButton)
self.hboxLayout.addStretch(1)
layout.addWidget(self.lineEdit)
@@ -896,8 +915,7 @@ class LegendsDockWidget(qt.QDockWidget):
self.layout().setContentsMargins(0, 0, 0, 0)
self.setWidget(self._legendWidget)
- self.visibilityChanged.connect(
- self._visibilityChangedHandler)
+ self.visibilityChanged.connect(self._visibilityChangedHandler)
self._legendWidget.sigLegendSignal.connect(self._legendSignalHandler)
@@ -906,6 +924,7 @@ class LegendsDockWidget(qt.QDockWidget):
"""The :class:`.PlotWindow` this widget is attached to."""
return self._plotRef()
+ @deprecated(reason="No longer needed", since_version="2.0.0")
def renameCurve(self, oldLegend, newLegend):
"""Change the name of a curve using remove and addCurve
@@ -914,88 +933,77 @@ class LegendsDockWidget(qt.QDockWidget):
"""
is_active = self.plot.getActiveCurve(just_legend=True) == oldLegend
curve = self.plot.getCurve(oldLegend)
- self.plot.remove(oldLegend, kind='curve')
- self.plot.addCurve(curve.getXData(copy=False),
- curve.getYData(copy=False),
- legend=newLegend,
- info=curve.getInfo(),
- color=curve.getColor(),
- symbol=curve.getSymbol(),
- linewidth=curve.getLineWidth(),
- linestyle=curve.getLineStyle(),
- xlabel=curve.getXLabel(),
- ylabel=curve.getYLabel(),
- xerror=curve.getXErrorData(copy=False),
- yerror=curve.getYErrorData(copy=False),
- z=curve.getZValue(),
- selectable=curve.isSelectable(),
- fill=curve.isFill(),
- resetzoom=False)
+ self.plot.remove(oldLegend, kind="curve")
+ self.plot.addCurve(
+ curve.getXData(copy=False),
+ curve.getYData(copy=False),
+ legend=newLegend,
+ info=curve.getInfo(),
+ color=curve.getColor(),
+ symbol=curve.getSymbol(),
+ linewidth=curve.getLineWidth(),
+ linestyle=curve.getLineStyle(),
+ xlabel=curve.getXLabel(),
+ ylabel=curve.getYLabel(),
+ xerror=curve.getXErrorData(copy=False),
+ yerror=curve.getYErrorData(copy=False),
+ z=curve.getZValue(),
+ selectable=curve.isSelectable(),
+ fill=curve.isFill(),
+ resetzoom=False,
+ )
if is_active:
self.plot.setActiveCurve(newLegend)
def _legendSignalHandler(self, ddict):
"""Handles events from the LegendListView signal"""
_logger.debug("Legend signal ddict = %s", str(ddict))
+ # If item is not provided, retrieve it from its legend
+ curve = ddict.get("item", None)
+ if curve is None:
+ curve = self.plot.getCurve(ddict["legend"])
- if ddict['event'] == "legendClicked":
- if ddict['button'] == "left":
- self.plot.setActiveCurve(ddict['legend'])
+ if ddict["event"] == "legendClicked":
+ if ddict["button"] == "left":
+ self.plot.setActiveCurve(curve)
- elif ddict['event'] == "removeCurve":
- self.plot.removeCurve(ddict['legend'])
+ elif ddict["event"] == "removeCurve":
+ self.plot.removeItem(curve)
- elif ddict['event'] == "renameCurve":
+ elif ddict["event"] == "renameCurve":
curveList = self.plot.getAllCurves(just_legend=True)
- oldLegend = ddict['legend']
+ oldLegend = ddict["legend"]
dialog = RenameCurveDialog(self.plot, oldLegend, curveList)
ret = dialog.exec()
if ret:
newLegend = dialog.getText()
- self.renameCurve(oldLegend, newLegend)
-
- elif ddict['event'] == "setActiveCurve":
- self.plot.setActiveCurve(ddict['legend'])
-
- elif ddict['event'] == "checkBoxClicked":
- self.plot.hideCurve(ddict['legend'], not ddict['selected'])
-
- elif ddict['event'] in ["mapToRight", "mapToLeft"]:
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- yaxis = 'right' if ddict['event'] == 'mapToRight' else 'left'
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- yaxis=yaxis)
-
- elif ddict['event'] == "togglePoints":
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- symbol = ddict['symbol'] if ddict['points'] else ''
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- symbol=symbol)
-
- elif ddict['event'] == "toggleLine":
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- linestyle = ddict['linestyle'] if ddict['line'] else ''
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- linestyle=linestyle)
+ wasActive = self.plot.getActiveCurve() is curve
+ self.plot.removeItem(curve)
+ curve.setName(newLegend)
+ self.plot.addItem(curve)
+ if wasActive:
+ self.plot.setActiveCurve(curve)
+
+ elif ddict["event"] == "setActiveCurve":
+ self.plot.setActiveCurve(curve)
+
+ elif ddict["event"] == "checkBoxClicked":
+ curve.setVisible(ddict["selected"])
+
+ elif ddict["event"] in ["mapToRight", "mapToLeft"]:
+ curve.setYAxis("right" if ddict["event"] == "mapToRight" else "left")
+
+ elif ddict["event"] == "togglePoints":
+ curve.setSymbol(ddict["symbol"] if ddict["points"] else "")
+
+ elif ddict["event"] == "toggleLine":
+ curve.setLineStyle(ddict["linestyle"] if ddict["line"] else "")
else:
- _logger.debug("unhandled event %s", str(ddict['event']))
+ _logger.debug("unhandled event %s", str(ddict["event"]))
def updateLegends(self, *args):
- """Sync the LegendSelector widget displayed info with the plot.
- """
+ """Sync the LegendSelector widget displayed info with the plot."""
legendList = []
for curve in self.plot.getAllCurves(withhidden=True):
legend = curve.getName()
@@ -1005,15 +1013,17 @@ class LegendsDockWidget(qt.QDockWidget):
color = style.getColor()
if numpy.array(color, copy=False).ndim != 1:
# array of colors, use transparent black
- color = 0., 0., 0., 0.
+ color = 0.0, 0.0, 0.0, 0.0
curveInfo = {
- 'color': qt.QColor.fromRgbF(*color),
- 'linewidth': style.getLineWidth(),
- 'linestyle': style.getLineStyle(),
- 'symbol': style.getSymbol(),
- 'selected': not self.plot.isCurveHidden(legend),
- 'active': isActive}
+ "color": qt.QColor.fromRgbF(*color),
+ "linewidth": style.getLineWidth(),
+ "linestyle": style.getLineStyle(),
+ "symbol": style.getSymbol(),
+ "selected": not self.plot.isCurveHidden(legend),
+ "active": isActive,
+ "item": curve,
+ }
legendList.append((legend, curveInfo))
self._legendWidget.setLegendList(legendList)
@@ -1030,10 +1040,3 @@ class LegendsDockWidget(qt.QDockWidget):
self.plot.sigContentChanged.disconnect(self.updateLegends)
self.plot.sigActiveCurveChanged.disconnect(self.updateLegends)
self._isConnected = False
-
- def showEvent(self, event):
- """Make sure this widget is raised when it is shown
- (when it is first created as a tab in PlotWindow or when it is shown
- again after hiding).
- """
- self.raise_()
diff --git a/src/silx/gui/plot/LimitsHistory.py b/src/silx/gui/plot/LimitsHistory.py
index a323548..f4e0afc 100644
--- a/src/silx/gui/plot/LimitsHistory.py
+++ b/src/silx/gui/plot/LimitsHistory.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017 European Synchrotron Radiation Facility
@@ -56,8 +55,8 @@ class LimitsHistory(qt.QObject):
"""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()
+ 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):
diff --git a/src/silx/gui/plot/MaskToolsWidget.py b/src/silx/gui/plot/MaskToolsWidget.py
index 522be48..40b2717 100644
--- a/src/silx/gui/plot/MaskToolsWidget.py
+++ b/src/silx/gui/plot/MaskToolsWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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,7 +29,6 @@ This widget is meant to work with :class:`silx.gui.plot.PlotWidget`.
- :class:`MaskToolsWidget`: GUI for :class:`Mask`
- :class:`MaskToolsDockWidget`: DockWidget to integrate in :class:`PlotWindow`
"""
-from __future__ import division
__authors__ = ["T. Vincent", "P. Knobel"]
__license__ = "MIT"
@@ -40,9 +38,12 @@ import os
import sys
import numpy
import logging
-import collections
import h5py
+import fabio
+from fabio.edfimage import EdfImage
+from fabio.TiffIO import TiffIO
+
from silx.image import shapes
from silx.io.utils import NEXUS_HDF5_EXT, is_dataset
from silx.gui.dialog.DatasetDialog import DatasetDialog
@@ -53,14 +54,10 @@ from ..colors import cursorColorForColormap, rgba
from .. import qt
from ..utils import LockReentrant
-from silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
-
-import fabio
_logger = logging.getLogger(__name__)
-_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT])
+_HDF5_EXT_STR = " ".join(["*" + ext for ext in NEXUS_HDF5_EXT])
def _selectDataset(filename, mode=DatasetDialog.SaveMode):
@@ -112,16 +109,17 @@ class ImageMask(BaseMask):
or 'msk' (if FabIO is installed)
:raise Exception: Raised if the file writing fail
"""
- if kind == 'edf':
- edfFile = EdfFile(filename, access="w+")
- header = {"program_name": "silx-mask", "masked_value": "nonzero"}
- edfFile.WriteImage(header, self.getMask(copy=False), Append=0)
+ if kind == "edf":
+ EdfImage(
+ data=self.getMask(),
+ header={"program_name": "silx-mask", "masked_value": "nonzero"},
+ ).write(filename)
- elif kind == 'tif':
- tiffFile = TiffIO(filename, mode='w')
- tiffFile.writeImage(self.getMask(copy=False), software='silx')
+ elif kind == "tif":
+ tiffFile = TiffIO(filename, mode="w")
+ tiffFile.writeImage(self.getMask(copy=False), software="silx")
- elif kind == 'npy':
+ elif kind == "npy":
try:
numpy.save(filename, self.getMask(copy=False))
except IOError:
@@ -130,7 +128,7 @@ class ImageMask(BaseMask):
elif ("." + kind) in NEXUS_HDF5_EXT:
self._saveToHdf5(filename, self.getMask(copy=False))
- elif kind == 'msk':
+ elif kind == "msk":
try:
data = self.getMask(copy=False)
image = fabio.fabioimage.FabioImage(data=data)
@@ -161,10 +159,11 @@ class ImageMask(BaseMask):
existing_ds = h5f.get(dataPath)
if existing_ds is not None:
reply = qt.QMessageBox.question(
- None,
- "Confirm overwrite",
- "Do you want to overwrite an existing dataset?",
- qt.QMessageBox.Yes | qt.QMessageBox.No)
+ None,
+ "Confirm overwrite",
+ "Do you want to overwrite an existing dataset?",
+ qt.QMessageBox.Yes | qt.QMessageBox.No,
+ )
if reply != qt.QMessageBox.Yes:
return False
del h5f[dataPath]
@@ -188,10 +187,11 @@ class ImageMask(BaseMask):
assert 0 < level < 256
if row + height <= 0 or col + width <= 0:
return # Rectangle outside image, avoid negative indices
- selection = self._mask[max(0, row):row + height + 1,
- max(0, col):col + width + 1]
+ selection = self._mask[
+ max(0, row) : row + height + 1, max(0, col) : col + width + 1
+ ]
if mask:
- selection[:,:] = level
+ selection[:, :] = level
else:
selection[selection == level] = 0
self._notify()
@@ -207,8 +207,7 @@ class ImageMask(BaseMask):
if mask:
self._mask[fill != 0] = level
else:
- self._mask[numpy.logical_and(fill != 0,
- self._mask == level)] = 0
+ self._mask[numpy.logical_and(fill != 0, self._mask == level)] = 0
self._notify()
def updatePoints(self, level, rows, cols, mask=True):
@@ -223,8 +222,8 @@ class ImageMask(BaseMask):
"""
valid = numpy.logical_and(
numpy.logical_and(rows >= 0, cols >= 0),
- numpy.logical_and(rows < self._mask.shape[0],
- cols < self._mask.shape[1]))
+ numpy.logical_and(rows < self._mask.shape[0], cols < self._mask.shape[1]),
+ )
rows, cols = rows[valid], cols[valid]
if mask:
@@ -280,10 +279,9 @@ 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
+ super(MaskToolsWidget, self).__init__(parent, plot, mask=ImageMask())
+ self._origin = (0.0, 0.0) # Mask origin in plot
+ self._scale = (1.0, 1.0) # Mask scale in plot
self._z = 1 # Mask layer in plot
self._data = numpy.zeros((0, 0), dtype=numpy.uint8) # Store image
@@ -338,11 +336,11 @@ class MaskToolsWidget(BaseMaskToolsWidget):
mask = numpy.array(mask, copy=False, dtype=numpy.uint8)
if len(mask.shape) != 2:
- _logger.error('Not an image, shape: %d', len(mask.shape))
+ _logger.error("Not an image, shape: %d", len(mask.shape))
return None
# Handle mask with single level
- if self.multipleMasks() == 'single':
+ if self.multipleMasks() == "single":
mask = numpy.array(mask != 0, dtype=numpy.uint8)
# if mask has not changed, do nothing
@@ -354,15 +352,17 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._mask.commit()
return mask.shape
else:
- _logger.warning('Mask has not the same size as current image.'
- ' Mask will be cropped or padded to fit image'
- ' dimensions. %s != %s',
- str(mask.shape), str(self._data.shape))
- resizedMask = numpy.zeros(self._data.shape[0:2],
- dtype=numpy.uint8)
+ _logger.warning(
+ "Mask has not the same size as current image."
+ " Mask will be cropped or padded to fit image"
+ " dimensions. %s != %s",
+ str(mask.shape),
+ str(self._data.shape),
+ )
+ resizedMask = numpy.zeros(self._data.shape[0:2], dtype=numpy.uint8)
height = min(self._data.shape[0], mask.shape[0])
width = min(self._data.shape[1], mask.shape[1])
- resizedMask[:height,:width] = mask[:height,:width]
+ resizedMask[:height, :width] = mask[:height, :width]
self._mask.setMask(resizedMask, copy=False)
self._mask.commit()
return resizedMask.shape
@@ -389,12 +389,13 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self.plot.addItem(maskItem)
elif self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
+ self.plot.remove(self._maskName, kind="image")
def showEvent(self, event):
try:
self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
+ self._activeImageChangedAfterCare
+ )
except (RuntimeError, TypeError):
pass
@@ -404,8 +405,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
def hideEvent(self, event):
try:
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
+ self.plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
except (RuntimeError, TypeError):
pass
@@ -418,7 +418,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if self.isMaskInteractionActivated():
# Disable drawing tool
- self.browseAction.trigger()
+ self.plot.resetInteractiveMode()
if self.isItemMaskUpdated(): # No "after-care"
self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
@@ -426,11 +426,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._mask.reset()
if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
+ self.plot.remove(self._maskName, kind="image")
elif self.getSelectionMask(copy=False) is not None:
- self.plot.sigActiveImageChanged.connect(
- self._activeImageChangedAfterCare)
+ self.plot.sigActiveImageChanged.connect(self._activeImageChangedAfterCare)
def _activeImageChanged(self, previous, current):
"""Reacts upon active image change.
@@ -450,10 +449,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
"""
if isinstance(image, items.ColormapMixIn):
colormap = image.getColormap()
- self._defaultOverlayColor = rgba(
- cursorColorForColormap(colormap['name']))
+ self._defaultOverlayColor = rgba(cursorColorForColormap(colormap["name"]))
else:
- self._defaultOverlayColor = rgba('black')
+ self._defaultOverlayColor = rgba("black")
def _activeImageChangedAfterCare(self, *args):
"""Check synchro of active image and mask when mask widget is hidden.
@@ -469,15 +467,17 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._mask.reset()
if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
+ self.plot.remove(self._maskName, kind="image")
self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
+ self._activeImageChangedAfterCare
+ )
else:
self._setOverlayColorForImage(activeImage)
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._setMaskColors(
+ self.levelSpinBox.value(),
+ self.transparencySlider.value() / self.transparencySlider.maximum(),
+ )
self._origin = activeImage.getOrigin()
self._scale = activeImage.getScale()
@@ -486,10 +486,11 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if self._data.shape[:2] != self._mask.getMask(copy=False).shape:
# Image has not the same size, remove mask and stop listening
if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
+ self.plot.remove(self._maskName, kind="image")
self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
+ self._activeImageChangedAfterCare
+ )
else:
# Refresh in case origin, scale, z changed
self._mask.setDataItem(activeImage)
@@ -521,11 +522,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if self.isItemMaskUpdated():
if image.getMaskData(copy=False) is None:
# Image item has no mask: use current mask from the tool
- image.setMaskData(
- self.getSelectionMask(copy=False), copy=True)
+ image.setMaskData(self.getSelectionMask(copy=False), copy=True)
else: # Image item has a mask: set it in tool
- self.setSelectionMask(
- image.getMaskData(copy=False), copy=True)
+ self.setSelectionMask(image.getMaskData(copy=False), copy=True)
self._mask.resetHistory()
self.__imageUpdated()
if self.isVisible():
@@ -538,17 +537,21 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.error("Mask is not attached to an image")
return
- if event in (items.ItemChangedType.COLORMAP,
- items.ItemChangedType.DATA,
- items.ItemChangedType.POSITION,
- items.ItemChangedType.SCALE,
- items.ItemChangedType.VISIBLE,
- items.ItemChangedType.ZVALUE):
+ if event in (
+ items.ItemChangedType.COLORMAP,
+ items.ItemChangedType.DATA,
+ items.ItemChangedType.POSITION,
+ items.ItemChangedType.SCALE,
+ items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.ZVALUE,
+ ):
self.__imageUpdated()
- elif (event == items.ItemChangedType.MASK and
- self.isItemMaskUpdated() and
- not self.__itemMaskUpdatedLock.locked()):
+ elif (
+ event == items.ItemChangedType.MASK
+ and self.isItemMaskUpdated()
+ and not self.__itemMaskUpdatedLock.locked()
+ ):
# Update mask from the image item unless mask tool is updating it
self.setSelectionMask(image.getMaskData(copy=False), copy=True)
@@ -561,9 +564,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self._setOverlayColorForImage(image)
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._setMaskColors(
+ self.levelSpinBox.value(),
+ self.transparencySlider.value() / self.transparencySlider.maximum(),
+ )
self._origin = image.getOrigin()
self._scale = image.getScale()
@@ -604,26 +608,11 @@ 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)
- except Exception as e:
- _logger.error("Can't load filename %s", filename)
- _logger.debug("Backtrace", exc_info=True)
- raise e
- elif extension == "msk":
+ elif extension in ("edf", "msk", "tif", "tiff"):
try:
mask = fabio.open(filename).data
except Exception as e:
- _logger.error("Can't load fit2d mask file")
+ _logger.error(f"Can't load filename {filename}")
_logger.debug("Backtrace", exc_info=True)
raise e
elif ("." + extension) in NEXUS_HDF5_EXT:
@@ -638,7 +627,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if effectiveMaskShape is None:
return
if mask.shape != effectiveMaskShape:
- msg = 'Mask was resized from %s to %s'
+ msg = "Mask was resized from %s to %s"
msg = msg % (str(mask.shape), str(effectiveMaskShape))
raise RuntimeWarning(msg)
@@ -648,7 +637,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
dialog.setWindowTitle("Load Mask")
dialog.setModal(1)
- extensions = collections.OrderedDict()
+ extensions = {}
extensions["EDF files"] = "*.edf"
extensions["TIFF files"] = "*.tif *.tiff"
extensions["NumPy binary files"] = "*.npy"
@@ -714,17 +703,17 @@ class MaskToolsWidget(BaseMaskToolsWidget):
"""Open Save mask dialog"""
dialog = qt.QFileDialog(self)
dialog.setWindowTitle("Save Mask")
- dialog.setOption(dialog.DontUseNativeDialog)
+ dialog.setOption(qt.QFileDialog.DontUseNativeDialog)
dialog.setModal(1)
- hdf5Filter = 'HDF5 (%s)' % _HDF5_EXT_STR
+ hdf5Filter = "HDF5 (%s)" % _HDF5_EXT_STR
filters = [
- 'EDF (*.edf)',
- 'TIFF (*.tif)',
- 'NumPy binary file (*.npy)',
+ "EDF (*.edf)",
+ "TIFF (*.tif)",
+ "NumPy binary file (*.npy)",
hdf5Filter,
# Fit2D mask is displayed anyway fabio is here or not
# to show to the user that the option exists
- 'Fit2D mask (*.msk)',
+ "Fit2D mask (*.msk)",
]
dialog.setNameFilters(filters)
dialog.setFileMode(qt.QFileDialog.AnyFile)
@@ -735,9 +724,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
# disable overwrite confirmation for HDF5,
# because we append the data to existing files
if filt_ == hdf5Filter:
- dialog.setOption(dialog.DontConfirmOverwrite)
+ dialog.setOption(qt.QFileDialog.DontConfirmOverwrite)
else:
- dialog.setOption(dialog.DontConfirmOverwrite, False)
+ dialog.setOption(qt.QFileDialog.DontConfirmOverwrite, False)
dialog.filterSelected.connect(onFilterSelection)
if not dialog.exec():
@@ -751,8 +740,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
if "HDF5" in nameFilter:
has_allowed_ext = False
for ext in NEXUS_HDF5_EXT:
- if (len(filename) > len(ext) and
- filename[-len(ext):].lower() == ext.lower()):
+ if (
+ len(filename) > len(ext)
+ and filename[-len(ext) :].lower() == ext.lower()
+ ):
has_allowed_ext = True
extension = ext
if not has_allowed_ext:
@@ -776,8 +767,7 @@ class MaskToolsWidget(BaseMaskToolsWidget):
strerror = e.strerror
else:
strerror = sys.exc_info()[1]
- msg.setText("Cannot save.\n"
- "Input Output Error: %s" % strerror)
+ msg.setText("Cannot save.\n" "Input Output Error: %s" % strerror)
msg.exec()
return
@@ -805,8 +795,10 @@ class MaskToolsWidget(BaseMaskToolsWidget):
def _plotDrawEvent(self, event):
"""Handle draw events from the plot"""
- if (self._drawingMode is None or
- event['event'] not in ('drawingProgress', 'drawingFinished')):
+ if self._drawingMode is None or event["event"] not in (
+ "drawingProgress",
+ "drawingFinished",
+ ):
return
if not len(self._data):
@@ -814,56 +806,54 @@ class MaskToolsWidget(BaseMaskToolsWidget):
level = self.levelSpinBox.value()
- if self._drawingMode == 'rectangle':
- if event['event'] == 'drawingFinished':
+ if self._drawingMode == "rectangle":
+ if event["event"] == "drawingFinished":
# Convert from plot to array coords
doMask = self._isMasking()
ox, oy = self._origin
sx, sy = self._scale
- height = int(abs(event['height'] / sy))
- width = int(abs(event['width'] / sx))
+ height = int(abs(event["height"] / sy))
+ width = int(abs(event["width"] / sx))
- row = int((event['y'] - oy) / sy)
+ row = int((event["y"] - oy) / sy)
if sy < 0:
row -= height
- col = int((event['x'] - ox) / sx)
+ col = int((event["x"] - ox) / sx)
if sx < 0:
col -= width
self._mask.updateRectangle(
- level,
- row=row,
- col=col,
- height=height,
- width=width,
- mask=doMask)
+ level, row=row, col=col, height=height, width=width, mask=doMask
+ )
self._mask.commit()
- elif self._drawingMode == 'ellipse':
- if event['event'] == 'drawingFinished':
+ elif self._drawingMode == "ellipse":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
# Convert from plot to array coords
- center = (event['points'][0] - self._origin) / self._scale
- size = event['points'][1] / self._scale
+ center = (event["points"][0] - self._origin) / self._scale
+ size = event["points"][1] / self._scale
center = center.astype(numpy.int64) # (row, col)
- self._mask.updateEllipse(level, center[1], center[0], size[1], size[0], doMask)
+ self._mask.updateEllipse(
+ level, center[1], center[0], size[1], size[0], doMask
+ )
self._mask.commit()
- elif self._drawingMode == 'polygon':
- if event['event'] == 'drawingFinished':
+ elif self._drawingMode == "polygon":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
# Convert from plot to array coords
- vertices = (event['points'] - self._origin) / self._scale
+ vertices = (event["points"] - self._origin) / self._scale
vertices = vertices.astype(numpy.int64)[:, (1, 0)] # (row, col)
self._mask.updatePolygon(level, vertices, doMask)
self._mask.commit()
- elif self._drawingMode == 'pencil':
+ elif self._drawingMode == "pencil":
doMask = self._isMasking()
# convert from plot to array coords
- col, row = (event['points'][-1] - self._origin) / self._scale
+ col, row = (event["points"][-1] - self._origin) / self._scale
col, row = int(col), int(row)
brushSize = self._getPencilWidth()
@@ -872,15 +862,18 @@ class MaskToolsWidget(BaseMaskToolsWidget):
# Draw the line
self._mask.updateLine(
level,
- self._lastPencilPos[0], self._lastPencilPos[1],
- row, col,
+ self._lastPencilPos[0],
+ self._lastPencilPos[1],
+ row,
+ col,
brushSize,
- doMask)
+ doMask,
+ )
# Draw the very first, or last point
- self._mask.updateDisk(level, row, col, brushSize / 2., doMask)
+ self._mask.updateDisk(level, row, col, brushSize / 2.0, doMask)
- if event['event'] == 'drawingFinished':
+ if event["event"] == "drawingFinished":
self._mask.commit()
self._lastPencilPos = None
else:
@@ -891,15 +884,17 @@ class MaskToolsWidget(BaseMaskToolsWidget):
def _loadRangeFromColormapTriggered(self):
"""Set range from active image colormap range"""
activeImage = self.plot.getActiveImage()
- if (isinstance(activeImage, items.ColormapMixIn) and
- activeImage.getName() != self._maskName):
+ if (
+ isinstance(activeImage, items.ColormapMixIn)
+ and activeImage.getName() != self._maskName
+ ):
# Update thresholds according to colormap
colormap = activeImage.getColormap()
- if colormap['autoscale']:
+ if colormap["autoscale"]:
min_ = numpy.nanmin(activeImage.getData(copy=False))
max_ = numpy.nanmax(activeImage.getData(copy=False))
else:
- min_, max_ = colormap['vmin'], colormap['vmax']
+ min_, max_ = colormap["vmin"], colormap["vmax"]
self.minLineEdit.setText(str(min_))
self.maxLineEdit.setText(str(max_))
@@ -914,6 +909,6 @@ class MaskToolsDockWidget(BaseMaskToolsDockWidget):
:paran str name: The title of this widget
"""
- def __init__(self, parent=None, plot=None, name='Mask'):
+ def __init__(self, parent=None, plot=None, name="Mask"):
widget = MaskToolsWidget(plot=plot)
super(MaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/src/silx/gui/plot/PlotActions.py b/src/silx/gui/plot/PlotActions.py
deleted file mode 100644
index dd16221..0000000
--- a/src/silx/gui/plot/PlotActions.py
+++ /dev/null
@@ -1,67 +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.
-#
-# ###########################################################################*/
-"""Depracted module linking old PlotAction with the actions.xxx"""
-
-
-__author__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/06/2017"
-
-from silx.utils.deprecation import deprecated_warning
-
-deprecated_warning(type_='module',
- name=__file__,
- reason='PlotActions refactoring',
- replacement='plot.actions',
- since_version='0.6')
-
-from .actions import PlotAction
-
-from .actions.io import CopyAction
-from .actions.io import PrintAction
-from .actions.io import SaveAction
-
-from .actions.control import ColormapAction
-from .actions.control import CrosshairAction
-from .actions.control import CurveStyleAction
-from .actions.control import GridAction
-from .actions.control import KeepAspectRatioAction
-from .actions.control import PanWithArrowKeysAction
-from .actions.control import ResetZoomAction
-from .actions.control import XAxisAutoScaleAction
-from .actions.control import XAxisLogarithmicAction
-from .actions.control import YAxisAutoScaleAction
-from .actions.control import YAxisLogarithmicAction
-from .actions.control import YAxisInvertedAction
-from .actions.control import ZoomInAction
-from .actions.control import ZoomOutAction
-
-from .actions.medfilt import MedianFilter1DAction
-from .actions.medfilt import MedianFilter2DAction
-from .actions.medfilt import MedianFilterAction
-
-from .actions.histogram import PixelIntensitiesHistoAction
-
-from .actions.fit import FitAction
diff --git a/src/silx/gui/plot/PlotEvents.py b/src/silx/gui/plot/PlotEvents.py
index 83f253c..b4cbe30 100644
--- a/src/silx/gui/plot/PlotEvents.py
+++ b/src/silx/gui/plot/PlotEvents.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
@@ -34,60 +33,71 @@ import numpy as np
def prepareDrawingSignal(event, type_, points, parameters=None):
"""See Plot documentation for content of events"""
- assert event in ('drawingProgress', 'drawingFinished')
+ assert event in ("drawingProgress", "drawingFinished")
if parameters is None:
parameters = {}
eventDict = {}
- eventDict['event'] = event
- eventDict['type'] = type_
+ eventDict["event"] = event
+ eventDict["type"] = type_
points = np.array(points, dtype=np.float32)
points.shape = -1, 2
- eventDict['points'] = points
- eventDict['xdata'] = points[:, 0]
- eventDict['ydata'] = points[:, 1]
- if type_ in ('rectangle',):
- eventDict['x'] = eventDict['xdata'].min()
- eventDict['y'] = eventDict['ydata'].min()
- eventDict['width'] = eventDict['xdata'].max() - eventDict['x']
- eventDict['height'] = eventDict['ydata'].max() - eventDict['y']
- eventDict['parameters'] = parameters.copy()
+ eventDict["points"] = points
+ eventDict["xdata"] = points[:, 0]
+ eventDict["ydata"] = points[:, 1]
+ if type_ in ("rectangle",):
+ eventDict["x"] = eventDict["xdata"].min()
+ eventDict["y"] = eventDict["ydata"].min()
+ eventDict["width"] = eventDict["xdata"].max() - eventDict["x"]
+ eventDict["height"] = eventDict["ydata"].max() - eventDict["y"]
+ eventDict["parameters"] = parameters.copy()
return eventDict
def prepareMouseSignal(eventType, button, xData, yData, xPixel, yPixel):
"""See Plot documentation for content of events"""
- assert eventType in ('mouseMoved', 'mouseClicked', 'mouseDoubleClicked')
- assert button in (None, 'left', 'middle', 'right')
+ assert eventType in ("mouseMoved", "mouseClicked", "mouseDoubleClicked")
+ assert button in (None, "left", "middle", "right")
- return {'event': eventType,
- 'x': xData,
- 'y': yData,
- 'xpixel': xPixel,
- 'ypixel': yPixel,
- 'button': button}
+ return {
+ "event": eventType,
+ "x": xData,
+ "y": yData,
+ "xpixel": xPixel,
+ "ypixel": yPixel,
+ "button": button,
+ }
def prepareHoverSignal(label, type_, posData, posPixel, draggable, selectable):
"""See Plot documentation for content of events"""
- return {'event': 'hover',
- 'label': label,
- 'type': type_,
- 'x': posData[0],
- 'y': posData[1],
- 'xpixel': posPixel[0],
- 'ypixel': posPixel[1],
- 'draggable': draggable,
- 'selectable': selectable}
-
-
-def prepareMarkerSignal(eventType, button, label, type_,
- draggable, selectable,
- posDataMarker,
- posPixelCursor=None, posDataCursor=None):
+ return {
+ "event": "hover",
+ "label": label,
+ "type": type_,
+ "x": posData[0],
+ "y": posData[1],
+ "xpixel": posPixel[0],
+ "ypixel": posPixel[1],
+ "draggable": draggable,
+ "selectable": selectable,
+ }
+
+
+def prepareMarkerSignal(
+ eventType,
+ button,
+ label,
+ type_,
+ draggable,
+ selectable,
+ posDataMarker,
+ posPixelCursor=None,
+ posDataCursor=None,
+):
"""See Plot documentation for content of events"""
- if eventType == 'markerClicked':
+ if eventType == "markerClicked":
assert posPixelCursor is not None
assert posDataCursor is None
@@ -97,11 +107,11 @@ def prepareMarkerSignal(eventType, button, label, type_,
if hasattr(posDataCursor[1], "__len__"):
posDataCursor[1] = posDataCursor[1][-1]
- elif eventType == 'markerMoving':
+ elif eventType == "markerMoving":
assert posPixelCursor is not None
assert posDataCursor is not None
- elif eventType == 'markerMoved':
+ elif eventType == "markerMoved":
assert posPixelCursor is None
assert posDataCursor is None
@@ -109,58 +119,66 @@ def prepareMarkerSignal(eventType, button, label, type_,
else:
raise NotImplementedError("Unknown event type {0}".format(eventType))
- eventDict = {'event': eventType,
- 'button': button,
- 'label': label,
- 'type': type_,
- 'x': posDataCursor[0],
- 'y': posDataCursor[1],
- 'xdata': posDataMarker[0],
- 'ydata': posDataMarker[1],
- 'draggable': draggable,
- 'selectable': selectable}
-
- if eventType in ('markerMoving', 'markerClicked'):
- eventDict['xpixel'] = posPixelCursor[0]
- eventDict['ypixel'] = posPixelCursor[1]
+ eventDict = {
+ "event": eventType,
+ "button": button,
+ "label": label,
+ "type": type_,
+ "x": posDataCursor[0],
+ "y": posDataCursor[1],
+ "xdata": posDataMarker[0],
+ "ydata": posDataMarker[1],
+ "draggable": draggable,
+ "selectable": selectable,
+ }
+
+ if eventType in ("markerMoving", "markerClicked"):
+ eventDict["xpixel"] = posPixelCursor[0]
+ eventDict["ypixel"] = posPixelCursor[1]
return eventDict
-def prepareImageSignal(button, label, type_, col, row,
- x, y, xPixel, yPixel):
+def prepareImageSignal(button, item, col, row, x, y, xPixel, yPixel):
"""See Plot documentation for content of events"""
- return {'event': 'imageClicked',
- 'button': button,
- 'label': label,
- 'type': type_,
- 'col': col,
- 'row': row,
- 'x': x,
- 'y': y,
- 'xpixel': xPixel,
- 'ypixel': yPixel}
-
-
-def prepareCurveSignal(button, label, type_, xData, yData,
- x, y, xPixel, yPixel):
+ return {
+ "event": "imageClicked",
+ "button": button,
+ "item": item,
+ "label": item.getName(),
+ "type": "image",
+ "col": col,
+ "row": row,
+ "x": x,
+ "y": y,
+ "xpixel": xPixel,
+ "ypixel": yPixel,
+ }
+
+
+def prepareCurveSignal(button, item, xData, yData, x, y, xPixel, yPixel):
"""See Plot documentation for content of events"""
- return {'event': 'curveClicked',
- 'button': button,
- 'label': label,
- 'type': type_,
- 'xdata': xData,
- 'ydata': yData,
- 'x': x,
- 'y': y,
- 'xpixel': xPixel,
- 'ypixel': yPixel}
+ return {
+ "event": "curveClicked",
+ "button": button,
+ "item": item,
+ "label": item.getName(),
+ "type": "curve",
+ "xdata": xData,
+ "ydata": yData,
+ "x": x,
+ "y": y,
+ "xpixel": xPixel,
+ "ypixel": yPixel,
+ }
def prepareLimitsChangedSignal(sourceObj, xRange, yRange, y2Range):
"""See Plot documentation for content of events"""
- return {'event': 'limitsChanged',
- 'source': id(sourceObj),
- 'xdata': xRange,
- 'ydata': yRange,
- 'y2data': y2Range}
+ return {
+ "event": "limitsChanged",
+ "source": id(sourceObj),
+ "xdata": xRange,
+ "ydata": yRange,
+ "y2data": y2Range,
+ }
diff --git a/src/silx/gui/plot/PlotInteraction.py b/src/silx/gui/plot/PlotInteraction.py
index 6ebe6b1..d19bb6d 100644
--- a/src/silx/gui/plot/PlotInteraction.py
+++ b/src/silx/gui/plot/PlotInteraction.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,6 +23,8 @@
# ###########################################################################*/
"""Implementation of the interaction for the :class:`Plot`."""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/02/2019"
@@ -33,30 +34,53 @@ import math
import numpy
import time
import weakref
+from typing import NamedTuple, Optional
+from silx.gui import qt
from .. import colors
-from .. import qt
from . import items
-from .Interaction import (ClickOrDrag, LEFT_BTN, RIGHT_BTN, MIDDLE_BTN,
- State, StateMachine)
-from .PlotEvents import (prepareCurveSignal, prepareDrawingSignal,
- prepareHoverSignal, prepareImageSignal,
- prepareMarkerSignal, prepareMouseSignal)
-
-from .backends.BackendBase import (CURSOR_POINTING, CURSOR_SIZE_HOR,
- CURSOR_SIZE_VER, CURSOR_SIZE_ALL)
-
-from ._utils import (FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX,
- applyZoomToPlot)
+from .Interaction import (
+ ClickOrDrag,
+ LEFT_BTN,
+ RIGHT_BTN,
+ MIDDLE_BTN,
+ State,
+ StateMachine,
+)
+from .PlotEvents import (
+ prepareCurveSignal,
+ prepareDrawingSignal,
+ prepareHoverSignal,
+ prepareImageSignal,
+ prepareMarkerSignal,
+ prepareMouseSignal,
+)
+
+from .backends.BackendBase import (
+ CURSOR_POINTING,
+ CURSOR_SIZE_HOR,
+ CURSOR_SIZE_VER,
+ CURSOR_SIZE_ALL,
+)
+
+from ._utils import (
+ FLOAT32_SAFE_MIN,
+ FLOAT32_MINPOS,
+ FLOAT32_SAFE_MAX,
+ applyZoomToPlot,
+ EnabledAxes,
+)
# Base class ##################################################################
+
class _PlotInteraction(object):
"""Base class for interaction handler.
It provides a weakref to the plot and methods to set/reset overlay.
"""
+
def __init__(self, plot):
"""Init.
@@ -72,7 +96,7 @@ class _PlotInteraction(object):
assert plot is not None
return plot
- def setSelectionArea(self, points, fill, color, name='', shape='polygon'):
+ def setSelectionArea(self, points, fill, color, name="", shape="polygon"):
"""Set a polygon selection area overlaid on the plot.
Multiple simultaneous areas are supported through the name parameter.
@@ -84,7 +108,7 @@ class _PlotInteraction(object):
:param name: The key associated with this selection area
:param str shape: Shape of the area in 'polygon', 'polylines'
"""
- assert shape in ('polygon', 'polylines')
+ assert shape in ("polygon", "polylines")
if color is None:
return
@@ -92,9 +116,9 @@ class _PlotInteraction(object):
points = numpy.asarray(points)
# TODO Not very nice, but as is for now
- legend = '__SELECTION_AREA__' + name
+ legend = "__SELECTION_AREA__" + name
- fill = fill != 'none' # TODO not very nice either
+ fill = fill != "none" # TODO not very nice either
greyed = colors.greyed(color)[0]
if greyed < 0.5:
@@ -102,36 +126,39 @@ class _PlotInteraction(object):
else:
color2 = "black"
- self.plot.addShape(points[:, 0], points[:, 1], legend=legend,
- replace=False,
- shape=shape, fill=fill,
- color=color, linebgcolor=color2, linestyle="--",
- overlay=True)
+ self.plot.addShape(
+ points[:, 0],
+ points[:, 1],
+ legend=legend,
+ replace=False,
+ shape=shape,
+ fill=fill,
+ color=color,
+ gapcolor=color2,
+ linestyle="--",
+ overlay=True,
+ )
self._selectionAreas.add(legend)
def resetSelectionArea(self):
"""Remove all selection areas set by setSelectionArea."""
for legend in self._selectionAreas:
- self.plot.remove(legend, kind='item')
+ self.plot.remove(legend, kind="item")
self._selectionAreas = set()
# Zoom/Pan ####################################################################
-class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
- """:class:`ClickOrDrag` state machine with zooming on mouse wheel.
+
+class _PlotInteractionWithClickEvents(ClickOrDrag, _PlotInteraction):
+ """:class:`ClickOrDrag` state machine emitting click and double click events.
Base class for :class:`Pan` and :class:`Zoom`
"""
_DOUBLE_CLICK_TIMEOUT = 0.4
- class Idle(ClickOrDrag.Idle):
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.machine.plot, scaleF, (x, y))
-
def click(self, x, y, btn):
"""Handle clicks by sending events
@@ -145,18 +172,19 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
# Signal mouse double clicked event first
if (time.time() - lastClickTime) <= self._DOUBLE_CLICK_TIMEOUT:
# Use position of first click
- eventDict = prepareMouseSignal('mouseDoubleClicked', 'left',
- *lastClickPos)
+ eventDict = prepareMouseSignal(
+ "mouseDoubleClicked", "left", *lastClickPos
+ )
self.plot.notify(**eventDict)
- self._lastClick = 0., None
+ self._lastClick = 0.0, None
else:
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', 'left',
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareMouseSignal(
+ "mouseClicked", "left", dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**eventDict)
self._lastClick = time.time(), (dataPos[0], dataPos[1], x, y)
@@ -165,9 +193,9 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', 'right',
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareMouseSignal(
+ "mouseClicked", "right", dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**eventDict)
def __init__(self, plot, **kwargs):
@@ -175,7 +203,7 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
:param plot: The plot to apply modifications to.
"""
- self._lastClick = 0., None
+ self._lastClick = 0.0, None
_PlotInteraction.__init__(self, plot)
ClickOrDrag.__init__(self, **kwargs)
@@ -183,12 +211,13 @@ class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
# Pan #########################################################################
-class Pan(_ZoomOnWheel):
+
+class Pan(_PlotInteractionWithClickEvents):
"""Pan plot content and zoom on wheel state machine."""
def _pixelToData(self, x, y):
xData, yData = self.plot.pixelToData(x, y)
- _, y2Data = self.plot.pixelToData(x, y, axis='right')
+ _, y2Data = self.plot.pixelToData(x, y, axis="right")
return xData, yData, y2Data
def beginDrag(self, x, y, btn):
@@ -200,13 +229,13 @@ class Pan(_ZoomOnWheel):
xMin, xMax = self.plot.getXAxis().getLimits()
yMin, yMax = self.plot.getYAxis().getLimits()
- y2Min, y2Max = self.plot.getYAxis(axis='right').getLimits()
+ y2Min, y2Max = self.plot.getYAxis(axis="right").getLimits()
if self.plot.getXAxis()._isLogarithmic():
try:
dx = math.log10(xData) - math.log10(lastX)
- newXMin = pow(10., (math.log10(xMin) - dx))
- newXMax = pow(10., (math.log10(xMax) - dx))
+ newXMin = pow(10.0, (math.log10(xMin) - dx))
+ newXMax = pow(10.0, (math.log10(xMax) - dx))
except (ValueError, OverflowError):
newXMin, newXMax = xMin, xMax
@@ -224,19 +253,23 @@ class Pan(_ZoomOnWheel):
if self.plot.getYAxis()._isLogarithmic():
try:
dy = math.log10(yData) - math.log10(lastY)
- newYMin = pow(10., math.log10(yMin) - dy)
- newYMax = pow(10., math.log10(yMax) - dy)
+ newYMin = pow(10.0, math.log10(yMin) - dy)
+ newYMax = pow(10.0, math.log10(yMax) - dy)
dy2 = math.log10(y2Data) - math.log10(lastY2)
- newY2Min = pow(10., math.log10(y2Min) - dy2)
- newY2Max = pow(10., math.log10(y2Max) - dy2)
+ newY2Min = pow(10.0, math.log10(y2Min) - dy2)
+ newY2Max = pow(10.0, math.log10(y2Max) - dy2)
except (ValueError, OverflowError):
newYMin, newYMax = yMin, yMax
newY2Min, newY2Max = y2Min, y2Max
# Makes sure y and y2 stays in positive float32 range
- if (newYMin < FLOAT32_MINPOS or newYMax > FLOAT32_SAFE_MAX or
- newY2Min < FLOAT32_MINPOS or newY2Max > FLOAT32_SAFE_MAX):
+ if (
+ newYMin < FLOAT32_MINPOS
+ or newYMax > FLOAT32_SAFE_MAX
+ or newY2Min < FLOAT32_MINPOS
+ or newY2Max > FLOAT32_SAFE_MAX
+ ):
newYMin, newYMax = yMin, yMax
newY2Min, newY2Max = y2Min, y2Max
else:
@@ -246,16 +279,16 @@ class Pan(_ZoomOnWheel):
newY2Min, newY2Max = y2Min - dy2, y2Max - dy2
# Makes sure y and y2 stays in float32 range
- if (newYMin < FLOAT32_SAFE_MIN or
- newYMax > FLOAT32_SAFE_MAX or
- newY2Min < FLOAT32_SAFE_MIN or
- newY2Max > FLOAT32_SAFE_MAX):
+ if (
+ newYMin < FLOAT32_SAFE_MIN
+ or newYMax > FLOAT32_SAFE_MAX
+ or newY2Min < FLOAT32_SAFE_MIN
+ or newY2Max > FLOAT32_SAFE_MAX
+ ):
newYMin, newYMax = yMin, yMax
newY2Min, newY2Max = y2Min, y2Max
- self.plot.setLimits(newXMin, newXMax,
- newYMin, newYMax,
- newY2Min, newY2Max)
+ self.plot.setLimits(newXMin, newXMax, newYMin, newYMax, newY2Min, newY2Max)
self._previousDataPos = self._pixelToData(x, y)
@@ -268,7 +301,17 @@ class Pan(_ZoomOnWheel):
# Zoom ########################################################################
-class Zoom(_ZoomOnWheel):
+
+class AxesExtent(NamedTuple):
+ xmin: float
+ xmax: float
+ ymin: float
+ ymax: float
+ y2min: float
+ y2max: float
+
+
+class Zoom(_PlotInteractionWithClickEvents):
"""Zoom-in/out state machine.
Zoom-in on selected area, zoom-out on right click,
@@ -279,34 +322,67 @@ class Zoom(_ZoomOnWheel):
def __init__(self, plot, color):
self.color = color
+ self.enabledAxes = EnabledAxes()
super(Zoom, self).__init__(plot)
self.plot.getLimitsHistory().clear()
- def _areaWithAspectRatio(self, x0, y0, x1, y1):
- _plotLeft, _plotTop, plotW, plotH = self.plot.getPlotBoundsInPixels()
-
- areaX0, areaY0, areaX1, areaY1 = x0, y0, x1, y1
-
- if plotH != 0.:
- plotRatio = plotW / float(plotH)
- width, height = math.fabs(x1 - x0), math.fabs(y1 - y0)
-
- if height != 0. and width != 0.:
- if width / height > plotRatio:
- areaHeight = width / plotRatio
- areaX0, areaX1 = x0, x1
+ def _getAxesExtent(
+ self,
+ x0: float,
+ y0: float,
+ x1: float,
+ y1: float,
+ enabledAxes: Optional[EnabledAxes] = None,
+ ) -> AxesExtent:
+ """Convert selection coordinates (pixels) to axes coordinates (data)
+
+ This takes into account axes selected for zoom and aspect ratio.
+ """
+ if enabledAxes is None:
+ enabledAxes = self.enabledAxes
+
+ y2_0, y2_1 = y0, y1
+ left, top, width, height = self.plot.getPlotBoundsInPixels()
+
+ if not all(enabledAxes) and not self.plot.isKeepDataAspectRatio():
+ # Handle axes disabled for zoom if plot is not keeping aspec ratio
+ if not enabledAxes.xaxis:
+ x0, x1 = left, left + width
+ if not enabledAxes.yaxis:
+ y0, y1 = top, top + height
+ if not enabledAxes.y2axis:
+ y2_0, y2_1 = top, top + height
+
+ if self.plot.isKeepDataAspectRatio() and height != 0 and width != 0:
+ ratio = width / height
+ xextent, yextent = math.fabs(x1 - x0), math.fabs(y1 - y0)
+ if xextent != 0 and yextent != 0:
+ if xextent / yextent > ratio:
+ areaHeight = xextent / ratio
center = 0.5 * (y0 + y1)
- areaY0 = center - numpy.sign(y1 - y0) * 0.5 * areaHeight
- areaY1 = center + numpy.sign(y1 - y0) * 0.5 * areaHeight
+ y0 = center - numpy.sign(y1 - y0) * 0.5 * areaHeight
+ y1 = center + numpy.sign(y1 - y0) * 0.5 * areaHeight
else:
- areaWidth = height * plotRatio
- areaY0, areaY1 = y0, y1
+ areaWidth = yextent * ratio
center = 0.5 * (x0 + x1)
- areaX0 = center - numpy.sign(x1 - x0) * 0.5 * areaWidth
- areaX1 = center + numpy.sign(x1 - x0) * 0.5 * areaWidth
+ x0 = center - numpy.sign(x1 - x0) * 0.5 * areaWidth
+ x1 = center + numpy.sign(x1 - x0) * 0.5 * areaWidth
- return areaX0, areaY0, areaX1, areaY1
+ # Convert to data space
+ x0, y0 = self.plot.pixelToData(x0, y0, check=False)
+ x1, y1 = self.plot.pixelToData(x1, y1, check=False)
+ y2_0 = self.plot.pixelToData(None, y2_0, axis="right", check=False)[1]
+ y2_1 = self.plot.pixelToData(None, y2_1, axis="right", check=False)[1]
+
+ return AxesExtent(
+ min(x0, x1),
+ max(x0, x1),
+ min(y0, y1),
+ max(y0, y1),
+ min(y2_0, y2_1),
+ max(y2_0, y2_1),
+ )
def beginDrag(self, x, y, btn):
dataPos = self.plot.pixelToData(x, y)
@@ -320,66 +396,54 @@ class Zoom(_ZoomOnWheel):
dataPos = self.plot.pixelToData(x1, y1)
assert dataPos is not None
- if self.plot.isKeepDataAspectRatio():
- area = self._areaWithAspectRatio(self.x0, self.y0, x1, y1)
- areaX0, areaY0, areaX1, areaY1 = area
- areaPoints = ((areaX0, areaY0),
- (areaX1, areaY0),
- (areaX1, areaY1),
- (areaX0, areaY1))
- areaPoints = numpy.array([self.plot.pixelToData(
- x, y, check=False) for (x, y) in areaPoints])
-
- if self.color != 'video inverted':
+ if self.plot.isKeepDataAspectRatio() or not all(self.enabledAxes):
+ # Patch enabledAxes to display the right Y axis area on the left Y axis
+ # since the selection area is always displayed on the left Y axis
+ isY2Visible = self.plot.getYAxis("right").isVisible()
+ areaZoomEnabledAxes = EnabledAxes(
+ self.enabledAxes.xaxis,
+ self.enabledAxes.yaxis and (not isY2Visible or self.enabledAxes.y2axis),
+ self.enabledAxes.y2axis,
+ )
+ extents = self._getAxesExtent(self.x0, self.y0, x1, y1, areaZoomEnabledAxes)
+ areaCorners = (
+ (extents.xmin, extents.ymin),
+ (extents.xmax, extents.ymin),
+ (extents.xmax, extents.ymax),
+ (extents.xmin, extents.ymax),
+ )
+
+ if self.color != "video inverted":
areaColor = list(self.color)
areaColor[3] *= 0.25
else:
- areaColor = [1., 1., 1., 1.]
+ areaColor = [1.0, 1.0, 1.0, 1.0]
- self.setSelectionArea(areaPoints,
- fill='none',
- color=areaColor,
- name="zoomedArea")
+ self.setSelectionArea(
+ areaCorners, fill="none", color=areaColor, name="zoomedArea"
+ )
- corners = ((self.x0, self.y0),
- (self.x0, y1),
- (x1, y1),
- (x1, self.y0))
- corners = numpy.array([self.plot.pixelToData(x, y, check=False)
- for (x, y) in corners])
+ corners = ((self.x0, self.y0), (self.x0, y1), (x1, y1), (x1, self.y0))
+ corners = numpy.array(
+ [self.plot.pixelToData(x, y, check=False) for (x, y) in corners]
+ )
- self.setSelectionArea(corners, fill='none', color=self.color)
+ self.setSelectionArea(corners, fill="none", color=self.color)
def _zoom(self, x0, y0, x1, y1):
- """Zoom to the rectangle view x0,y0 x1,y1.
- """
- startPos = x0, y0
- endPos = x1, y1
-
+ """Zoom to the rectangle view x0,y0 x1,y1."""
# Store current zoom state in stack
self.plot.getLimitsHistory().push()
- if self.plot.isKeepDataAspectRatio():
- x0, y0, x1, y1 = self._areaWithAspectRatio(x0, y0, x1, y1)
-
- # Convert to data space and set limits
- x0, y0 = self.plot.pixelToData(x0, y0, check=False)
-
- dataPos = self.plot.pixelToData(
- startPos[0], startPos[1], axis="right", check=False)
- y2_0 = dataPos[1]
-
- x1, y1 = self.plot.pixelToData(x1, y1, check=False)
-
- dataPos = self.plot.pixelToData(
- endPos[0], endPos[1], axis="right", check=False)
- y2_1 = dataPos[1]
-
- xMin, xMax = min(x0, x1), max(x0, x1)
- yMin, yMax = min(y0, y1), max(y0, y1)
- y2Min, y2Max = min(y2_0, y2_1), max(y2_0, y2_1)
-
- self.plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
+ extents = self._getAxesExtent(x0, y0, x1, y1)
+ self.plot.setLimits(
+ extents.xmin,
+ extents.xmax,
+ extents.ymin,
+ extents.ymax,
+ extents.y2min,
+ extents.y2max,
+ )
def endDrag(self, startPos, endPos, btn):
x0, y0 = startPos
@@ -392,12 +456,13 @@ class Zoom(_ZoomOnWheel):
self.resetSelectionArea()
def cancel(self):
- if isinstance(self.state, self.states['drag']):
+ if isinstance(self.state, self.states["drag"]):
self.resetSelectionArea()
# Select ######################################################################
+
class Select(StateMachine, _PlotInteraction):
"""Base class for drawing selection areas."""
@@ -413,13 +478,9 @@ class Select(StateMachine, _PlotInteraction):
self.parameters = parameters
StateMachine.__init__(self, states, state)
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.plot, scaleF, (x, y))
-
@property
def color(self):
- return self.parameters.get('color', None)
+ return self.parameters.get("color", None)
class SelectPolygon(Select):
@@ -430,7 +491,7 @@ class SelectPolygon(Select):
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('select', x, y)
+ self.goto("select", x, y)
return True
class Select(State):
@@ -447,25 +508,28 @@ class SelectPolygon(Select):
x, y = self.machine.plot.dataToPixel(*self._firstPos, check=False)
offset = self.machine.getDragThreshold()
- points = [(x - offset, y - offset),
- (x - offset, y + offset),
- (x + offset, y + offset),
- (x + offset, y - offset)]
- points = [self.machine.plot.pixelToData(xpix, ypix, check=False)
- for xpix, ypix in points]
- self.machine.setSelectionArea(points, fill=None,
- color=self.machine.color,
- name='first_point')
+ points = [
+ (x - offset, y - offset),
+ (x - offset, y + offset),
+ (x + offset, y + offset),
+ (x + offset, y - offset),
+ ]
+ points = [
+ self.machine.plot.pixelToData(xpix, ypix, check=False)
+ for xpix, ypix in points
+ ]
+ self.machine.setSelectionArea(
+ points, fill=None, color=self.machine.color, name="first_point"
+ )
def updateSelectionArea(self):
"""Update drawing selection area using self.points"""
- self.machine.setSelectionArea(self.points,
- fill='hatch',
- color=self.machine.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'polygon',
- self.points,
- self.machine.parameters)
+ self.machine.setSelectionArea(
+ self.points, fill="hatch", color=self.machine.color
+ )
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "polygon", self.points, self.machine.parameters
+ )
self.machine.plot.notify(**eventDict)
def validate(self):
@@ -479,12 +543,11 @@ class SelectPolygon(Select):
def closePolygon(self):
self.machine.resetSelectionArea()
self.points[-1] = self.points[0]
- eventDict = prepareDrawingSignal('drawingFinished',
- 'polygon',
- self.points,
- self.machine.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "polygon", self.points, self.machine.parameters
+ )
self.machine.plot.notify(**eventDict)
- self.goto('idle')
+ self.goto("idle")
def onWheel(self, x, y, angle):
self.machine.onWheel(x, y, angle)
@@ -494,8 +557,7 @@ class SelectPolygon(Select):
if btn == LEFT_BTN:
# checking if the position is close to the first point
# if yes : closing the "loop"
- firstPos = self.machine.plot.dataToPixel(*self._firstPos,
- check=False)
+ firstPos = self.machine.plot.dataToPixel(*self._firstPos, check=False)
dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)
threshold = self.machine.getDragThreshold()
@@ -517,8 +579,9 @@ class SelectPolygon(Select):
# in Idle state, but with a slightly different position that
# the mouse press. So we had the two first vertices that were
# almost identical.
- previousPos = self.machine.plot.dataToPixel(*self.points[-2],
- check=False)
+ previousPos = self.machine.plot.dataToPixel(
+ *self.points[-2], check=False
+ )
dx, dy = abs(previousPos[0] - x), abs(previousPos[1] - y)
if dx >= threshold or dy >= threshold:
self.points.append(dataPos)
@@ -529,8 +592,7 @@ class SelectPolygon(Select):
return False
def onMove(self, x, y):
- firstPos = self.machine.plot.dataToPixel(*self._firstPos,
- check=False)
+ firstPos = self.machine.plot.dataToPixel(*self._firstPos, check=False)
dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)
threshold = self.machine.getDragThreshold()
@@ -543,15 +605,11 @@ class SelectPolygon(Select):
self.updateSelectionArea()
def __init__(self, plot, parameters):
- states = {
- 'idle': SelectPolygon.Idle,
- 'select': SelectPolygon.Select
- }
- super(SelectPolygon, self).__init__(plot, parameters,
- states, 'idle')
+ states = {"idle": SelectPolygon.Idle, "select": SelectPolygon.Select}
+ super(SelectPolygon, self).__init__(plot, parameters, states, "idle")
def cancel(self):
- if isinstance(self.state, self.states['select']):
+ if isinstance(self.state, self.states["select"]):
self.resetSelectionArea()
def getDragThreshold(self):
@@ -565,10 +623,11 @@ class SelectPolygon(Select):
class Select2Points(Select):
"""Base class for drawing selection based on 2 input points."""
+
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('start', x, y)
+ self.goto("start", x, y)
return True
class Start(State):
@@ -576,11 +635,11 @@ class Select2Points(Select):
self.machine.beginSelect(x, y)
def onMove(self, x, y):
- self.goto('select', x, y)
+ self.goto("select", x, y)
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('select', x, y)
+ self.goto("select", x, y)
return True
class Select(State):
@@ -593,16 +652,15 @@ class Select2Points(Select):
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
self.machine.endSelect(x, y)
- self.goto('idle')
+ self.goto("idle")
def __init__(self, plot, parameters):
states = {
- 'idle': Select2Points.Idle,
- 'start': Select2Points.Start,
- 'select': Select2Points.Select
+ "idle": Select2Points.Idle,
+ "start": Select2Points.Start,
+ "select": Select2Points.Select,
}
- super(Select2Points, self).__init__(plot, parameters,
- states, 'idle')
+ super(Select2Points, self).__init__(plot, parameters, states, "idle")
def beginSelect(self, x, y):
pass
@@ -617,12 +675,13 @@ class Select2Points(Select):
pass
def cancel(self):
- if isinstance(self.state, self.states['select']):
+ if isinstance(self.state, self.states["select"]):
self.cancelSelect()
class SelectEllipse(Select2Points):
"""Drawing ellipse selection area state machine."""
+
def beginSelect(self, x, y):
self.center = self.plot.pixelToData(x, y)
assert self.center is not None
@@ -668,21 +727,23 @@ class SelectEllipse(Select2Points):
width, height = self._getEllipseSize(dataPos)
# Circle used for circle preview
- nbpoints = 27.
+ nbpoints = 27.0
angles = numpy.arange(nbpoints) * numpy.pi * 2.0 / nbpoints
- circleShape = numpy.array((numpy.cos(angles) * width,
- numpy.sin(angles) * height)).T
+ circleShape = numpy.array(
+ (numpy.cos(angles) * width, numpy.sin(angles) * height)
+ ).T
circleShape += numpy.array(self.center)
- self.setSelectionArea(circleShape,
- shape="polygon",
- fill='hatch',
- color=self.color)
+ self.setSelectionArea(
+ circleShape, shape="polygon", fill="hatch", color=self.color
+ )
- eventDict = prepareDrawingSignal('drawingProgress',
- 'ellipse',
- (self.center, (width, height)),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress",
+ "ellipse",
+ (self.center, (width, height)),
+ self.parameters,
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
@@ -692,10 +753,12 @@ class SelectEllipse(Select2Points):
assert dataPos is not None
width, height = self._getEllipseSize(dataPos)
- eventDict = prepareDrawingSignal('drawingFinished',
- 'ellipse',
- (self.center, (width, height)),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished",
+ "ellipse",
+ (self.center, (width, height)),
+ self.parameters,
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -704,6 +767,7 @@ class SelectEllipse(Select2Points):
class SelectRectangle(Select2Points):
"""Drawing rectangle selection area state machine."""
+
def beginSelect(self, x, y):
self.startPt = self.plot.pixelToData(x, y)
assert self.startPt is not None
@@ -712,17 +776,20 @@ class SelectRectangle(Select2Points):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- self.setSelectionArea((self.startPt,
- (self.startPt[0], dataPos[1]),
- dataPos,
- (dataPos[0], self.startPt[1])),
- fill='hatch',
- color=self.color)
-
- eventDict = prepareDrawingSignal('drawingProgress',
- 'rectangle',
- (self.startPt, dataPos),
- self.parameters)
+ self.setSelectionArea(
+ (
+ self.startPt,
+ (self.startPt[0], dataPos[1]),
+ dataPos,
+ (dataPos[0], self.startPt[1]),
+ ),
+ fill="hatch",
+ color=self.color,
+ )
+
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "rectangle", (self.startPt, dataPos), self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
@@ -731,10 +798,9 @@ class SelectRectangle(Select2Points):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareDrawingSignal('drawingFinished',
- 'rectangle',
- (self.startPt, dataPos),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "rectangle", (self.startPt, dataPos), self.parameters
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -743,6 +809,7 @@ class SelectRectangle(Select2Points):
class SelectLine(Select2Points):
"""Drawing line selection area state machine."""
+
def beginSelect(self, x, y):
self.startPt = self.plot.pixelToData(x, y)
assert self.startPt is not None
@@ -751,14 +818,11 @@ class SelectLine(Select2Points):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- self.setSelectionArea((self.startPt, dataPos),
- fill='hatch',
- color=self.color)
+ self.setSelectionArea((self.startPt, dataPos), fill="hatch", color=self.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'line',
- (self.startPt, dataPos),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "line", (self.startPt, dataPos), self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
@@ -767,10 +831,9 @@ class SelectLine(Select2Points):
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareDrawingSignal('drawingFinished',
- 'line',
- (self.startPt, dataPos),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "line", (self.startPt, dataPos), self.parameters
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -779,10 +842,11 @@ class SelectLine(Select2Points):
class Select1Point(Select):
"""Base class for drawing selection area based on one input point."""
+
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('select', x, y)
+ self.goto("select", x, y)
return True
class Select(State):
@@ -795,18 +859,15 @@ class Select1Point(Select):
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
self.machine.endSelect(x, y)
- self.goto('idle')
+ self.goto("idle")
def onWheel(self, x, y, angle):
self.machine.onWheel(x, y, angle) # Call select default wheel
self.machine.select(x, y)
def __init__(self, plot, parameters):
- states = {
- 'idle': Select1Point.Idle,
- 'select': Select1Point.Select
- }
- super(Select1Point, self).__init__(plot, parameters, states, 'idle')
+ states = {"idle": Select1Point.Idle, "select": Select1Point.Select}
+ super(Select1Point, self).__init__(plot, parameters, states, "idle")
def select(self, x, y):
pass
@@ -818,12 +879,13 @@ class Select1Point(Select):
pass
def cancel(self):
- if isinstance(self.state, self.states['select']):
+ if isinstance(self.state, self.states["select"]):
self.cancelSelect()
class SelectHLine(Select1Point):
"""Drawing a horizontal line selection area state machine."""
+
def _hLine(self, y):
"""Return points in data coords of the segment visible in the plot.
@@ -837,21 +899,19 @@ class SelectHLine(Select1Point):
def select(self, x, y):
points = self._hLine(y)
- self.setSelectionArea(points, fill='hatch', color=self.color)
+ self.setSelectionArea(points, fill="hatch", color=self.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'hline',
- points,
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "hline", points, self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
self.resetSelectionArea()
- eventDict = prepareDrawingSignal('drawingFinished',
- 'hline',
- self._hLine(y),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "hline", self._hLine(y), self.parameters
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -860,6 +920,7 @@ class SelectHLine(Select1Point):
class SelectVLine(Select1Point):
"""Drawing a vertical line selection area state machine."""
+
def _vLine(self, x):
"""Return points in data coords of the segment visible in the plot.
@@ -873,21 +934,19 @@ class SelectVLine(Select1Point):
def select(self, x, y):
points = self._vLine(x)
- self.setSelectionArea(points, fill='hatch', color=self.color)
+ self.setSelectionArea(points, fill="hatch", color=self.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'vline',
- points,
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "vline", points, self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
self.resetSelectionArea()
- eventDict = prepareDrawingSignal('drawingFinished',
- 'vline',
- self._vLine(x),
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "vline", self._vLine(x), self.parameters
+ )
self.plot.notify(**eventDict)
def cancelSelect(self):
@@ -902,7 +961,7 @@ class DrawFreeHand(Select):
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
- self.goto('select', x, y)
+ self.goto("select", x, y)
return True
def onMove(self, x, y):
@@ -925,7 +984,7 @@ class DrawFreeHand(Select):
if self.__isOut:
self.machine.resetSelectionArea()
self.machine.endSelect(x, y)
- self.goto('idle')
+ self.goto("idle")
def onEnter(self):
self.__isOut = False
@@ -935,20 +994,16 @@ class DrawFreeHand(Select):
def __init__(self, plot, parameters):
# Circle used for pencil preview
- angle = numpy.arange(13.) * numpy.pi * 2.0 / 13.
- size = parameters.get('width', 1.) * 0.5
- self._circle = size * numpy.array((numpy.cos(angle),
- numpy.sin(angle))).T
+ angle = numpy.arange(13.0) * numpy.pi * 2.0 / 13.0
+ size = parameters.get("width", 1.0) * 0.5
+ self._circle = size * numpy.array((numpy.cos(angle), numpy.sin(angle))).T
- states = {
- 'idle': DrawFreeHand.Idle,
- 'select': DrawFreeHand.Select
- }
- super(DrawFreeHand, self).__init__(plot, parameters, states, 'idle')
+ states = {"idle": DrawFreeHand.Idle, "select": DrawFreeHand.Select}
+ super(DrawFreeHand, self).__init__(plot, parameters, states, "idle")
@property
def width(self):
- return self.parameters.get('width', None)
+ return self.parameters.get("width", None)
def setFirstPoint(self, x, y):
self._points = []
@@ -960,7 +1015,7 @@ class DrawFreeHand(Select):
polygon = center + self._circle
- self.setSelectionArea(polygon, fill='none', color=self.color)
+ self.setSelectionArea(polygon, fill="none", color=self.color)
def select(self, x, y):
pos = self.plot.pixelToData(x, y, check=False)
@@ -969,10 +1024,9 @@ class DrawFreeHand(Select):
# Skip same points
return
self._points.append(pos)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'polylines',
- self._points,
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingProgress", "polylines", self._points, self.parameters
+ )
self.plot.notify(**eventDict)
def endSelect(self, x, y):
@@ -982,10 +1036,9 @@ class DrawFreeHand(Select):
# Append if different
self._points.append(pos)
- eventDict = prepareDrawingSignal('drawingFinished',
- 'polylines',
- self._points,
- self.parameters)
+ eventDict = prepareDrawingSignal(
+ "drawingFinished", "polylines", self._points, self.parameters
+ )
self.plot.notify(**eventDict)
self._points = None
@@ -1011,13 +1064,9 @@ class SelectFreeLine(ClickOrDrag, _PlotInteraction):
_PlotInteraction.__init__(self, plot)
self.parameters = parameters
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.plot, scaleF, (x, y))
-
@property
def color(self):
- return self.parameters.get('color', None)
+ return self.parameters.get("color", None)
def click(self, x, y, btn):
if btn == LEFT_BTN:
@@ -1046,21 +1095,24 @@ class SelectFreeLine(ClickOrDrag, _PlotInteraction):
if isNewPoint or isLast:
eventDict = prepareDrawingSignal(
- 'drawingFinished' if isLast else 'drawingProgress',
- 'polylines',
+ "drawingFinished" if isLast else "drawingProgress",
+ "polylines",
self._points,
- self.parameters)
+ self.parameters,
+ )
self.plot.notify(**eventDict)
if not isLast:
- self.setSelectionArea(self._points, fill='none', color=self.color,
- shape='polylines')
+ self.setSelectionArea(
+ self._points, fill="none", color=self.color, shape="polylines"
+ )
else:
self.cancel()
# ItemInteraction #############################################################
+
class ItemsInteraction(ClickOrDrag, _PlotInteraction):
"""Interaction with items (markers, curves and images).
@@ -1074,9 +1126,12 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
super(ItemsInteraction.Idle, self).__init__(*args, **kw)
self._hoverMarker = None
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.machine.plot, scaleF, (x, y))
+ def enterState(self):
+ widget = self.machine.plot.getWidgetHandle()
+ if widget is None or not widget.isVisible():
+ return
+ position = widget.mapFromGlobal(qt.QCursor.pos())
+ self.onMove(position.x(), position.y())
def onMove(self, x, y):
marker = self.machine.plot._getMarkerAt(x, y)
@@ -1085,30 +1140,18 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
dataPos = self.machine.plot.pixelToData(x, y)
assert dataPos is not None
eventDict = prepareHoverSignal(
- marker.getName(), 'marker',
- dataPos, (x, y),
+ marker.getName(),
+ "marker",
+ dataPos,
+ (x, y),
marker.isDraggable(),
- marker.isSelectable())
+ marker.isSelectable(),
+ )
self.machine.plot.notify(**eventDict)
if marker != self._hoverMarker:
self._hoverMarker = marker
-
- if marker is None:
- self.machine.plot.setGraphCursorShape()
-
- elif marker.isDraggable():
- if isinstance(marker, items.YMarker):
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_VER)
- elif isinstance(marker, items.XMarker):
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_HOR)
- else:
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_ALL)
-
- elif marker.isSelectable():
- self.machine.plot.setGraphCursorShape(CURSOR_POINTING)
- else:
- self.machine.plot.setGraphCursorShape()
+ self.machine._setCursorForMarker(marker)
return True
@@ -1116,9 +1159,30 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
self._pan = Pan(plot)
_PlotInteraction.__init__(self, plot)
- ClickOrDrag.__init__(self,
- clickButtons=(LEFT_BTN, RIGHT_BTN),
- dragButtons=(LEFT_BTN, MIDDLE_BTN))
+ ClickOrDrag.__init__(
+ self, clickButtons=(LEFT_BTN, RIGHT_BTN), dragButtons=(LEFT_BTN, MIDDLE_BTN)
+ )
+
+ def _setCursorForMarker(self, marker: Optional[items.MarkerBase] = None):
+ """Set mouse cursor for given marker"""
+ if marker is None:
+ cursor = None
+
+ elif marker.isDraggable():
+ if isinstance(marker, items.YMarker):
+ cursor = CURSOR_SIZE_VER
+ elif isinstance(marker, items.XMarker):
+ cursor = CURSOR_SIZE_HOR
+ else:
+ cursor = CURSOR_SIZE_ALL
+
+ elif marker.isSelectable():
+ cursor = CURSOR_POINTING
+
+ else:
+ cursor = None
+
+ self.plot.setGraphCursorShape(cursor)
def click(self, x, y, btn):
"""Handle mouse click
@@ -1131,9 +1195,9 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareMouseSignal(
+ "mouseClicked", btn, dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**eventDict)
eventDict = self._handleClick(x, y, btn)
@@ -1164,14 +1228,17 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
if yData is None:
yData = [0, 1]
- eventDict = prepareMarkerSignal('markerClicked',
- 'left',
- item.getName(),
- 'marker',
- item.isDraggable(),
- item.isSelectable(),
- (xData, yData),
- (x, y), None)
+ eventDict = prepareMarkerSignal(
+ "markerClicked",
+ "left",
+ item.getName(),
+ "marker",
+ item.isDraggable(),
+ item.isSelectable(),
+ (xData, yData),
+ (x, y),
+ None,
+ )
return eventDict
elif isinstance(item, items.Curve):
@@ -1182,13 +1249,16 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
yData = item.getYData(copy=False)
indices = result.getIndices(copy=False)
- eventDict = prepareCurveSignal('left',
- item.getName(),
- 'curve',
- xData[indices],
- yData[indices],
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareCurveSignal(
+ "left",
+ item,
+ xData[indices],
+ yData[indices],
+ dataPos[0],
+ dataPos[1],
+ x,
+ y,
+ )
return eventDict
elif isinstance(item, items.ImageBase):
@@ -1197,12 +1267,9 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
indices = result.getIndices(copy=False)
row, column = indices[0][0], indices[1][0]
- eventDict = prepareImageSignal('left',
- item.getName(),
- 'image',
- column, row,
- dataPos[0], dataPos[1],
- x, y)
+ eventDict = prepareImageSignal(
+ "left", item, column, row, dataPos[0], dataPos[1], x, y
+ )
return eventDict
return None
@@ -1219,24 +1286,26 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
posDataCursor = self.plot.pixelToData(x, y)
assert posDataCursor is not None
- eventDict = prepareMarkerSignal(eventType,
- 'left',
- marker.getName(),
- 'marker',
- marker.isDraggable(),
- marker.isSelectable(),
- (xData, yData),
- (x, y),
- posDataCursor)
+ eventDict = prepareMarkerSignal(
+ eventType,
+ "left",
+ marker.getName(),
+ "marker",
+ marker.isDraggable(),
+ marker.isSelectable(),
+ (xData, yData),
+ (x, y),
+ posDataCursor,
+ )
self.plot.notify(**eventDict)
@staticmethod
def __isDraggableItem(item):
return isinstance(item, items.DraggableMixIn) and item.isDraggable()
- def __terminateDrag(self):
+ def __terminateDrag(self, x, y):
"""Finalize a drag operation by reseting to initial state"""
- self.plot.setGraphCursorShape()
+ self._setCursorForMarker(self.plot._getMarkerAt(x, y))
self.draggedItemRef = None
def beginDrag(self, x, y, btn):
@@ -1257,11 +1326,11 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
self.draggedItemRef = None if item is None else weakref.ref(item)
if item is None:
- self.__terminateDrag()
+ self.__terminateDrag(x, y)
return False
if isinstance(item, items.MarkerBase):
- self._signalMarkerMovingEvent('markerMoving', item, x, y)
+ self._signalMarkerMovingEvent("markerMoving", item, x, y)
item._startDrag()
return True
@@ -1279,7 +1348,7 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
item.drag(self._lastPos, dataPos)
if isinstance(item, items.MarkerBase):
- self._signalMarkerMovingEvent('markerMoving', item, x, y)
+ self._signalMarkerMovingEvent("markerMoving", item, x, y)
self._lastPos = dataPos
elif btn == MIDDLE_BTN:
@@ -1291,46 +1360,52 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
if isinstance(item, items.MarkerBase):
posData = list(item.getPosition())
if posData[0] is None:
- posData[0] = 1.
+ posData[0] = 1.0
if posData[1] is None:
- posData[1] = 1.
+ posData[1] = 1.0
eventDict = prepareMarkerSignal(
- 'markerMoved',
- 'left',
+ "markerMoved",
+ "left",
item.getLegend(),
- 'marker',
+ "marker",
item.isDraggable(),
item.isSelectable(),
- posData)
+ posData,
+ )
self.plot.notify(**eventDict)
item._endDrag()
- self.__terminateDrag()
+ self.__terminateDrag(*endPos)
elif btn == MIDDLE_BTN:
self._pan.endDrag(startPos, endPos, btn)
def cancel(self):
self._pan.cancel()
- self.__terminateDrag()
+ widget = self.plot.getWidgetHandle()
+ if widget is None or not widget.isVisible():
+ return
+ position = widget.mapFromGlobal(qt.QCursor.pos())
+ self.__terminateDrag(position.x(), position.y())
class ItemsInteractionForCombo(ItemsInteraction):
- """Interaction with items to combine through :class:`FocusManager`.
- """
+ """Interaction with items to combine through :class:`FocusManager`."""
class Idle(ItemsInteraction.Idle):
@staticmethod
def __isItemSelectableOrDraggable(item):
- return (item.isSelectable() or (
- isinstance(item, items.DraggableMixIn) and item.isDraggable()))
+ return item.isSelectable() or (
+ isinstance(item, items.DraggableMixIn) and item.isDraggable()
+ )
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
result = self.machine.plot._pickTopMost(
- x, y, self.__isItemSelectableOrDraggable)
+ x, y, self.__isItemSelectableOrDraggable
+ )
if result is not None: # Request focus and handle interaction
- self.goto('clickOrDrag', x, y, btn)
+ self.goto("clickOrDrag", x, y, btn)
return True
else: # Do not request focus
return False
@@ -1340,19 +1415,21 @@ class ItemsInteractionForCombo(ItemsInteraction):
# FocusManager ################################################################
+
class FocusManager(StateMachine):
"""Manages focus across multiple event handlers
On press an event handler can acquire focus.
By default it looses focus when all buttons are released.
"""
+
class Idle(State):
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
for eventHandler in self.machine.eventHandlers:
- requestFocus = eventHandler.handleEvent('press', x, y, btn)
+ requestFocus = eventHandler.handleEvent("press", x, y, btn)
if requestFocus:
- self.goto('focus', eventHandler, btn)
+ self.goto("focus", eventHandler, btn)
break
def _processEvent(self, *args):
@@ -1362,14 +1439,14 @@ class FocusManager(StateMachine):
break
def onMove(self, x, y):
- self._processEvent('move', x, y)
+ self._processEvent("move", x, y)
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
- self._processEvent('release', x, y, btn)
+ self._processEvent("release", x, y, btn)
def onWheel(self, x, y, angle):
- self._processEvent('wheel', x, y, angle)
+ self._processEvent("wheel", x, y, angle)
class Focus(State):
def enterState(self, eventHandler, btn):
@@ -1378,34 +1455,31 @@ class FocusManager(StateMachine):
def validate(self):
self.eventHandler.validate()
- self.goto('idle')
+ self.goto("idle")
def onPress(self, x, y, btn):
if btn == LEFT_BTN:
self.focusBtns.add(btn)
- self.eventHandler.handleEvent('press', x, y, btn)
+ self.eventHandler.handleEvent("press", x, y, btn)
def onMove(self, x, y):
- self.eventHandler.handleEvent('move', x, y)
+ self.eventHandler.handleEvent("move", x, y)
def onRelease(self, x, y, btn):
if btn == LEFT_BTN:
self.focusBtns.discard(btn)
- requestFocus = self.eventHandler.handleEvent('release', x, y, btn)
+ requestFocus = self.eventHandler.handleEvent("release", x, y, btn)
if len(self.focusBtns) == 0 and not requestFocus:
- self.goto('idle')
+ self.goto("idle")
def onWheel(self, x, y, angleInDegrees):
- self.eventHandler.handleEvent('wheel', x, y, angleInDegrees)
+ self.eventHandler.handleEvent("wheel", x, y, angleInDegrees)
def __init__(self, eventHandlers=()):
self.eventHandlers = list(eventHandlers)
- states = {
- 'idle': FocusManager.Idle,
- 'focus': FocusManager.Focus
- }
- super(FocusManager, self).__init__(states, 'idle')
+ states = {"idle": FocusManager.Idle, "focus": FocusManager.Focus}
+ super(FocusManager, self).__init__(states, "idle")
def cancel(self):
for handler in self.eventHandlers:
@@ -1429,6 +1503,15 @@ class ZoomAndSelect(ItemsInteraction):
"""Color of the zoom area"""
return self._zoom.color
+ @property
+ def zoomEnabledAxes(self) -> EnabledAxes:
+ """Whether or not to apply zoom for each axis"""
+ return self._zoom.enabledAxes
+
+ @zoomEnabledAxes.setter
+ def zoomEnabledAxes(self, enabledAxes: EnabledAxes):
+ self._zoom.enabledAxes = enabledAxes
+
def click(self, x, y, btn):
"""Handle mouse click
@@ -1443,9 +1526,9 @@ class ZoomAndSelect(ItemsInteraction):
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- clickedEventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
+ clickedEventDict = prepareMouseSignal(
+ "mouseClicked", btn, dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**clickedEventDict)
self.plot.notify(**eventDict)
@@ -1514,9 +1597,9 @@ class PanAndSelect(ItemsInteraction):
# Signal mouse clicked event
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
- clickedEventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
+ clickedEventDict = prepareMouseSignal(
+ "mouseClicked", btn, dataPos[0], dataPos[1], x, y
+ )
self.plot.notify(**clickedEventDict)
self.plot.notify(**eventDict)
@@ -1564,15 +1647,15 @@ class PanAndSelect(ItemsInteraction):
# Mapping of draw modes: event handler
_DRAW_MODES = {
- 'polygon': SelectPolygon,
- 'rectangle': SelectRectangle,
- 'ellipse': SelectEllipse,
- 'line': SelectLine,
- 'vline': SelectVLine,
- 'hline': SelectHLine,
- 'polylines': SelectFreeLine,
- 'pencil': DrawFreeHand,
- }
+ "polygon": SelectPolygon,
+ "rectangle": SelectRectangle,
+ "ellipse": SelectEllipse,
+ "line": SelectLine,
+ "vline": SelectVLine,
+ "hline": SelectHLine,
+ "polylines": SelectFreeLine,
+ "pencil": DrawFreeHand,
+}
class DrawMode(FocusManager):
@@ -1581,19 +1664,22 @@ class DrawMode(FocusManager):
def __init__(self, plot, shape, label, color, width):
eventHandlerClass = _DRAW_MODES[shape]
parameters = {
- 'shape': shape,
- 'label': label,
- 'color': color,
- 'width': width,
- }
- super().__init__((
- Pan(plot, clickButtons=(), dragButtons=(MIDDLE_BTN,)),
- eventHandlerClass(plot, parameters)))
+ "shape": shape,
+ "label": label,
+ "color": color,
+ "width": width,
+ }
+ super().__init__(
+ (
+ Pan(plot, clickButtons=(), dragButtons=(MIDDLE_BTN,)),
+ eventHandlerClass(plot, parameters),
+ )
+ )
def getDescription(self):
"""Returns the dict describing this interactive mode"""
params = self.eventHandlers[1].parameters.copy()
- params['mode'] = 'draw'
+ params["mode"] = "draw"
return params
@@ -1605,27 +1691,27 @@ class DrawSelectMode(FocusManager):
self._pan = Pan(plot)
self._panStart = None
parameters = {
- 'shape': shape,
- 'label': label,
- 'color': color,
- 'width': width,
- }
- super().__init__((
- ItemsInteractionForCombo(plot),
- eventHandlerClass(plot, parameters)))
+ "shape": shape,
+ "label": label,
+ "color": color,
+ "width": width,
+ }
+ super().__init__(
+ (ItemsInteractionForCombo(plot), eventHandlerClass(plot, parameters))
+ )
def handleEvent(self, eventName, *args, **kwargs):
# Hack to add pan interaction to select-draw
# See issue Refactor PlotWidget interaction #3292
- if eventName == 'press' and args[2] == MIDDLE_BTN:
+ if eventName == "press" and args[2] == MIDDLE_BTN:
self._panStart = args[:2]
self._pan.beginDrag(*args)
return # Consume middle click events
- elif eventName == 'release' and args[2] == MIDDLE_BTN:
+ elif eventName == "release" and args[2] == MIDDLE_BTN:
self._panStart = None
self._pan.endDrag(self._panStart, args[:2], MIDDLE_BTN)
return # Consume middle click events
- elif self._panStart is not None and eventName == 'move':
+ elif self._panStart is not None and eventName == "move":
x, y = args[:2]
self._pan.drag(x, y, MIDDLE_BTN)
@@ -1634,67 +1720,94 @@ class DrawSelectMode(FocusManager):
def getDescription(self):
"""Returns the dict describing this interactive mode"""
params = self.eventHandlers[1].parameters.copy()
- params['mode'] = 'select-draw'
+ params["mode"] = "select-draw"
return params
-class PlotInteraction(object):
- """Proxy to currently use state machine for interaction.
-
- This allows to switch interactive mode.
+class PlotInteraction(qt.QObject):
+ """PlotWidget user interaction handler.
- :param plot: The :class:`Plot` to apply interaction to
+ :param plot: The :class:`PlotWidget` to apply interaction to
"""
+ sigChanged = qt.Signal()
+ """Signal emitted when the interaction configuration has changed"""
+
_DRAW_MODES = {
- 'polygon': SelectPolygon,
- 'rectangle': SelectRectangle,
- 'ellipse': SelectEllipse,
- 'line': SelectLine,
- 'vline': SelectVLine,
- 'hline': SelectHLine,
- 'polylines': SelectFreeLine,
- 'pencil': DrawFreeHand,
+ "polygon": SelectPolygon,
+ "rectangle": SelectRectangle,
+ "ellipse": SelectEllipse,
+ "line": SelectLine,
+ "vline": SelectVLine,
+ "hline": SelectHLine,
+ "polylines": SelectFreeLine,
+ "pencil": DrawFreeHand,
}
- def __init__(self, plot):
- self._plot = weakref.ref(plot) # Avoid cyclic-ref
-
- self.zoomOnWheel = True
- """True to enable zoom on wheel, False otherwise."""
+ def __init__(self, parent):
+ super().__init__(parent)
+ self.__zoomOnWheel = True
+ self.__zoomEnabledAxes = EnabledAxes()
# Default event handler
- self._eventHandler = ItemsInteraction(plot)
+ self._eventHandler = ItemsInteraction(parent)
+
+ def isZoomOnWheelEnabled(self) -> bool:
+ """Returns whether or not wheel interaction triggers zoom"""
+ return self.__zoomOnWheel
+
+ def setZoomOnWheelEnabled(self, enabled: bool):
+ """Toggle zoom on wheel interaction"""
+ if enabled != self.__zoomOnWheel:
+ self.__zoomOnWheel = enabled
+ self.sigChanged.emit()
- def getInteractiveMode(self):
+ def setZoomEnabledAxes(self, xaxis: bool, yaxis: bool, y2axis: bool):
+ """Toggle zoom interaction for each axis
+
+ This is taken into account only if the plot does not keep aspect ratio.
+ """
+ zoomEnabledAxes = EnabledAxes(xaxis, yaxis, y2axis)
+ if zoomEnabledAxes != self.__zoomEnabledAxes:
+ self.__zoomEnabledAxes = zoomEnabledAxes
+ if isinstance(self._eventHandler, ZoomAndSelect):
+ self._eventHandler.zoomEnabledAxes = zoomEnabledAxes
+ self.sigChanged.emit()
+
+ def getZoomEnabledAxes(self) -> EnabledAxes:
+ """Returns axes for which zoom is enabled"""
+ return self.__zoomEnabledAxes
+
+ 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', 'select-draw', 'zoom'.
It can also contains extra keys (e.g., 'color') specific to a mode
- as provided to :meth:`setInteractiveMode`.
+ as provided to :meth:`_setInteractiveMode`.
"""
if isinstance(self._eventHandler, ZoomAndSelect):
- return {'mode': 'zoom', 'color': self._eventHandler.color}
+ return {"mode": "zoom", "color": self._eventHandler.color}
elif isinstance(self._eventHandler, (DrawMode, DrawSelectMode)):
return self._eventHandler.getDescription()
elif isinstance(self._eventHandler, PanAndSelect):
- return {'mode': 'pan'}
+ return {"mode": "pan"}
else:
- return {'mode': 'select'}
+ return {"mode": "select"}
- def validate(self):
+ def _validate(self):
"""Validate the current interaction if possible
If was designed to close the polygon interaction.
"""
self._eventHandler.validate()
- def setInteractiveMode(self, mode, color='black',
- shape='polygon', label=None, width=None):
+ def _setInteractiveMode(
+ self, mode, color="black", shape="polygon", label=None, width=None
+ ):
"""Switch the interactive mode.
:param str mode: The name of the interactive mode.
@@ -1711,36 +1824,62 @@ class PlotInteraction(object):
:param str label: Only for 'draw' mode.
:param float width: Width of the pencil. Only for draw pencil mode.
"""
- assert mode in ('draw', 'pan', 'select', 'select-draw', 'zoom')
+ assert mode in ("draw", "pan", "select", "select-draw", "zoom")
- plot = self._plot()
- assert plot is not None
+ plotWidget = self.parent()
+ assert plotWidget is not None
- if isinstance(color, numpy.ndarray) or color not in (None, 'video inverted'):
+ if isinstance(color, numpy.ndarray) or color not in (None, "video inverted"):
color = colors.rgba(color)
- if mode in ('draw', 'select-draw'):
+ if mode in ("draw", "select-draw"):
self._eventHandler.cancel()
- handlerClass = DrawMode if mode == 'draw' else DrawSelectMode
- self._eventHandler = handlerClass(plot, shape, label, color, width)
+ handlerClass = DrawMode if mode == "draw" else DrawSelectMode
+ self._eventHandler = handlerClass(plotWidget, shape, label, color, width)
- elif mode == 'pan':
+ elif mode == "pan":
# Ignores color, shape and label
self._eventHandler.cancel()
- self._eventHandler = PanAndSelect(plot)
+ self._eventHandler = PanAndSelect(plotWidget)
- elif mode == 'zoom':
+ elif mode == "zoom":
# Ignores shape and label
self._eventHandler.cancel()
- self._eventHandler = ZoomAndSelect(plot, color)
+ self._eventHandler = ZoomAndSelect(plotWidget, color)
+ self._eventHandler.zoomEnabledAxes = self.getZoomEnabledAxes()
else: # Default mode: interaction with plot objects
# Ignores color, shape and label
self._eventHandler.cancel()
- self._eventHandler = ItemsInteraction(plot)
+ self._eventHandler = ItemsInteraction(plotWidget)
+
+ self.sigChanged.emit()
def handleEvent(self, event, *args, **kwargs):
"""Forward event to current interactive mode state machine."""
- if not self.zoomOnWheel and event == 'wheel':
- return # Discard wheel events
+ if event == "wheel": # Handle wheel events directly
+ self._onWheel(*args, **kwargs)
+ return
+
self._eventHandler.handleEvent(event, *args, **kwargs)
+
+ def _onWheel(self, x: float, y: float, angle: float):
+ """Handle wheel events"""
+ if not self.isZoomOnWheelEnabled():
+ return
+
+ plotWidget = self.parent()
+ if plotWidget is None:
+ return
+
+ # All axes are enabled if keep aspect ratio is on
+ enabledAxes = (
+ EnabledAxes()
+ if plotWidget.isKeepDataAspectRatio()
+ else self.getZoomEnabledAxes()
+ )
+ if enabledAxes.isDisabled():
+ return
+
+ scale = 1.1 if angle > 0 else 1.0 / 1.1
+ applyZoomToPlot(plotWidget, scale, (x, y), enabledAxes)
diff --git a/src/silx/gui/plot/PlotToolButtons.py b/src/silx/gui/plot/PlotToolButtons.py
index 3970896..e132877 100644
--- a/src/silx/gui/plot/PlotToolButtons.py
+++ b/src/silx/gui/plot/PlotToolButtons.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
@@ -30,6 +29,7 @@ The following QToolButton are available:
- :class:`.AspectToolButton`
- :class:`.YAxisOriginToolButton`
- :class:`.ProfileToolButton`
+- :class:`.RulerToolButton`
- :class:`.SymbolToolButton`
"""
@@ -41,11 +41,11 @@ __date__ = "27/06/2017"
import functools
import logging
-import weakref
from .. import icons
from .. import qt
from ... import config
+from .tools.PlotToolButton import PlotToolButton
from .items import SymbolMixIn, Scatter
@@ -53,58 +53,6 @@ from .items import SymbolMixIn, Scatter
_logger = logging.getLogger(__name__)
-class PlotToolButton(qt.QToolButton):
- """A QToolButton connected to a :class:`~silx.gui.plot.PlotWidget`.
- """
-
- def __init__(self, parent=None, plot=None):
- super(PlotToolButton, self).__init__(parent)
- self._plotRef = None
- if plot is not None:
- self.setPlot(plot)
-
- def plot(self):
- """
- Returns the plot connected to the widget.
- """
- return None if self._plotRef is None else self._plotRef()
-
- def setPlot(self, plot):
- """
- Set the plot connected to the widget
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- """
- previousPlot = self.plot()
-
- if previousPlot is plot:
- return
- if previousPlot is not None:
- self._disconnectPlot(previousPlot)
-
- if plot is None:
- self._plotRef = None
- else:
- self._plotRef = weakref.ref(plot)
- self._connectPlot(plot)
-
- def _connectPlot(self, plot):
- """
- Called when the plot is connected to the widget
-
- :param plot: :class:`.PlotWidget` instance
- """
- pass
-
- def _disconnectPlot(self, plot):
- """
- Called when the plot is disconnected from the widget
-
- :param plot: :class:`.PlotWidget` instance
- """
- pass
-
-
class AspectToolButton(PlotToolButton):
"""Tool button to switch keep aspect ratio of a plot"""
@@ -115,11 +63,11 @@ class AspectToolButton(PlotToolButton):
if self.STATE is None:
self.STATE = {}
# dont keep ratio
- self.STATE[False, "icon"] = icons.getQIcon('shape-ellipse-solid')
+ self.STATE[False, "icon"] = icons.getQIcon("shape-ellipse-solid")
self.STATE[False, "state"] = "Aspect ratio is not kept"
self.STATE[False, "action"] = "Do no keep data aspect ratio"
# keep ratio
- self.STATE[True, "icon"] = icons.getQIcon('shape-circle-solid')
+ self.STATE[True, "icon"] = icons.getQIcon("shape-circle-solid")
self.STATE[True, "state"] = "Aspect ratio is kept"
self.STATE[True, "action"] = "Keep data aspect ratio"
@@ -167,7 +115,10 @@ class AspectToolButton(PlotToolButton):
def _keepDataAspectRatioChanged(self, aspectRatio):
"""Handle Plot set keep aspect ratio signal"""
- icon, toolTip = self.STATE[aspectRatio, "icon"], self.STATE[aspectRatio, "state"]
+ icon, toolTip = (
+ self.STATE[aspectRatio, "icon"],
+ self.STATE[aspectRatio, "state"],
+ )
self.setIcon(icon)
self.setToolTip(toolTip)
@@ -182,11 +133,11 @@ class YAxisOriginToolButton(PlotToolButton):
if self.STATE is None:
self.STATE = {}
# is down
- self.STATE[False, "icon"] = icons.getQIcon('plot-ydown')
+ self.STATE[False, "icon"] = icons.getQIcon("plot-ydown")
self.STATE[False, "state"] = "Y-axis is oriented downward"
self.STATE[False, "action"] = "Orient Y-axis downward"
# keep ration
- self.STATE[True, "icon"] = icons.getQIcon('plot-yup')
+ self.STATE[True, "icon"] = icons.getQIcon("plot-yup")
self.STATE[True, "state"] = "Y-axis is oriented upward"
self.STATE[True, "action"] = "Orient Y-axis upward"
@@ -243,28 +194,29 @@ class YAxisOriginToolButton(PlotToolButton):
class ProfileOptionToolButton(PlotToolButton):
"""Button to define option on the profile"""
+
sigMethodChanged = qt.Signal(str)
-
+
def __init__(self, parent=None, plot=None):
PlotToolButton.__init__(self, parent=parent, plot=plot)
self.STATE = {}
# is down
- self.STATE['sum', "icon"] = icons.getQIcon('math-sigma')
- self.STATE['sum', "state"] = "Compute profile sum"
- self.STATE['sum', "action"] = "Compute profile sum"
+ self.STATE["sum", "icon"] = icons.getQIcon("math-sigma")
+ self.STATE["sum", "state"] = "Compute profile sum"
+ self.STATE["sum", "action"] = "Compute profile sum"
# keep ration
- self.STATE['mean', "icon"] = icons.getQIcon('math-mean')
- self.STATE['mean', "state"] = "Compute profile mean"
- self.STATE['mean', "action"] = "Compute profile mean"
+ self.STATE["mean", "icon"] = icons.getQIcon("math-mean")
+ self.STATE["mean", "state"] = "Compute profile mean"
+ self.STATE["mean", "action"] = "Compute profile mean"
- self.sumAction = self._createAction('sum')
+ self.sumAction = self._createAction("sum")
self.sumAction.triggered.connect(self.setSum)
self.sumAction.setIconVisibleInMenu(True)
self.sumAction.setCheckable(True)
self.sumAction.setChecked(True)
- self.meanAction = self._createAction('mean')
+ self.meanAction = self._createAction("mean")
self.meanAction.triggered.connect(self.setMean)
self.meanAction.setIconVisibleInMenu(True)
self.meanAction.setCheckable(True)
@@ -274,7 +226,7 @@ class ProfileOptionToolButton(PlotToolButton):
menu.addAction(self.meanAction)
self.setMenu(menu)
self.setPopupMode(qt.QToolButton.InstantPopup)
- self._method = 'mean'
+ self._method = "mean"
self._update()
def _createAction(self, method):
@@ -283,7 +235,7 @@ class ProfileOptionToolButton(PlotToolButton):
return qt.QAction(icon, text, self)
def setSum(self):
- self.setMethod('sum')
+ self.setMethod("sum")
def _update(self):
icon = self.STATE[self._method, "icon"]
@@ -294,7 +246,7 @@ class ProfileOptionToolButton(PlotToolButton):
self.meanAction.setChecked(self._method == "mean")
def setMean(self):
- self.setMethod('mean')
+ self.setMethod("mean")
def setMethod(self, method):
"""Set the method to use.
@@ -302,13 +254,12 @@ class ProfileOptionToolButton(PlotToolButton):
:param str method: Either 'sum' or 'mean'
"""
if method != self._method:
- if method in ('sum', 'mean'):
+ if method in ("sum", "mean"):
self._method = method
self.sigMethodChanged.emit(self._method)
self._update()
else:
- _logger.warning(
- "Unsupported method '%s'. Setting ignored.", method)
+ _logger.warning("Unsupported method '%s'. Setting ignored.", method)
def getMethod(self):
"""Returns the current method in use (See :meth:`setMethod`).
@@ -321,6 +272,7 @@ class ProfileOptionToolButton(PlotToolButton):
class ProfileToolButton(PlotToolButton):
"""Button used in Profile3DToolbar to switch between 2D profile
and 1D profile."""
+
STATE = None
"""Lazy loaded states used to feed ProfileToolButton"""
@@ -329,12 +281,16 @@ class ProfileToolButton(PlotToolButton):
def __init__(self, parent=None, plot=None):
if self.STATE is None:
self.STATE = {
- (1, "icon"): icons.getQIcon('profile1D'),
+ (1, "icon"): icons.getQIcon("profile1D"),
(1, "state"): "1D profile is computed on visible image",
(1, "action"): "1D profile on visible image",
- (2, "icon"): icons.getQIcon('profile2D'),
- (2, "state"): "2D profile is computed, one 1D profile for each image in the stack",
- (2, "action"): "2D profile on image stack"}
+ (2, "icon"): icons.getQIcon("profile2D"),
+ (
+ 2,
+ "state",
+ ): "2D profile is computed, one 1D profile for each image in the stack",
+ (2, "action"): "2D profile on image stack",
+ }
# Compute 1D profile
# Compute 2D profile
@@ -360,7 +316,7 @@ class ProfileToolButton(PlotToolButton):
menu.addAction(profile2DAction)
self.setMenu(menu)
self.setPopupMode(qt.QToolButton.InstantPopup)
- menu.setTitle('Select profile dimension')
+ menu.setTitle("Select profile dimension")
self.computeProfileIn1D()
def _createAction(self, profileDimension):
@@ -432,12 +388,12 @@ class _SymbolToolButtonBase(PlotToolButton):
:param QMenu menu:
"""
- for marker, name in zip(SymbolMixIn.getSupportedSymbols(),
- SymbolMixIn.getSupportedSymbolNames()):
+ for marker, name in zip(
+ SymbolMixIn.getSupportedSymbols(), SymbolMixIn.getSupportedSymbolNames()
+ ):
action = qt.QAction(name, menu)
action.setCheckable(False)
- action.triggered.connect(
- functools.partial(self._markerChanged, marker))
+ action.triggered.connect(functools.partial(self._markerChanged, marker))
menu.addAction(action)
def _sizeChanged(self, value):
@@ -477,8 +433,8 @@ class SymbolToolButton(_SymbolToolButtonBase):
def __init__(self, parent=None, plot=None):
super(SymbolToolButton, self).__init__(parent=parent, plot=plot)
- self.setToolTip('Set symbol size and marker')
- self.setIcon(icons.getQIcon('plot-symbols'))
+ self.setToolTip("Set symbol size and marker")
+ self.setIcon(icons.getQIcon("plot-symbols"))
menu = qt.QMenu(self)
self._addSizeSliderToMenu(menu)
@@ -497,12 +453,10 @@ class ScatterVisualizationToolButton(_SymbolToolButtonBase):
"""
def __init__(self, parent=None, plot=None):
- super(ScatterVisualizationToolButton, self).__init__(
- parent=parent, plot=plot)
+ super(ScatterVisualizationToolButton, self).__init__(parent=parent, plot=plot)
- self.setToolTip(
- 'Set scatter visualization mode, symbol marker and size')
- self.setIcon(icons.getQIcon('eye'))
+ self.setToolTip("Set scatter visualization mode, symbol marker and size")
+ self.setIcon(icons.getQIcon("eye"))
menu = qt.QMenu(self)
@@ -514,26 +468,33 @@ class ScatterVisualizationToolButton(_SymbolToolButtonBase):
action = qt.QAction(name, menu)
action.setCheckable(False)
action.triggered.connect(
- functools.partial(self._visualizationChanged, mode, None))
+ functools.partial(self._visualizationChanged, mode, None)
+ )
menu.addAction(action)
if Scatter.Visualization.BINNED_STATISTIC in Scatter.supportedVisualizations():
reductions = Scatter.supportedVisualizationParameterValues(
- Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION)
+ Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ )
if reductions:
- submenu = menu.addMenu('Binned Statistic')
+ submenu = menu.addMenu("Binned Statistic")
for reduction in reductions:
name = reduction.capitalize()
action = qt.QAction(name, menu)
action.setCheckable(False)
- action.triggered.connect(functools.partial(
- self._visualizationChanged,
- Scatter.Visualization.BINNED_STATISTIC,
- {Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION: reduction}))
+ action.triggered.connect(
+ functools.partial(
+ self._visualizationChanged,
+ Scatter.Visualization.BINNED_STATISTIC,
+ {
+ Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION: reduction
+ },
+ )
+ )
submenu.addAction(action)
submenu.addSeparator()
- binsmenu = submenu.addMenu('N Bins')
+ binsmenu = submenu.addMenu("N Bins")
slider = qt.QSlider(qt.Qt.Horizontal)
slider.setRange(10, 1000)
@@ -546,10 +507,10 @@ class ScatterVisualizationToolButton(_SymbolToolButtonBase):
menu.addSeparator()
- submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol")
+ submenu = menu.addMenu(icons.getQIcon("plot-symbols"), "Symbol")
self._addSymbolsToMenu(submenu)
- submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol Size")
+ submenu = menu.addMenu(icons.getQIcon("plot-symbols"), "Symbol Size")
self._addSizeSliderToMenu(submenu)
self.setMenu(menu)
@@ -588,5 +549,6 @@ class ScatterVisualizationToolButton(_SymbolToolButtonBase):
if isinstance(item, Scatter):
item.setVisualizationParameter(
Scatter.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- (value, value))
+ (value, value),
+ )
item.setVisualization(Scatter.Visualization.BINNED_STATISTIC)
diff --git a/src/silx/gui/plot/PlotTools.py b/src/silx/gui/plot/PlotTools.py
deleted file mode 100644
index 5929473..0000000
--- a/src/silx/gui/plot/PlotTools.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Set of widgets to associate with a :class:'PlotWidget'.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/03/2018"
-
-
-from ...utils.deprecation import deprecated_warning
-
-deprecated_warning(type_='module',
- name=__file__,
- reason='Plot tools refactoring',
- replacement='silx.gui.plot.tools',
- since_version='0.8')
-
-from .tools import PositionInfo, LimitsToolBar # noqa
diff --git a/src/silx/gui/plot/PlotWidget.py b/src/silx/gui/plot/PlotWidget.py
index 6cb5ef5..a01ca48 100755
--- a/src/silx/gui/plot/PlotWidget.py
+++ b/src/silx/gui/plot/PlotWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,8 +25,7 @@
The :class:`PlotWidget` implements the plot API initially provided in PyMca.
"""
-from __future__ import division
-
+from __future__ import annotations
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
@@ -38,19 +36,20 @@ import logging
_logger = logging.getLogger(__name__)
-from collections import OrderedDict, namedtuple
+from collections import namedtuple
+from collections.abc import Sequence
from contextlib import contextmanager
+from typing import Optional, Union
import datetime as dt
import itertools
-import typing
+import numbers
import warnings
import numpy
import silx
from silx.utils.weakref import WeakMethodProxy
-from silx.utils.property import classproperty
-from silx.utils.deprecation import deprecated, deprecated_warning
+
try:
# Import matplotlib now to init matplotlib our way
import silx.gui.utils.matplotlib # noqa
@@ -71,17 +70,13 @@ from .items.axis import TickMode # noqa
from .. import qt
from ._utils.panzoom import ViewConstraints
from ...gui.plot._utils.dtime_ticklayout import timestamp
+from ...utils.deprecation import deprecated_warning
-
-_COLORDICT = colors.COLORDICT
-_COLORLIST = silx.config.DEFAULT_PLOT_CURVE_COLORS
-
"""
Object returned when requesting the data range.
"""
-_PlotDataRange = namedtuple('PlotDataRange',
- ['x', 'y', 'yright'])
+_PlotDataRange = namedtuple("PlotDataRange", ["x", "y", "yright"])
class _PlotWidgetSelection(qt.QObject):
@@ -107,10 +102,14 @@ class _PlotWidgetSelection(qt.QObject):
# Init history
self.__history = [ # Store active items from most recent to oldest
- item for item in (parent.getActiveCurve(),
- parent.getActiveImage(),
- parent.getActiveScatter())
- if item is not None]
+ item
+ for item in (
+ parent.getActiveCurve(),
+ parent.getActiveImage(),
+ parent.getActiveScatter(),
+ )
+ if item is not None
+ ]
self.__current = self.__mostRecentActiveItem()
@@ -118,11 +117,11 @@ class _PlotWidgetSelection(qt.QObject):
parent.sigActiveCurveChanged.connect(self._activeCurveChanged)
parent.sigActiveScatterChanged.connect(self._activeScatterChanged)
- def __mostRecentActiveItem(self) -> typing.Optional[items.Item]:
+ def __mostRecentActiveItem(self) -> Optional[items.Item]:
"""Returns most recent active item."""
return self.__history[0] if len(self.__history) >= 1 else None
- def getSelectedItems(self) -> typing.Tuple[items.Item]:
+ def getSelectedItems(self) -> tuple[items.Item]:
"""Returns the list of currently selected items in the :class:`PlotWidget`.
The list is given from most recently current item to oldest one."""
@@ -139,11 +138,11 @@ class _PlotWidgetSelection(qt.QObject):
return active
- def getCurrentItem(self) -> typing.Optional[items.Item]:
- """Returns the current item in the :class:`PlotWidget` or None. """
+ def getCurrentItem(self) -> Optional[items.Item]:
+ """Returns the current item in the :class:`PlotWidget` or None."""
return self.__current
- def setCurrentItem(self, item: typing.Optional[items.Item]):
+ def setCurrentItem(self, item: Optional[items.Item]):
"""Set the current item in the :class:`PlotWidget`.
:param item:
@@ -169,20 +168,21 @@ class _PlotWidgetSelection(qt.QObject):
elif isinstance(item, items.Item):
plot = self.parent()
if plot is None or item.getPlot() is not plot:
- raise ValueError(
- "Item is not in the PlotWidget: %s" % str(item))
+ raise ValueError("Item is not in the PlotWidget: %s" % str(item))
self.__current = item
kind = plot._itemKind(item)
# Clean-up history to be safe
- self.__history = [item for item in self.__history
- if PlotWidget._itemKind(item) != kind]
+ self.__history = [
+ item for item in self.__history if PlotWidget._itemKind(item) != kind
+ ]
# Sync active item if needed
- if (kind in plot._ACTIVE_ITEM_KINDS and
- item is not plot._getActiveItem(kind)):
- plot._setActiveItem(kind, item.getName())
+ if kind in plot._ACTIVE_ITEM_KINDS and item is not plot._getActiveItem(
+ kind
+ ):
+ plot._setActiveItem(kind, item)
else:
raise ValueError("Not an Item: %s" % str(item))
@@ -191,10 +191,9 @@ class _PlotWidgetSelection(qt.QObject):
if previousSelected != self.getSelectedItems():
self.sigSelectedItemsChanged.emit()
- def __activeItemChanged(self,
- kind: str,
- previous: typing.Optional[str],
- legend: typing.Optional[str]):
+ def __activeItemChanged(
+ self, kind: str, previous: Optional[str], legend: Optional[str]
+ ):
"""Set current item from kind and legend"""
if previous == legend:
return # No-op for update of item
@@ -206,8 +205,9 @@ class _PlotWidgetSelection(qt.QObject):
previousSelected = self.getSelectedItems()
# Remove items of this kind from the history
- self.__history = [item for item in self.__history
- if PlotWidget._itemKind(item) != kind]
+ self.__history = [
+ item for item in self.__history if PlotWidget._itemKind(item) != kind
+ ]
# Retrieve current item
if legend is None: # Use most recent active item
@@ -233,15 +233,15 @@ class _PlotWidgetSelection(qt.QObject):
def _activeImageChanged(self, previous, current):
"""Handle active image change"""
- self.__activeItemChanged('image', previous, current)
+ self.__activeItemChanged("image", previous, current)
def _activeCurveChanged(self, previous, current):
"""Handle active curve change"""
- self.__activeItemChanged('curve', previous, current)
+ self.__activeItemChanged("curve", previous, current)
def _activeScatterChanged(self, previous, current):
"""Handle active scatter change"""
- self.__activeItemChanged('scatter', previous, current)
+ self.__activeItemChanged("scatter", previous, current)
class PlotWidget(qt.QMainWindow):
@@ -263,15 +263,10 @@ class PlotWidget(qt.QMainWindow):
:type backend: str or :class:`BackendBase.BackendBase`
"""
- # TODO: Can be removed for silx 0.10
- @classproperty
- @deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
- def DEFAULT_BACKEND(self):
- """Class attribute setting the default backend for all instances."""
- return silx.config.DEFAULT_PLOT_BACKEND
-
- colorList = _COLORLIST
- colorDict = _COLORDICT
+ # The following 2 class attributes are no longer used
+ # but there is no way to warn about deprecation
+ colorList = silx.config.DEFAULT_PLOT_CURVE_COLORS
+ colorDict = colors.COLORDICT
sigPlotSignal = qt.Signal(object)
"""Signal for all events of the plot.
@@ -371,6 +366,9 @@ class PlotWidget(qt.QMainWindow):
It provides the menu which will be displayed.
"""
+ sigBackendChanged = qt.Signal()
+ """Signal emitted when the backend have changed."""
+
def __init__(self, parent=None, backend=None):
self._autoreplot = False
self._dirty = False
@@ -385,7 +383,7 @@ class PlotWidget(qt.QMainWindow):
# behave as a widget
self.setWindowFlags(qt.Qt.Widget)
else:
- self.setWindowTitle('PlotWidget')
+ self.setWindowTitle("PlotWidget")
# Init the backend
self._backend = self.__getBackendClass(backend)(self, self)
@@ -393,25 +391,28 @@ class PlotWidget(qt.QMainWindow):
self.setCallback() # set _callback
# Items handling
- self._content = OrderedDict()
- self._contentToUpdate = [] # Used as an OrderedSet
+ self.__items = []
+ self.__itemsToUpdate = [] # Used as an OrderedSet
+ self.__activeItems = {"curve": None, "image": None, "scatter": None}
self._dataRange = None
# line types
- self._styleList = ['-', '--', '-.', ':']
+ self._defaultColors = None
+ self._styleList = ["-", "--", "-.", ":"]
self._colorIndex = 0
self._styleIndex = 0
self._activeCurveSelectionMode = "atmostone"
- self._activeCurveStyle = CurveStyle(color='#000000')
- self._activeLegend = {'curve': None, 'image': None,
- 'scatter': None}
+ self._activeCurveStyle = CurveStyle(
+ color=silx.config.DEFAULT_PLOT_ACTIVE_CURVE_COLOR,
+ linewidth=silx.config.DEFAULT_PLOT_ACTIVE_CURVE_LINEWIDTH,
+ )
# plot colors (updated later to sync backend)
- self._foregroundColor = 0., 0., 0., 1.
- self._gridColor = .7, .7, .7, 1.
- self._backgroundColor = 1., 1., 1., 1.
+ self._foregroundColor = 0.0, 0.0, 0.0, 1.0
+ self._gridColor = 0.7, 0.7, 0.7, 1.0
+ self._backgroundColor = 1.0, 1.0, 1.0, 1.0
self._dataBackgroundColor = None
# default properties
@@ -422,18 +423,18 @@ class PlotWidget(qt.QMainWindow):
self._yRightAxis = items.YRightAxis(self, self._yAxis)
self._grid = None
- self._graphTitle = ''
- self.__graphCursorShape = 'default'
+ self._graphTitle = ""
+ self.__graphCursorShape = "default"
# Set axes margins
self.__axesDisplayed = True
- self.__axesMargins = 0., 0., 0., 0.
- self.setAxesMargins(.15, .1, .1, .15)
+ self.__axesMargins = 0.0, 0.0, 0.0, 0.0
+ self.setAxesMargins(0.15, 0.1, 0.1, 0.15)
self.setGraphTitle()
self.setGraphXLabel()
self.setGraphYLabel()
- self.setGraphYLabel('', axis='right')
+ self.setGraphYLabel("", axis="right")
self.setDefaultColormap() # Init default colormap
@@ -443,12 +444,14 @@ class PlotWidget(qt.QMainWindow):
self._limitsHistory = LimitsHistory(self)
self._eventHandler = PlotInteraction.PlotInteraction(self)
- self._eventHandler.setInteractiveMode('zoom', color=(0., 0., 0., 1.))
+ self._eventHandler._setInteractiveMode("zoom", color=(0.0, 0.0, 0.0, 1.0))
+ self._eventHandler.sigChanged.connect(self.__interactionChanged)
+ self.__isInteractionSignalForwarded = True
self._previousDefaultMode = "zoom", True
self._pressedButtons = [] # Currently pressed mouse buttons
- self._defaultDataMargins = (0., 0., 0., 0.)
+ self._defaultDataMargins = (0.0, 0.0, 0.0, 0.0)
# Only activate autoreplot at the end
# This avoids errors when loaded in Qt designer
@@ -465,9 +468,9 @@ class PlotWidget(qt.QMainWindow):
self.setFocus(qt.Qt.OtherFocusReason)
# Set default limits
- self.setGraphXLimits(0., 100.)
- self.setGraphYLimits(0., 100., axis='right')
- self.setGraphYLimits(0., 100., axis='left')
+ self.setGraphXLimits(0.0, 100.0)
+ self.setGraphYLimits(0.0, 100.0, axis="right")
+ self.setGraphYLimits(0.0, 100.0, axis="left")
# Sync backend colors with default ones
self._foregroundColorsUpdated()
@@ -495,30 +498,32 @@ class PlotWidget(qt.QMainWindow):
elif isinstance(backend, str):
backend = backend.lower()
- if backend in ('matplotlib', 'mpl'):
+ if backend in ("matplotlib", "mpl"):
try:
- from .backends.BackendMatplotlib import \
- BackendMatplotlibQt as backendClass
+ from .backends.BackendMatplotlib import (
+ BackendMatplotlibQt as backendClass,
+ )
except ImportError:
_logger.debug("Backtrace", exc_info=True)
raise RuntimeError("matplotlib backend is not available")
- elif backend in ('gl', 'opengl'):
+ elif backend in ("gl", "opengl"):
from ..utils.glutils import isOpenGLAvailable
+
checkOpenGL = isOpenGLAvailable(version=(2, 1), runtimeCheck=False)
if not checkOpenGL:
_logger.debug("OpenGL check failed")
raise RuntimeError(
- "OpenGL backend is not available: %s" % checkOpenGL.error)
+ "OpenGL backend is not available: %s" % checkOpenGL.error
+ )
try:
- from .backends.BackendOpenGL import \
- BackendOpenGL as backendClass
+ from .backends.BackendOpenGL import BackendOpenGL as backendClass
except ImportError:
_logger.debug("Backtrace", exc_info=True)
raise RuntimeError("OpenGL backend is not available")
- elif backend == 'none':
+ elif backend == "none":
from .backends.BackendBase import BackendBase as backendClass
else:
@@ -543,20 +548,6 @@ class PlotWidget(qt.QMainWindow):
self.__selection = _PlotWidgetSelection(parent=self)
return self.__selection
- # TODO: Can be removed for silx 0.10
- @staticmethod
- @deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
- def setDefaultBackend(backend):
- """Set system wide default plot backend.
-
- .. versionadded:: 0.6
-
- :param backend: The backend to use, in:
- 'matplotlib' (default), 'mpl', 'opengl', 'gl', 'none'
- or a :class:`BackendBase.BackendBase` class
- """
- silx.config.DEFAULT_PLOT_BACKEND = backend
-
def setBackend(self, backend):
"""Set the backend to use for rendering.
@@ -579,8 +570,8 @@ class PlotWidget(qt.QMainWindow):
# First save state that is stored in the backend
xaxis = self.getXAxis()
xmin, xmax = xaxis.getLimits()
- ymin, ymax = self.getYAxis(axis='left').getLimits()
- y2min, y2max = self.getYAxis(axis='right').getLimits()
+ ymin, ymax = self.getYAxis(axis="left").getLimits()
+ y2min, y2max = self.getYAxis(axis="right").getLimits()
isKeepDataAspectRatio = self.isKeepDataAspectRatio()
xTimeZone = xaxis.getTimeZone()
isXAxisTimeSeries = xaxis.getTickMode() == TickMode.TIME_SERIES
@@ -609,7 +600,7 @@ class PlotWidget(qt.QMainWindow):
self._backend.setGraphCursorShape(self.getGraphCursorShape())
crosshairConfig = self.getGraphCursor()
if crosshairConfig is None:
- self._backend.setGraphCursor(False, 'black', 1, '-')
+ self._backend.setGraphCursor(False, "black", 1, "-")
else:
self._backend.setGraphCursor(True, *crosshairConfig)
@@ -618,21 +609,21 @@ class PlotWidget(qt.QMainWindow):
if self.isAxesDisplayed():
self._backend.setAxesMargins(*self.getAxesMargins())
else:
- self._backend.setAxesMargins(0., 0., 0., 0.)
+ self._backend.setAxesMargins(0.0, 0.0, 0.0, 0.0)
# Set axes
xaxis = self.getXAxis()
self._backend.setGraphXLabel(xaxis.getLabel())
self._backend.setXAxisTimeZone(xTimeZone)
self._backend.setXAxisTimeSeries(isXAxisTimeSeries)
- self._backend.setXAxisLogarithmic(
- xaxis.getScale() == items.Axis.LOGARITHMIC)
+ self._backend.setXAxisLogarithmic(xaxis.getScale() == items.Axis.LOGARITHMIC)
- for axis in ('left', 'right'):
+ for axis in ("left", "right"):
self._backend.setGraphYLabel(self.getYAxis(axis).getLabel(), axis)
self._backend.setYAxisInverted(isYAxisInverted)
self._backend.setYAxisLogarithmic(
- self.getYAxis().getScale() == items.Axis.LOGARITHMIC)
+ self.getYAxis().getScale() == items.Axis.LOGARITHMIC
+ )
# Finally restore aspect ratio and limits
self._backend.setKeepDataAspectRatio(isKeepDataAspectRatio)
@@ -642,6 +633,8 @@ class PlotWidget(qt.QMainWindow):
for item in self.getItems():
item._updated()
+ self.sigBackendChanged.emit()
+
def getBackend(self):
"""Returns the backend currently used by :class:`PlotWidget`.
@@ -668,12 +661,16 @@ class PlotWidget(qt.QMainWindow):
"""Override QWidget.contextMenuEvent to implement the context menu"""
menu = qt.QMenu(self)
from .actions.control import ZoomBackAction # Avoid cyclic import
+
zoomBackAction = ZoomBackAction(plot=self, parent=menu)
menu.addAction(zoomBackAction)
mode = self.getInteractiveMode()
if "shape" in mode and mode["shape"] == "polygon":
- from .actions.control import ClosePolygonInteractionAction # Avoid cyclic import
+ from .actions.control import (
+ ClosePolygonInteractionAction,
+ ) # Avoid cyclic import
+
action = ClosePolygonInteractionAction(plot=self, parent=menu)
menu.addAction(action)
@@ -694,7 +691,7 @@ class PlotWidget(qt.QMainWindow):
wasDirty = self._dirty
if not self._dirty and overlayOnly:
- self._dirty = 'overlay'
+ self._dirty = "overlay"
else:
self._dirty = True
@@ -707,8 +704,7 @@ class PlotWidget(qt.QMainWindow):
gridColor = self._foregroundColor
else:
gridColor = self._gridColor
- self._backend.setForegroundColors(
- self._foregroundColor, gridColor)
+ self._backend.setForegroundColors(self._foregroundColor, gridColor)
self._setDirtyPlot()
def getForegroundColor(self):
@@ -762,8 +758,7 @@ class PlotWidget(qt.QMainWindow):
dataBGColor = self._backgroundColor
else:
dataBGColor = self._dataBackgroundColor
- self._backend.setBackgroundColors(
- self._backgroundColor, dataBGColor)
+ self._backend.setBackgroundColors(self._backgroundColor, dataBGColor)
self._setDirtyPlot()
def getBackgroundColor(self):
@@ -832,7 +827,14 @@ class PlotWidget(qt.QMainWindow):
def hideEvent(self, event):
super(PlotWidget, self).hideEvent(event)
- self.sigVisibilityChanged.emit(False)
+ if qt.BINDING == "PySide6":
+ # Workaround RuntimeError: The SignalInstance object was already deleted
+ try:
+ self.sigVisibilityChanged.emit(False)
+ except RuntimeError as e:
+ _logger.error(f"Exception occured: {e}")
+ else:
+ self.sigVisibilityChanged.emit(False)
def _invalidateDataRange(self):
"""
@@ -845,42 +847,43 @@ class PlotWidget(qt.QMainWindow):
"""
Recomputes the range of the data displayed on this PlotWidget.
"""
- xMin = yMinLeft = yMinRight = float('nan')
- xMax = yMaxLeft = yMaxRight = float('nan')
+ xMin = yMinLeft = yMinRight = float("nan")
+ xMax = yMaxLeft = yMaxRight = float("nan")
for item in self.getItems():
if item.isVisible():
bounds = item.getBounds()
if bounds is not None:
with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ warnings.simplefilter("ignore", category=RuntimeWarning)
# Ignore All-NaN slice encountered
xMin = numpy.nanmin([xMin, bounds[0]])
xMax = numpy.nanmax([xMax, bounds[1]])
# Take care of right axis
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
+ if (
+ isinstance(item, items.YAxisMixIn)
+ and item.getYAxis() == "right"
+ ):
with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ warnings.simplefilter("ignore", category=RuntimeWarning)
# Ignore All-NaN slice encountered
yMinRight = numpy.nanmin([yMinRight, bounds[2]])
yMaxRight = numpy.nanmax([yMaxRight, bounds[3]])
else:
with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
+ warnings.simplefilter("ignore", category=RuntimeWarning)
# Ignore All-NaN slice encountered
yMinLeft = numpy.nanmin([yMinLeft, bounds[2]])
yMaxLeft = numpy.nanmax([yMaxLeft, bounds[3]])
def lGetRange(x, y):
return None if numpy.isnan(x) and numpy.isnan(y) else (x, y)
+
xRange = lGetRange(xMin, xMax)
yLeftRange = lGetRange(yMinLeft, yMaxLeft)
yRightRange = lGetRange(yMinRight, yMaxRight)
- self._dataRange = _PlotDataRange(x=xRange,
- y=yLeftRange,
- yright=yRightRange)
+ self._dataRange = _PlotDataRange(x=xRange, y=yLeftRange, yright=yRightRange)
def getDataRange(self):
"""
@@ -898,16 +901,19 @@ class PlotWidget(qt.QMainWindow):
# Content management
_KIND_TO_CLASSES = {
- 'curve': (items.Curve,),
- 'image': (items.ImageBase,),
- 'scatter': (items.Scatter,),
- 'marker': (items.MarkerBase,),
- 'item': (items.Shape,
- items.BoundingRect,
- items.XAxisExtent,
- items.YAxisExtent),
- 'histogram': (items.Histogram,),
- }
+ "curve": (items.Curve,),
+ "image": (items.ImageBase,),
+ "scatter": (items.Scatter,),
+ "marker": (items.MarkerBase,),
+ "item": (
+ items.Line,
+ items.Shape,
+ items.BoundingRect,
+ items.XAxisExtent,
+ items.YAxisExtent,
+ ),
+ "histogram": (items.Histogram,),
+ }
"""Mapping kind to item classes of this kind"""
@classmethod
@@ -920,11 +926,15 @@ class PlotWidget(qt.QMainWindow):
for kind, itemClasses in cls._KIND_TO_CLASSES.items():
if isinstance(item, itemClasses):
return kind
- raise ValueError('Unsupported item type %s' % type(item))
+ return "other"
def _notifyContentChanged(self, item):
- self.notify('contentChanged', action='add',
- kind=self._itemKind(item), legend=item.getName())
+ self.notify(
+ "contentChanged",
+ action="add",
+ kind=self._itemKind(item),
+ legend=item.getName(),
+ )
def _itemRequiresUpdate(self, item):
"""Called by items in the plot for asynchronous update
@@ -933,34 +943,25 @@ class PlotWidget(qt.QMainWindow):
"""
assert item.getPlot() == self
# Put item at the end of the list
- if item in self._contentToUpdate:
- self._contentToUpdate.remove(item)
- self._contentToUpdate.append(item)
+ if item in self.__itemsToUpdate:
+ self.__itemsToUpdate.remove(item)
+ self.__itemsToUpdate.append(item)
self._setDirtyPlot(overlayOnly=item.isOverlay())
- def addItem(self, item=None, *args, **kwargs):
+ def addItem(self, item):
"""Add an item to the plot content.
:param ~silx.gui.plot.items.Item item: The item to add.
:raises ValueError: If item is already in the plot.
"""
if not isinstance(item, items.Item):
- deprecated_warning(
- 'Function',
- 'addItem',
- replacement='addShape',
- since_version='0.13')
- if item is None and not args: # Only kwargs
- return self.addShape(**kwargs)
- else:
- return self.addShape(item, *args, **kwargs)
+ raise ValueError(f"argument must be a subclass of Item")
- assert not args and not kwargs
if item in self.getItems():
- raise ValueError('Item already in the plot')
+ raise ValueError("Item already in the plot")
# Add item to plot
- self._content[(item.getName(), self._itemKind(item))] = item
+ self.__items.append(item)
item._setPlot(self)
self._itemRequiresUpdate(item)
if isinstance(item, items.DATA_ITEMS):
@@ -975,19 +976,11 @@ class PlotWidget(qt.QMainWindow):
:param ~silx.gui.plot.items.Item item: Item to remove from the plot.
:raises ValueError: If item is not in the plot.
"""
- if not isinstance(item, items.Item): # Previous method usage
- deprecated_warning(
- 'Function',
- 'removeItem',
- replacement='remove(legend, kind="item")',
- since_version='0.13')
- if item is None:
- return
- self.remove(item, kind='item')
- return
+ if not isinstance(item, items.Item):
+ raise ValueError("argument must be an Item")
if item not in self.getItems():
- raise ValueError('Item not in the plot')
+ raise ValueError("Item not in the plot")
self.sigItemAboutToBeRemoved.emit(item)
@@ -999,9 +992,9 @@ class PlotWidget(qt.QMainWindow):
self._setActiveItem(kind, None)
# Remove item from plot
- self._content.pop((item.getName(), kind))
- if item in self._contentToUpdate:
- self._contentToUpdate.remove(item)
+ self.__items.remove(item)
+ if item in self.__itemsToUpdate:
+ self.__itemsToUpdate.remove(item)
if item.isVisible():
self._setDirtyPlot(overlayOnly=item.isOverlay())
if item.getBounds() is not None:
@@ -1009,14 +1002,12 @@ class PlotWidget(qt.QMainWindow):
item._removeBackendRenderer(self._backend)
item._setPlot(None)
- if (kind == 'curve' and not self.getAllCurves(just_legend=True,
- withhidden=True)):
+ if kind == "curve" and not self.getAllCurves(just_legend=True, withhidden=True):
self._resetColorAndStyle()
self.sigItemRemoved.emit(item)
- self.notify('contentChanged', action='remove',
- kind=kind, legend=item.getName())
+ self.notify("contentChanged", action="remove", kind=kind, legend=item.getName())
def discardItem(self, item) -> bool:
"""Remove the item from the plot.
@@ -1033,20 +1024,12 @@ class PlotWidget(qt.QMainWindow):
else:
return True
- @deprecated(replacement='addItem', since_version='0.13')
- def _add(self, item):
- return self.addItem(item)
-
- @deprecated(replacement='removeItem', since_version='0.13')
- def _remove(self, item):
- return self.removeItem(item)
-
def getItems(self):
"""Returns the list of items in the plot
:rtype: List[silx.gui.plot.items.Item]
"""
- return tuple(self._content.values())
+ return tuple(self.__items)
@contextmanager
def _muteActiveItemChangedSignal(self):
@@ -1064,15 +1047,30 @@ class PlotWidget(qt.QMainWindow):
# 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,
- 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,
- baseline=None):
+ def addCurve(
+ self,
+ x,
+ y,
+ legend=None,
+ info=None,
+ replace=False,
+ color=None,
+ symbol=None,
+ linewidth=None,
+ linestyle=None,
+ xlabel=None,
+ ylabel=None,
+ yaxis=None,
+ xerror=None,
+ yerror=None,
+ z=None,
+ selectable=None,
+ fill=None,
+ resetzoom=True,
+ histogram=None,
+ copy=True,
+ baseline=None,
+ ):
"""Add a 1D curve given by x an y to the graph.
Curves are uniquely identified by their legend.
@@ -1130,8 +1128,8 @@ class PlotWidget(qt.QMainWindow):
: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.
+ of same length as the data: row 0 for lower errors,
+ row 1 for upper 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)
@@ -1155,18 +1153,19 @@ class PlotWidget(qt.QMainWindow):
False to use provided arrays.
:param baseline: curve baseline
:type: Union[None,float,numpy.ndarray]
- :returns: The key string identify this curve
+ :returns: The curve item
"""
# 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 = self.addHistogram(
+ histogram=y,
+ edges=x,
+ legend=legend,
+ color=color,
+ fill=fill,
+ align=histogram,
+ copy=copy,
+ )
histo.setInfo(info)
if linewidth is not None:
@@ -1174,25 +1173,21 @@ class PlotWidget(qt.QMainWindow):
if linestyle is not None:
histo.setLineStyle(linestyle)
if xlabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support xlabel argument')
+ _logger.warning("addCurve: Histogram does not support xlabel argument")
if ylabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support ylabel argument')
+ _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')
+ "addCurve: Histogram does not support selectable argument"
+ )
- return
-
- legend = 'Unnamed curve 1.1' if legend is None else str(legend)
+ return histo
- # Check if curve was previously active
- wasActive = self.getActiveCurve(just_legend=True) == legend
+ legend = "Unnamed curve 1.1" if legend is None else str(legend)
if replace:
self._resetColorAndStyle()
@@ -1217,7 +1212,11 @@ class PlotWidget(qt.QMainWindow):
# Override previous/default values with provided ones
curve.setInfo(info)
if color is not None:
- curve.setColor(color)
+ curve.setColor(
+ colors.rgba(color, colors=self.getDefaultColors())
+ if isinstance(color, str)
+ else color
+ )
if symbol is not None:
curve.setSymbol(symbol)
if linewidth is not None:
@@ -1264,14 +1263,13 @@ class PlotWidget(qt.QMainWindow):
else:
self._notifyContentChanged(curve)
- if wasActive:
- self.setActiveCurve(curve.getName())
- elif self.getActiveCurveSelectionMode() == "legacy":
- if self.getActiveCurve(just_legend=True) is None:
- if len(self.getAllCurves(just_legend=True,
- withhidden=False)) == 1:
- if curve.isVisible():
- self.setActiveCurve(curve.getName())
+ if curve is self.getActiveCurve() or (
+ self.getActiveCurveSelectionMode() == "legacy"
+ and self.getActiveCurve() is None
+ and len(self.getAllCurves(just_legend=True, withhidden=False)) == 1
+ and curve.isVisible()
+ ):
+ self.setActiveCurve(curve)
if resetzoom:
# We ask for a zoom reset in order to handle the plot scaling
@@ -1279,19 +1277,21 @@ class PlotWidget(qt.QMainWindow):
# 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,
- z=None,
- baseline=None):
+ return curve
+
+ def addHistogram(
+ self,
+ histogram,
+ edges,
+ legend=None,
+ color=None,
+ fill=None,
+ align="center",
+ resetzoom=True,
+ copy=True,
+ z=None,
+ baseline=None,
+ ):
"""Add an histogram to the graph.
This is NOT computing the histogram, this method takes as parameter
@@ -1325,9 +1325,9 @@ class PlotWidget(qt.QMainWindow):
:param int z: Layer on which to draw the histogram
:param baseline: histogram baseline
:type: Union[None,float,numpy.ndarray]
- :returns: The key string identify this histogram
+ :returns: The histogram item
"""
- legend = 'Unnamed histogram' if legend is None else str(legend)
+ legend = "Unnamed histogram" if legend is None else str(legend)
# Create/Update histogram object
histo = self.getHistogram(legend)
@@ -1341,15 +1341,20 @@ class PlotWidget(qt.QMainWindow):
# Override previous/default values with provided ones
if color is not None:
- histo.setColor(color)
+ histo.setColor(
+ colors.rgba(color, colors=self.getDefaultColors())
+ if isinstance(color, str)
+ else color
+ )
if fill is not None:
histo.setFill(fill)
if z is not None:
histo.setZValue(z=z)
# Set histogram data
- histo.setData(histogram=histogram, edges=edges, baseline=baseline,
- align=align, copy=copy)
+ histo.setData(
+ histogram=histogram, edges=edges, baseline=baseline, align=align, copy=copy
+ )
if mustBeAdded:
self.addItem(histo)
@@ -1362,16 +1367,26 @@ class PlotWidget(qt.QMainWindow):
# axes has to be set to off.
self.resetZoom()
- return legend
-
- def addImage(self, data, legend=None, info=None,
- replace=False,
- z=None,
- selectable=None, draggable=None,
- colormap=None, pixmap=None,
- xlabel=None, ylabel=None,
- origin=None, scale=None,
- resetzoom=True, copy=True):
+ return histo
+
+ def addImage(
+ self,
+ data,
+ legend=None,
+ info=None,
+ replace=False,
+ z=None,
+ selectable=None,
+ draggable=None,
+ colormap=None,
+ pixmap=None,
+ xlabel=None,
+ ylabel=None,
+ origin=None,
+ scale=None,
+ resetzoom=True,
+ copy=True,
+ ):
"""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.
@@ -1421,13 +1436,10 @@ class PlotWidget(qt.QMainWindow):
: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
+ :returns: The image item
"""
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)
@@ -1480,7 +1492,8 @@ class PlotWidget(qt.QMainWindow):
else: # RGB(A) image
if pixmap is not None:
_logger.warning(
- 'addImage: pixmap argument ignored when data is RGB(A)')
+ "addImage: pixmap argument ignored when data is RGB(A)"
+ )
image.setData(data, copy=copy)
if replace:
@@ -1493,8 +1506,8 @@ class PlotWidget(qt.QMainWindow):
else:
self._notifyContentChanged(image)
- if len(self.getAllImages()) == 1 or wasActive:
- self.setActiveImage(legend)
+ if len(self.getAllImages()) == 1 or image is self.getActiveImage():
+ self.setActiveImage(image)
if resetzoom:
# We ask for a zoom reset in order to handle the plot scaling
@@ -1502,11 +1515,22 @@ class PlotWidget(qt.QMainWindow):
# 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):
+ return image
+
+ 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.
@@ -1540,8 +1564,8 @@ class PlotWidget(qt.QMainWindow):
: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.
+ of same length as the data: row 0 for lower errors,
+ row 1 for upper 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)
@@ -1549,16 +1573,12 @@ class PlotWidget(qt.QMainWindow):
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
- :returns: The key string identify this scatter
+ :returns: The scatter item
"""
- 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
+ legend = "Unnamed scatter 1.1" if legend is None else str(legend)
# Create/Update curve object
- scatter = self._getItem(kind='scatter', legend=legend)
+ scatter = self._getItem(kind="scatter", legend=legend)
mustBeAdded = scatter is None
if scatter is None:
# No previous scatter, create a default one and add it to the plot
@@ -1600,18 +1620,33 @@ class PlotWidget(qt.QMainWindow):
else:
self._notifyContentChanged(scatter)
- scatters = [item for item in self.getItems()
- if isinstance(item, items.Scatter) and item.isVisible()]
- if len(scatters) == 1 or wasActive:
- self._setActiveItem('scatter', scatter.getName())
-
- return legend
-
- def addShape(self, xdata, ydata, legend=None, info=None,
- replace=False,
- shape="polygon", color='black', fill=True,
- overlay=False, z=None, linestyle="-", linewidth=1.0,
- linebgcolor=None):
+ scatters = [
+ item
+ for item in self.getItems()
+ if isinstance(item, items.Scatter) and item.isVisible()
+ ]
+ if len(scatters) == 1 or scatter is self.getActiveScatter():
+ self.setActiveScatter(scatter)
+
+ return scatter
+
+ def addShape(
+ self,
+ xdata,
+ ydata,
+ legend=None,
+ info=None,
+ replace=False,
+ shape="polygon",
+ color="black",
+ fill=True,
+ overlay=False,
+ z=None,
+ linestyle="-",
+ linewidth=1.0,
+ linebgcolor="deprecated",
+ gapcolor=None,
+ ):
"""Add an item (i.e. a shape) to the plot.
Items are uniquely identified by their legend.
@@ -1624,7 +1659,8 @@ class PlotWidget(qt.QMainWindow):
: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 bool replace: True to delete already existing items
+ (the default is False)
:param str shape: Type of item to be drawn in
hline, polygon (the default), rectangle, vline,
polylines
@@ -1646,9 +1682,9 @@ class PlotWidget(qt.QMainWindow):
- ':' dotted line
:param float linewidth: Width of the line.
Only relevant for line markers where X or Y is None.
- :param str linebgcolor: Background color of the line, e.g., 'blue', 'b',
+ :param str gapcolor: Gap color of the line, e.g., 'blue', 'b',
'#FF0000'. It is used to draw dotted line using a second color.
- :returns: The key string identify this item
+ :returns: The shape item
"""
# expected to receive the same parameters as the signal
@@ -1657,9 +1693,9 @@ class PlotWidget(qt.QMainWindow):
z = int(z) if z is not None else 2
if replace:
- self.remove(kind='item')
+ self.remove(kind="item")
else:
- self.remove(legend, kind='item')
+ self.remove(legend, kind="item")
item = items.Shape(shape)
item.setName(legend)
@@ -1671,19 +1707,31 @@ class PlotWidget(qt.QMainWindow):
item.setPoints(numpy.array((xdata, ydata)).T)
item.setLineStyle(linestyle)
item.setLineWidth(linewidth)
- item.setLineBgColor(linebgcolor)
+ if linebgcolor != "deprecated":
+ deprecated_warning(
+ type_="Argument",
+ name="linebgcolor",
+ replacement="gapcolor",
+ since_version="2.0.0",
+ )
+ gapcolor = linebgcolor if gapcolor is None else gapcolor
+ item.setLineGapColor(gapcolor)
self.addItem(item)
- return legend
-
- def addXMarker(self, x, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- yaxis='left'):
+ return item
+
+ def addXMarker(
+ self,
+ x,
+ legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ constraint=None,
+ yaxis="left",
+ ):
"""Add a vertical line marker to the plot.
Markers are uniquely identified by their legend.
@@ -1710,22 +1758,32 @@ class PlotWidget(qt.QMainWindow):
the current cursor position in the plot as input
and that returns the filtered coordinates.
:param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
- """
- return self._addMarker(x=x, y=None, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint,
- yaxis=yaxis)
-
- def addYMarker(self, y,
- legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- yaxis='left'):
+ :return: The marker item
+ """
+ return self._addMarker(
+ x=x,
+ y=None,
+ legend=legend,
+ text=text,
+ color=color,
+ selectable=selectable,
+ draggable=draggable,
+ symbol=None,
+ constraint=constraint,
+ yaxis=yaxis,
+ )
+
+ def addYMarker(
+ self,
+ y,
+ legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ constraint=None,
+ yaxis="left",
+ ):
"""Add a horizontal line marker to the plot.
Markers are uniquely identified by their legend.
@@ -1752,22 +1810,34 @@ class PlotWidget(qt.QMainWindow):
the current cursor position in the plot as input
and that returns the filtered coordinates.
:param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
- """
- return self._addMarker(x=None, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint,
- yaxis=yaxis)
-
- def addMarker(self, x, y, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- symbol='+',
- constraint=None,
- yaxis='left'):
+ :return: The marker item
+ """
+ return self._addMarker(
+ x=None,
+ y=y,
+ legend=legend,
+ text=text,
+ color=color,
+ selectable=selectable,
+ draggable=draggable,
+ symbol=None,
+ constraint=constraint,
+ yaxis=yaxis,
+ )
+
+ def addMarker(
+ self,
+ x,
+ y,
+ legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ symbol="+",
+ constraint=None,
+ yaxis="left",
+ ):
"""Add a point marker to the plot.
Markers are uniquely identified by their legend.
@@ -1806,7 +1876,7 @@ class PlotWidget(qt.QMainWindow):
the current cursor position in the plot as input
and that returns the filtered coordinates.
:param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
+ :return: The marker item
"""
if x is None:
xmin, xmax = self._xAxis.getLimits()
@@ -1816,17 +1886,32 @@ class PlotWidget(qt.QMainWindow):
ymin, ymax = self._yAxis.getLimits()
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,
- yaxis=yaxis)
-
- def _addMarker(self, x, y, legend,
- text, color,
- selectable, draggable,
- symbol, constraint,
- yaxis=None):
+ return self._addMarker(
+ x=x,
+ y=y,
+ legend=legend,
+ text=text,
+ color=color,
+ selectable=selectable,
+ draggable=draggable,
+ symbol=symbol,
+ constraint=constraint,
+ yaxis=yaxis,
+ )
+
+ def _addMarker(
+ self,
+ x,
+ y,
+ legend,
+ text,
+ color,
+ selectable,
+ draggable,
+ symbol,
+ constraint,
+ yaxis=None,
+ ):
"""Common method for adding point, vline and hline marker.
See :meth:`addMarker` for argument documentation.
@@ -1834,8 +1919,11 @@ class PlotWidget(qt.QMainWindow):
assert (x, y) != (None, None)
if legend is None: # Find an unused legend
- markerLegends = [item.getName() for item in self.getItems()
- if isinstance(item, items.MarkerBase)]
+ markerLegends = [
+ item.getName()
+ for item in self.getItems()
+ if isinstance(item, items.MarkerBase)
+ ]
for index in itertools.count():
legend = "Unnamed Marker %d" % index
if legend not in markerLegends:
@@ -1852,8 +1940,9 @@ class PlotWidget(qt.QMainWindow):
# 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')
+ _logger.warning(
+ "Adding marker with same legend" " but different type replaces it"
+ )
self.removeItem(marker)
marker = None
@@ -1886,7 +1975,7 @@ class PlotWidget(qt.QMainWindow):
else:
self._notifyContentChanged(marker)
- return legend
+ return marker
# Hide
@@ -1896,7 +1985,7 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend key identifying the curve
:return: True if the associated curve is hidden, False otherwise
"""
- curve = self._getItem('curve', legend)
+ curve = self._getItem("curve", legend)
return curve is not None and not curve.isVisible()
def hideCurve(self, legend, flag=True):
@@ -1907,9 +1996,9 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend associated to the curve to be hidden
:param bool flag: True (default) to hide the curve, False to show it
"""
- curve = self._getItem('curve', legend)
+ curve = self._getItem("curve", legend)
if curve is None:
- _logger.warning('Curve not in plot: %s', legend)
+ _logger.warning("Curve not in plot: %s", legend)
return
isVisible = not flag
@@ -1918,13 +2007,17 @@ class PlotWidget(qt.QMainWindow):
# Remove
- ITEM_KINDS = 'curve', 'image', 'scatter', 'item', 'marker', 'histogram'
+ ITEM_KINDS = "curve", "image", "scatter", "item", "marker", "histogram"
"""List of supported kind of items in the plot."""
- _ACTIVE_ITEM_KINDS = 'curve', 'scatter', 'image'
+ _ACTIVE_ITEM_KINDS = "curve", "scatter", "image"
"""List of item's kind which have a active item."""
- def remove(self, legend=None, kind=ITEM_KINDS):
+ def remove(
+ self,
+ legend: str | items.Item | None = None,
+ kind: str | Sequence[str] = ITEM_KINDS,
+ ):
"""Remove one or all element(s) of the given legend and kind.
Examples:
@@ -1938,14 +2031,17 @@ class PlotWidget(qt.QMainWindow):
- ``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.
+ :param legend:
+ The legend of the item to remove or the item itself.
+ If None all items of given kind are removed.
+ :param kind: The kind of items to remove from the plot.
See :attr:`ITEM_KINDS`.
By default, it removes all kind of elements.
- :type kind: str or tuple of str to specify multiple kinds.
"""
- if kind == 'all': # Replace all by tuple of all kinds
+ if isinstance(legend, items.Item):
+ return self.removeItem(legend)
+
+ if kind == "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
@@ -1958,8 +2054,10 @@ class PlotWidget(qt.QMainWindow):
# Clear each given kind
for aKind in kind:
for item in self.getItems():
- if (isinstance(item, self._KIND_TO_CLASSES[aKind]) and
- item.getPlot() is self): # Make sure item is still in the plot
+ if (
+ isinstance(item, self._KIND_TO_CLASSES[aKind])
+ and item.getPlot() is self
+ ): # Make sure item is still in the plot
self.removeItem(item)
else: # This is removing a single element
@@ -1969,32 +2067,41 @@ class PlotWidget(qt.QMainWindow):
if item is not None:
self.removeItem(item)
- def removeCurve(self, legend):
+ def removeCurve(self, legend: str | items.Curve | None):
"""Remove the curve associated to legend from the graph.
- :param str legend: The legend associated to the curve to be deleted
+ :param legend:
+ The legend of the curve to be deleted or the curve item
"""
if legend is None:
return
- self.remove(legend, kind='curve')
+ if isinstance(legend, items.Item):
+ return self.removeItem(legend)
+ self.remove(legend, kind="curve")
- def removeImage(self, legend):
+ def removeImage(self, legend: str | items.ImageBase | None):
"""Remove the image associated to legend from the graph.
- :param str legend: The legend associated to the image to be deleted
+ :param legend:
+ The legend of the image to be deleted or the image item
"""
if legend is None:
return
- self.remove(legend, kind='image')
+ if isinstance(legend, items.Item):
+ return self.removeItem(legend)
+ self.remove(legend, kind="image")
- def removeMarker(self, legend):
+ def removeMarker(self, legend: str | items.Marker | None):
"""Remove the marker associated to legend from the graph.
- :param str legend: The legend associated to the marker to be deleted
+ :param legend:
+ The legend of the marker to be deleted or the marker item
"""
if legend is None:
return
- self.remove(legend, kind='marker')
+ if isinstance(legend, items.Item):
+ return self.removeItem(legend)
+ self.remove(legend, kind="marker")
# Clear
@@ -2006,19 +2113,19 @@ class PlotWidget(qt.QMainWindow):
def clearCurves(self):
"""Remove all the curves from the plot."""
- self.remove(kind='curve')
+ self.remove(kind="curve")
def clearImages(self):
"""Remove all the images from the plot."""
- self.remove(kind='image')
+ self.remove(kind="image")
def clearItems(self):
- """Remove all the items from the plot. """
- self.remove(kind='item')
+ """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')
+ self.remove(kind="marker")
# Interaction
@@ -2032,8 +2139,7 @@ class PlotWidget(qt.QMainWindow):
"""
return self._cursorConfiguration
- def setGraphCursor(self, flag=False, color='black',
- linewidth=1, linestyle='-'):
+ 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.
@@ -2057,11 +2163,11 @@ class PlotWidget(qt.QMainWindow):
else:
self._cursorConfiguration = None
- self._backend.setGraphCursor(flag=flag, color=color,
- linewidth=linewidth, linestyle=linestyle)
+ self._backend.setGraphCursor(
+ flag=flag, color=color, linewidth=linewidth, linestyle=linestyle
+ )
self._setDirtyPlot()
- self.notify('setGraphCursor',
- state=self._cursorConfiguration is not None)
+ 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.
@@ -2072,20 +2178,21 @@ class PlotWidget(qt.QMainWindow):
: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.
+ assert direction in ("up", "down", "left", "right")
+ assert factor > 0.0
- if direction in ('left', 'right'):
- xFactor = factor if direction == 'right' else - factor
+ if direction in ("left", "right"):
+ xFactor = factor if direction == "right" else -factor
xMin, xMax = self._xAxis.getLimits()
- xMin, xMax = _utils.applyPan(xMin, xMax, xFactor,
- self._xAxis.getScale() == self._xAxis.LOGARITHMIC)
+ xMin, xMax = _utils.applyPan(
+ xMin, xMax, xFactor, self._xAxis.getScale() == self._xAxis.LOGARITHMIC
+ )
self._xAxis.setLimits(xMin, xMax)
else: # direction in ('up', 'down')
- sign = -1. if self._yAxis.isInverted() else 1.
- yFactor = sign * (factor if direction == 'up' else -factor)
+ sign = -1.0 if self._yAxis.isInverted() else 1.0
+ yFactor = sign * (factor if direction == "up" else -factor)
yMin, yMax = self._yAxis.getLimits()
yIsLog = self._yAxis.getScale() == self._yAxis.LOGARITHMIC
@@ -2104,7 +2211,7 @@ class PlotWidget(qt.QMainWindow):
:rtype: bool
"""
- return self.getActiveCurveSelectionMode() != 'none'
+ return self.getActiveCurveSelectionMode() != "none"
def setActiveCurveHandling(self, flag=True):
"""Enable/Disable active curve selection.
@@ -2112,7 +2219,7 @@ class PlotWidget(qt.QMainWindow):
:param bool flag: True to enable 'atmostone' active curve selection,
False to disable active curve selection.
"""
- self.setActiveCurveSelectionMode('atmostone' if flag else 'none')
+ self.setActiveCurveSelectionMode("atmostone" if flag else "none")
def getActiveCurveStyle(self):
"""Returns the current style applied to active curve
@@ -2121,12 +2228,9 @@ class PlotWidget(qt.QMainWindow):
"""
return self._activeCurveStyle
- def setActiveCurveStyle(self,
- color=None,
- linewidth=None,
- linestyle=None,
- symbol=None,
- symbolsize=None):
+ def setActiveCurveStyle(
+ self, color=None, linewidth=None, linestyle=None, symbol=None, symbolsize=None
+ ):
"""Set the style of active curve
:param color: Color
@@ -2135,36 +2239,17 @@ class PlotWidget(qt.QMainWindow):
:param Union[str,None] symbol: Symbol of the markers
:param Union[float,None] symbolsize: Size of the symbols
"""
- self._activeCurveStyle = CurveStyle(color=color,
- linewidth=linewidth,
- linestyle=linestyle,
- symbol=symbol,
- symbolsize=symbolsize)
+ self._activeCurveStyle = CurveStyle(
+ color=color,
+ linewidth=linewidth,
+ linestyle=linestyle,
+ symbol=symbol,
+ symbolsize=symbolsize,
+ )
curve = self.getActiveCurve()
if curve is not None:
curve.setHighlightedStyle(self.getActiveCurveStyle())
- @deprecated(replacement="getActiveCurveStyle", since_version="0.9")
- def getActiveCurveColor(self):
- """Get the color used to display the currently active curve.
-
- See :meth:`setActiveCurveColor`.
- """
- return self._activeCurveStyle.getColor()
-
- @deprecated(replacement="setActiveCurveStyle", since_version="0.9")
- 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.setActiveCurveStyle(color=color)
-
def getActiveCurve(self, just_legend=False):
"""Return the currently active curve.
@@ -2180,7 +2265,7 @@ class PlotWidget(qt.QMainWindow):
if not self.isActiveCurveHandling():
return None
- return self._getActiveItem(kind='curve', just_legend=just_legend)
+ return self._getActiveItem(kind="curve", just_legend=just_legend)
def setActiveCurve(self, legend):
"""Make the curve associated to legend the active curve.
@@ -2193,10 +2278,11 @@ class PlotWidget(qt.QMainWindow):
return
if legend is None and self.getActiveCurveSelectionMode() == "legacy":
_logger.info(
- 'setActiveCurve(None) ignored due to active curve selection mode')
+ "setActiveCurve(None) ignored due to active curve selection mode"
+ )
return
- return self._setActiveItem(kind='curve', legend=legend)
+ return self._setActiveItem(kind="curve", item=legend)
def setActiveCurveSelectionMode(self, mode):
"""Sets the current selection mode.
@@ -2204,17 +2290,16 @@ class PlotWidget(qt.QMainWindow):
:param str mode: The active curve selection mode to use.
It can be: 'legacy', 'atmostone' or 'none'.
"""
- assert mode in ('legacy', 'atmostone', 'none')
+ assert mode in ("legacy", "atmostone", "none")
if mode != self._activeCurveSelectionMode:
self._activeCurveSelectionMode = mode
- if mode == 'none': # reset active curve
- self._setActiveItem(kind='curve', legend=None)
+ if mode == "none": # reset active curve
+ self._setActiveItem(kind="curve", item=None)
- elif mode == 'legacy' and self.getActiveCurve() is None:
+ elif mode == "legacy" and self.getActiveCurve() is None:
# Select an active curve
- curves = self.getAllCurves(just_legend=False,
- withhidden=False)
+ curves = self.getAllCurves(just_legend=False, withhidden=False)
if len(curves) == 1:
if curves[0].isVisible():
self.setActiveCurve(curves[0].getName())
@@ -2240,7 +2325,7 @@ class PlotWidget(qt.QMainWindow):
:rtype: str, :class:`.items.ImageData`, :class:`.items.ImageRgba`
or None
"""
- return self._getActiveItem(kind='image', just_legend=just_legend)
+ return self._getActiveItem(kind="image", just_legend=just_legend)
def setActiveImage(self, legend):
"""Make the image associated to legend the active image.
@@ -2248,7 +2333,7 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend associated to the image
or None to have no active image.
"""
- return self._setActiveItem(kind='image', legend=legend)
+ return self._setActiveItem(kind="image", item=legend)
def getActiveScatter(self, just_legend=False):
"""Returns the currently active scatter.
@@ -2261,7 +2346,7 @@ class PlotWidget(qt.QMainWindow):
:return: Active scatter's legend or corresponding scatter object
:rtype: str, :class:`.items.Scatter` or None
"""
- return self._getActiveItem(kind='scatter', just_legend=just_legend)
+ return self._getActiveItem(kind="scatter", just_legend=just_legend)
def setActiveScatter(self, legend):
"""Make the scatter associated to legend the active scatter.
@@ -2269,78 +2354,79 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend associated to the scatter
or None to have no active scatter.
"""
- return self._setActiveItem(kind='scatter', legend=legend)
+ return self._setActiveItem(kind="scatter", item=legend)
- def _getActiveItem(self, kind, just_legend=False):
- """Return the currently active item of that kind if any
+ def _getActiveItem(
+ self,
+ kind: str | None,
+ just_legend: bool = False,
+ ) -> items.Curve | items.Scatter | items.ImageBase | None:
+ """Return the currently active item of given 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
+ :param kind: Type of item: 'curve', 'scatter' or 'image'
+ :param just_legend:
+ True to get the item's legend, False (the default) to get the item
"""
assert kind in self._ACTIVE_ITEM_KINDS
+ item = self.__activeItems[kind]
+ if item is not None and just_legend:
+ return item.getName()
+ return item
- if self._activeLegend[kind] is None:
- return None
+ def _setActiveItem(
+ self,
+ kind: str,
+ item: items.Curve | items.ImageBase | items.Scatter | str | None,
+ ) -> str | None:
+ """Make the given item active.
+
+ Note: There is one active item per "kind" of item.
+ """
+ assert kind in self._ACTIVE_ITEM_KINDS
- item = self._getItem(kind, self._activeLegend[kind])
if item is None:
- return None
+ legend = None
+ elif isinstance(item, items.Item):
+ legend = item.getName()
+ else:
+ legend = str(item)
+ item = self._getItem(kind, legend)
+ if item is None:
+ _logger.warning("This %s does not exist: %s", kind, legend)
- return item.getName() if just_legend else item
+ oldActiveItem = self._getActiveItem(kind=kind)
- def _setActiveItem(self, kind, legend):
- """Make the curve associated to legend the active curve.
+ if oldActiveItem is None and item is None:
+ return None
- :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 self._ACTIVE_ITEM_KINDS
+ if oldActiveItem is not None:
+ # Stop listening previous active item
+ oldActiveItem.sigItemChanged.disconnect(self._activeItemChanged)
+ # Curve specific: Reset highlight of previous active curve
+ if kind == "curve":
+ oldActiveItem.setHighlighted(False)
+
+ self.__activeItems[kind] = item
xLabel = None
yLabel = None
yRightLabel = None
- oldActiveItem = self._getActiveItem(kind=kind)
+ if item is not None:
+ # Curve specific: handle highlight
+ if kind == "curve":
+ item.setHighlightedStyle(self.getActiveCurveStyle())
+ item.setHighlighted(True)
- if oldActiveItem is not None: # Stop listening previous active image
- oldActiveItem.sigItemChanged.disconnect(self._activeItemChanged)
+ if isinstance(item, items.LabelsMixIn):
+ xLabel = item.getXLabel()
+ if isinstance(item, items.YAxisMixIn) and item.getYAxis() == "right":
+ yRightLabel = item.getYLabel()
+ else:
+ yLabel = item.getYLabel()
- # 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.setHighlightedStyle(self.getActiveCurveStyle())
- 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()
-
- # Start listening new active item
- item.sigItemChanged.connect(self._activeItemChanged)
+ # Start listening new active item
+ item.sigItemChanged.connect(self._activeItemChanged)
# Store current labels and update plot
self._xAxis._setCurrentLabel(xLabel)
@@ -2349,19 +2435,13 @@ class PlotWidget(qt.QMainWindow):
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.getName()
- self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
- updated=oldActiveLegend != activeLegend,
- previous=oldActiveLegend,
- legend=activeLegend)
-
- return activeLegend
+ self.notify(
+ f"active{kind.capitalize()}Changed",
+ updated=oldActiveItem is not item,
+ previous=None if oldActiveItem is None else oldActiveItem.getName(),
+ legend=legend,
+ )
+ return legend
def _activeItemChanged(self, type_):
"""Listen for active item changed signal and broadcast signal
@@ -2373,10 +2453,11 @@ class PlotWidget(qt.QMainWindow):
if item is not None:
kind = self._itemKind(item)
self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
+ "active" + kind[0].upper() + kind[1:] + "Changed",
updated=False,
previous=item.getName(),
- legend=item.getName())
+ legend=item.getName(),
+ )
# Getters
@@ -2396,24 +2477,29 @@ class PlotWidget(qt.QMainWindow):
:return: list of curves' legend or :class:`.items.Curve`
:rtype: list of str or list of :class:`.items.Curve`
"""
- curves = [item for item in self.getItems() if
- isinstance(item, items.Curve) and
- (withhidden or item.isVisible())]
+ curves = [
+ item
+ for item in self.getItems()
+ if isinstance(item, items.Curve) and (withhidden or item.isVisible())
+ ]
return [curve.getName() for curve in curves] if just_legend else curves
- def getCurve(self, legend=None):
+ def getCurve(self, legend: str | items.Curve | None = None) -> items.Curve:
"""Get the object describing a specific curve.
It returns None in case no matching curve is found.
- :param str legend:
+ :param 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)
+ if isinstance(legend, items.Curve):
+ _logger.warning("getCurve call not needed: legend is already an item")
+ return legend
+ return self._getItem(kind="curve", legend=legend)
def getAllImages(self, just_legend=False):
"""Returns all images legend or objects.
@@ -2430,83 +2516,62 @@ class PlotWidget(qt.QMainWindow):
:return: list of images' legend or :class:`.items.ImageBase`
:rtype: list of str or list of :class:`.items.ImageBase`
"""
- images = [item for item in self.getItems()
- if isinstance(item, items.ImageBase)]
+ images = [item for item in self.getItems() if isinstance(item, items.ImageBase)]
return [image.getName() for image in images] if just_legend else images
- def getImage(self, legend=None):
+ def getImage(self, legend: str | items.ImageBase | None = None) -> items.ImageBase:
"""Get the object describing a specific image.
It returns None in case no matching image is found.
- :param str legend:
+ :param 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)
+ if isinstance(legend, items.ImageBase):
+ _logger.warning("getImage call not needed: legend is already an item")
+ return legend
+ return self._getItem(kind="image", legend=legend)
- def getScatter(self, legend=None):
+ def getScatter(self, legend: str | items.Scatter | None = None) -> items.Scatter:
"""Get the object describing a specific scatter.
It returns None in case no matching scatter is found.
- :param str legend:
+ :param 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)
+ if isinstance(legend, items.Scatter):
+ _logger.warning("getScatter call not needed: legend is already an item")
+ return legend
+ return self._getItem(kind="scatter", legend=legend)
- def getHistogram(self, legend=None):
+ def getHistogram(
+ self, legend: str | items.Histogram | None = None
+ ) -> items.Histogram:
"""Get the object describing a specific histogram.
It returns None in case no matching histogram is found.
- :param str legend:
+ :param 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)
-
- @deprecated(replacement='getItems', since_version='0.13')
- def _getItems(self, kind=ITEM_KINDS, just_legend=False, withhidden=False):
- """Retrieve all items of a kind in the plot
-
- :param kind: The kind of elements to retrieve from the plot.
- See :attr:`ITEM_KINDS`.
- By default, it removes all kind of elements.
- :type kind: str or tuple of str to specify multiple kinds.
- :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
- """
- if kind == '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
-
- output = []
- for item in self.getItems():
- type_ = self._itemKind(item)
- if type_ in kind and (withhidden or item.isVisible()):
- output.append(item.getName() if just_legend else item)
- return output
+ if isinstance(legend, items.Histogram):
+ _logger.warning("getHistogram call not needed: legend is already an item")
+ return legend
+ return self._getItem(kind="histogram", legend=legend)
- def _getItem(self, kind, legend=None):
+ def _getItem(self, kind, legend=None) -> items.Item:
"""Get an item from the plot: either an image or a curve.
Returns None if no match found.
@@ -2517,20 +2582,30 @@ class PlotWidget(qt.QMainWindow):
None to get active or last item
:return: Object describing the item or None
"""
+ if isinstance(legend, items.Item):
+ _logger.warning("_getItem call not needed: legend is already an item")
+ return legend
+
assert kind in self.ITEM_KINDS
if legend is not None:
- return self._content.get((legend, kind), None)
- else:
- if kind in self._ACTIVE_ITEM_KINDS:
- item = self._getActiveItem(kind=kind)
- if item is not None: # Return active item if available
+ for item in self.getItems():
+ if item.getName() == legend and kind == self._itemKind(item):
return item
- # Return last visible item if any
- itemClasses = self._KIND_TO_CLASSES[kind]
- allItems = [item for item in self.getItems()
- if isinstance(item, itemClasses) and item.isVisible()]
- return allItems[-1] if allItems else None
+ return None # No item found
+
+ if kind in self._ACTIVE_ITEM_KINDS:
+ item = self._getActiveItem(kind=kind)
+ if item is not None: # Return active item if available
+ return item
+ # Return last visible item if any
+ itemClasses = self._KIND_TO_CLASSES[kind]
+ allItems = [
+ item
+ for item in self.getItems()
+ if isinstance(item, itemClasses) and item.isVisible()
+ ]
+ return allItems[-1] if allItems else None
# Limits
@@ -2545,7 +2620,8 @@ class PlotWidget(qt.QMainWindow):
for axis, limits in zip(axes, ranges):
axis.sigLimitsChanged.emit(*limits)
event = PlotEvents.prepareLimitsChangedSignal(
- id(self.getWidgetHandle()), xRange, yRange, y2Range)
+ id(self.getWidgetHandle()), xRange, yRange, y2Range
+ )
self.notify(**event)
def getLimitsHistory(self):
@@ -2567,18 +2643,18 @@ class PlotWidget(qt.QMainWindow):
"""
self._xAxis.setLimits(xmin, xmax)
- def getGraphYLimits(self, axis='left'):
+ 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')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ assert axis in ("left", "right")
+ yAxis = self._yAxis if axis == "left" else self._yRightAxis
return yAxis.getLimits()
- def setGraphYLimits(self, ymin, ymax, axis='left'):
+ def setGraphYLimits(self, ymin, ymax, axis="left"):
"""Set the graph Y limits.
:param float ymin: minimum bottom axis value
@@ -2586,40 +2662,80 @@ class PlotWidget(qt.QMainWindow):
:param str axis: The axis for which to get the limits:
Either 'left' or 'right'
"""
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ assert axis in ("left", "right")
+ yAxis = self._yAxis if axis == "left" else self._yRightAxis
return yAxis.setLimits(ymin, ymax)
- def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
+ def setLimits(
+ self,
+ xmin: float,
+ xmax: float,
+ ymin: float,
+ ymax: float,
+ y2min: Optional[float] = None,
+ y2max: Optional[float] = None,
+ margins: Union[bool, tuple[float, float, float, float]] = False,
+ ):
"""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
- axis = self.getXAxis()
- xmin, xmax = axis._checkLimits(xmin, xmax)
- axis = self.getYAxis()
- ymin, ymax = axis._checkLimits(ymin, ymax)
-
- if y2min is None or y2max is None:
- # if one limit is None, both are ignored
- y2min, y2max = None, None
- else:
- axis = self.getYAxis(axis="right")
- y2min, y2max = axis._checkLimits(y2min, y2max)
+ :param xmin: minimum bottom axis value
+ :param xmax: maximum bottom axis value
+ :param ymin: minimum left axis value
+ :param ymax: maximum left axis value
+ :param y2min: minimum right axis value or None (the default)
+ :param y2max: maximum right axis value or None (the default)
+ :param margins:
+ Data margins to add to the limits or a boolean telling
+ whether or not to add margins from :meth:`getDataMargins`.
+ """
+ limits = [
+ *self.getXAxis()._checkLimits(xmin, xmax),
+ *self.getYAxis()._checkLimits(ymin, ymax),
+ ]
+
+ # Only consider y2 axis if both limits are not None
+ if None not in (y2min, y2max):
+ limits.extend(self.getYAxis(axis="right")._checkLimits(y2min, y2max))
+
+ if margins: # Add margins around limits inside the plot area
+ limits = list(
+ _utils.addMarginsToLimits(
+ self.getDataMargins() if margins is True else margins,
+ self.getXAxis()._isLogarithmic(),
+ self.getYAxis()._isLogarithmic(),
+ *limits,
+ )
+ )
+
+ if self.isKeepDataAspectRatio():
+ # Use limits with margins to keep ratio
+ xmin, xmax, ymin, ymax = limits[:4]
+
+ # Compute bbox wth figure aspect ratio
+ plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ if plotWidth > 0 and plotHeight > 0:
+ plotRatio = plotHeight / plotWidth
+ dataRatio = (ymax - ymin) / (xmax - xmin)
+ if dataRatio < plotRatio:
+ # Increase y range
+ ycenter = 0.5 * (ymax + ymin)
+ yrange = (xmax - xmin) * plotRatio
+ limits[2] = ycenter - 0.5 * yrange
+ limits[3] = ycenter + 0.5 * yrange
+
+ elif dataRatio > plotRatio:
+ # Increase x range
+ xcenter = 0.5 * (xmax + xmin)
+ xrange_ = (ymax - ymin) / plotRatio
+ limits[0] = xcenter - 0.5 * xrange_
+ limits[1] = xcenter + 0.5 * xrange_
if self._viewConstrains:
- view = self._viewConstrains.normalize(xmin, xmax, ymin, ymax)
- xmin, xmax, ymin, ymax = view
+ limits[:4] = self._viewConstrains.normalize(*limits[:4])
- self._backend.setLimits(xmin, xmax, ymin, ymax, y2min, y2max)
+ self._backend.setLimits(*limits)
self._setDirtyPlot()
self._notifyLimitsChanged()
@@ -2661,16 +2777,16 @@ class PlotWidget(qt.QMainWindow):
"""
self._xAxis.setLabel(label)
- def getGraphYLabel(self, axis='left'):
+ 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')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ assert axis in ("left", "right")
+ yAxis = self._yAxis if axis == "left" else self._yRightAxis
return yAxis.getLabel()
- def setGraphYLabel(self, label="Y", axis='left'):
+ 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
@@ -2679,8 +2795,8 @@ class PlotWidget(qt.QMainWindow):
: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')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ assert axis in ("left", "right")
+ yAxis = self._yAxis if axis == "left" else self._yRightAxis
return yAxis.setLabel(label)
# Axes
@@ -2703,7 +2819,7 @@ class PlotWidget(qt.QMainWindow):
('left' or 'right').
:rtype: :class:`.items.Axis`
"""
- assert(axis in ["left", "right"])
+ assert axis in ["left", "right"]
return self._yAxis if axis == "left" else self._yRightAxis
def setAxesDisplayed(self, displayed: bool):
@@ -2717,7 +2833,7 @@ class PlotWidget(qt.QMainWindow):
if displayed:
self._backend.setAxesMargins(*self.__axesMargins)
else:
- self._backend.setAxesMargins(0., 0., 0., 0.)
+ self._backend.setAxesMargins(0.0, 0.0, 0.0, 0.0)
self._setDirtyPlot()
self._sigAxesVisibilityChanged.emit(displayed)
@@ -2728,8 +2844,7 @@ class PlotWidget(qt.QMainWindow):
"""
return self.__axesDisplayed
- def setAxesMargins(
- self, left: float, top: float, right: float, bottom: float):
+ def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
"""Set ratios of margins surrounding data plot area.
All ratios must be within [0., 1.].
@@ -2742,9 +2857,9 @@ class PlotWidget(qt.QMainWindow):
:raises ValueError:
"""
for value in (left, top, right, bottom):
- if value < 0. or value > 1.:
+ if value < 0.0 or value > 1.0:
raise ValueError("Margin ratios must be within [0., 1.]")
- if left + right >= 1. or top + bottom >= 1.:
+ if left + right >= 1.0 or top + bottom >= 1.0:
raise ValueError("Sum of ratios of opposed sides >= 1")
margins = left, top, right, bottom
@@ -2835,7 +2950,7 @@ class PlotWidget(qt.QMainWindow):
self._backend.setKeepDataAspectRatio(flag=flag)
self._setDirtyPlot()
self._forceResetZoom()
- self.notify('setKeepDataAspectRatio', state=flag)
+ self.notify("setKeepDataAspectRatio", state=flag)
def getGraphGrid(self):
"""Return the current grid mode, either None, 'major' or 'both'.
@@ -2852,15 +2967,15 @@ class PlotWidget(qt.QMainWindow):
'both' for grid on both major and minor ticks.
:type which: str of bool
"""
- assert which in (None, True, False, 'both', 'major')
+ assert which in (None, True, False, "both", "major")
if not which:
which = None
elif which is True:
- which = 'major'
+ which = "major"
self._grid = which
self._backend.setGraphGrid(which)
self._setDirtyPlot()
- self.notify('setGraphGrid', which=str(which))
+ self.notify("setGraphGrid", which=str(which))
# Defaults
@@ -2876,7 +2991,7 @@ class PlotWidget(qt.QMainWindow):
:param bool flag: True to use 'o' as the default curve symbol,
False to use no symbol.
"""
- self._defaultPlotPoints = silx.config.DEFAULT_PLOT_SYMBOL if flag else ''
+ self._defaultPlotPoints = silx.config.DEFAULT_PLOT_SYMBOL if flag else ""
# Reset symbol of all curves
curves = self.getAllCurves(just_legend=False, withhidden=True)
@@ -2897,7 +3012,7 @@ class PlotWidget(qt.QMainWindow):
"""
self._plotLines = bool(flag)
- linestyle = '-' if self._plotLines else ' '
+ linestyle = "-" if self._plotLines else " "
# Reset linestyle of all curves
curves = self.getAllCurves(withhidden=True)
@@ -2927,16 +3042,18 @@ class PlotWidget(qt.QMainWindow):
autoscale gray colormap.
"""
if colormap is None:
- colormap = Colormap(name=silx.config.DEFAULT_COLORMAP_NAME,
- normalization='linear',
- vmin=None,
- vmax=None)
+ colormap = Colormap(
+ name=silx.config.DEFAULT_COLORMAP_NAME,
+ normalization="linear",
+ vmin=None,
+ vmax=None,
+ )
if isinstance(colormap, dict):
self._defaultColormap = Colormap._fromDict(colormap)
else:
assert isinstance(colormap, Colormap)
self._defaultColormap = colormap
- self.notify('defaultColormapChanged')
+ self.notify("defaultColormapChanged")
@staticmethod
def getSupportedColormaps():
@@ -2948,17 +3065,35 @@ class PlotWidget(qt.QMainWindow):
"""
return Colormap.getSupportedColormaps()
+ def setDefaultColors(self, colors: Optional[Tuple[str, ...]]):
+ """Set the list of colors to use as default for curves and histograms.
+
+ Set to None to use `silx.config.DEFAULT_PLOT_CURVE_COLORS`.
+ """
+ self._defaultColors = None if colors is None else tuple(colors)
+ self._resetColorAndStyle()
+
+ def getDefaultColors(self) -> Tuple[str, ...]:
+ """Returns the list of default colors for curves and histograms"""
+ if self._defaultColors is None:
+ return tuple(silx.config.DEFAULT_PLOT_CURVE_COLORS)
+ return self._defaultColors
+
def _resetColorAndStyle(self):
self._colorIndex = 0
self._styleIndex = 0
- def _getColorAndStyle(self):
- color = self.colorList[self._colorIndex]
+ def _getColorAndStyle(self) -> Tuple[str, str]:
+ defaultColors = self.getDefaultColors()
+ if self._colorIndex >= len(defaultColors): # Handle list length updated
+ self._colorIndex = 0
+
+ color = defaultColors[self._colorIndex]
style = self._styleList[self._styleIndex]
# Loop over color and then styles
self._colorIndex += 1
- if self._colorIndex >= len(self.colorList):
+ if self._colorIndex >= len(defaultColors):
self._colorIndex = 0
self._styleIndex = (self._styleIndex + 1) % len(self._styleList)
@@ -2967,7 +3102,7 @@ class PlotWidget(qt.QMainWindow):
color, style = self._getColorAndStyle()
if not self._plotLines:
- style = ' '
+ style = " "
return color, style
@@ -2990,32 +3125,30 @@ class PlotWidget(qt.QMainWindow):
:param kwargs: The information of the event.
"""
eventDict = kwargs.copy()
- eventDict['event'] = event
+ eventDict["event"] = event
self.sigPlotSignal.emit(eventDict)
- if event == 'setKeepDataAspectRatio':
- self.sigSetKeepDataAspectRatio.emit(kwargs['state'])
- elif event == 'setGraphGrid':
- self.sigSetGraphGrid.emit(kwargs['which'])
- elif event == 'setGraphCursor':
- self.sigSetGraphCursor.emit(kwargs['state'])
- elif event == 'contentChanged':
+ if event == "setKeepDataAspectRatio":
+ self.sigSetKeepDataAspectRatio.emit(kwargs["state"])
+ elif event == "setGraphGrid":
+ self.sigSetGraphGrid.emit(kwargs["which"])
+ elif event == "setGraphCursor":
+ self.sigSetGraphCursor.emit(kwargs["state"])
+ elif event == "contentChanged":
self.sigContentChanged.emit(
- kwargs['action'], kwargs['kind'], kwargs['legend'])
- elif event == 'activeCurveChanged':
- self.sigActiveCurveChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'activeImageChanged':
- self.sigActiveImageChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'activeScatterChanged':
- self.sigActiveScatterChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'interactiveModeChanged':
- self.sigInteractiveModeChanged.emit(kwargs['source'])
+ kwargs["action"], kwargs["kind"], kwargs["legend"]
+ )
+ elif event == "activeCurveChanged":
+ self.sigActiveCurveChanged.emit(kwargs["previous"], kwargs["legend"])
+ elif event == "activeImageChanged":
+ self.sigActiveImageChanged.emit(kwargs["previous"], kwargs["legend"])
+ elif event == "activeScatterChanged":
+ self.sigActiveScatterChanged.emit(kwargs["previous"], kwargs["legend"])
+ elif event == "interactiveModeChanged":
+ self.sigInteractiveModeChanged.emit(kwargs["source"])
eventDict = kwargs.copy()
- eventDict['event'] = event
+ eventDict["event"] = event
self._callback(eventDict)
def setCallback(self, callbackFunction=None):
@@ -3045,11 +3178,11 @@ class PlotWidget(qt.QMainWindow):
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'])
- qt.QToolTip.showText(self.cursor().pos(), ddict['label'])
- elif ddict['event'] == 'mouseClicked' and ddict['button'] == 'left':
+ if ddict["event"] == "curveClicked":
+ if ddict["button"] == "left":
+ self.setActiveCurve(ddict["item"])
+ qt.QToolTip.showText(self.cursor().pos(), ddict["label"])
+ elif ddict["event"] == "mouseClicked" and ddict["button"] == "left":
self.setActiveCurve(None)
def saveGraph(self, filename, fileFormat=None, dpi=None):
@@ -3066,42 +3199,51 @@ class PlotWidget(qt.QMainWindow):
:return: False if cannot save the plot, True otherwise
"""
if fileFormat is None:
- if not hasattr(filename, 'lower'):
- _logger.warning(
- 'saveGraph cancelled, cannot define file format.')
+ 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")
+ supportedFormats = (
+ "png",
+ "svg",
+ "pdf",
+ "ps",
+ "eps",
+ "tif",
+ "tiff",
+ "jpeg",
+ "jpg",
+ )
if fileFormat not in supportedFormats:
- _logger.warning('Unsupported format %s', fileFormat)
+ _logger.warning("Unsupported format %s", fileFormat)
return False
else:
- self._backend.saveGraph(filename,
- fileFormat=fileFormat,
- dpi=dpi)
+ self._backend.saveGraph(filename, fileFormat=fileFormat, dpi=dpi)
return True
- def getDataMargins(self):
+ def getDataMargins(self) -> tuple[float, float, float, float]:
"""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.):
+ def setDataMargins(
+ self,
+ xMinMargin: float = 0.0,
+ xMaxMargin: float = 0.0,
+ yMinMargin: float = 0.0,
+ yMaxMargin: float = 0.0,
+ ):
"""Set the default data margins to use in :meth:`resetZoom`.
- Set the default ratios of margins (as floats) to add around the data
+ Set the default ratios of margins to add around the data
inside the plot area for each side.
"""
- self._defaultDataMargins = (xMinMargin, xMaxMargin,
- yMinMargin, yMaxMargin)
+ self._defaultDataMargins = (xMinMargin, xMaxMargin, yMinMargin, yMaxMargin)
def getAutoReplot(self):
"""Return True if replot is automatically handled, False otherwise.
@@ -3133,10 +3275,10 @@ class PlotWidget(qt.QMainWindow):
It is in charge of performing required PlotWidget operations
"""
- for item in self._contentToUpdate:
+ for item in self.__itemsToUpdate:
item._update(self._backend)
- self._contentToUpdate = []
+ self.__itemsToUpdate = []
yield
self._dirty = False # reset dirty flag
@@ -3144,7 +3286,10 @@ class PlotWidget(qt.QMainWindow):
"""Request to draw the plot."""
self._backend.replot()
- def _forceResetZoom(self, dataMargins=None):
+ def _forceResetZoom(
+ self,
+ dataMargins: Optional[tuple[float, float, float, float]] = None,
+ ):
"""Reset the plot limits to the bounds of the data and redraw the plot.
This method forces a reset zoom and does not check axis autoscale.
@@ -3155,55 +3300,30 @@ class PlotWidget(qt.QMainWindow):
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).
+ :param dataMargins:
+ Ratios of margins to add around the data inside the plot area for each side.
+ If None (the default), use margins from :meth:`getDataMargins`.
"""
- if dataMargins is None:
- dataMargins = self._defaultDataMargins
-
# 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
+ xmin, xmax = (1.0, 100.0) if ranges.x is None else ranges.x
+ ymin, ymax = (1.0, 100.0) if ranges.y is None else ranges.y
if ranges.yright is None:
- ymin2, ymax2 = ymin, ymax
+ y2min, y2max = ymin, ymax
else:
- ymin2, ymax2 = ranges.yright
+ y2min, y2max = ranges.yright
if ranges.y is None:
ymin, ymax = ranges.yright
- # Add margins around data inside the plot area
- newLimits = list(_utils.addMarginsToLimits(
- dataMargins,
- self._xAxis._isLogarithmic(),
- self._yAxis._isLogarithmic(),
- 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:]
- if plotWidth > 0 and plotHeight > 0:
- plotRatio = plotHeight / plotWidth
- 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)
+ self.setLimits(
+ xmin,
+ xmax,
+ ymin,
+ ymax,
+ y2min,
+ y2max,
+ margins=dataMargins if dataMargins is not None else True,
+ )
def resetZoom(self, dataMargins=None):
"""Reset the plot limits to the bounds of the data and redraw the plot.
@@ -3233,7 +3353,9 @@ class PlotWidget(qt.QMainWindow):
# This avoids issues with toggling log scale with matplotlib 2.1.0
if self._xAxis.getScale() == self._xAxis.LOGARITHMIC and xLimits[0] <= 0:
xAuto = True
- if self._yAxis.getScale() == self._yAxis.LOGARITHMIC and (yLimits[0] <= 0 or y2Limits[0] <= 0):
+ if self._yAxis.getScale() == self._yAxis.LOGARITHMIC and (
+ yLimits[0] <= 0 or y2Limits[0] <= 0
+ ):
yAuto = True
if not xAuto and not yAuto:
@@ -3246,14 +3368,15 @@ class PlotWidget(qt.QMainWindow):
self.setGraphXLimits(*xLimits)
elif xAuto and not yAuto:
if y2Limits is not None:
- self.setGraphYLimits(
- y2Limits[0], y2Limits[1], axis='right')
+ self.setGraphYLimits(y2Limits[0], y2Limits[1], axis="right")
if yLimits is not None:
- self.setGraphYLimits(yLimits[0], yLimits[1], axis='left')
+ self.setGraphYLimits(yLimits[0], yLimits[1], axis="left")
- if (xLimits != self._xAxis.getLimits() or
- yLimits != self._yAxis.getLimits() or
- y2Limits != self._yRightAxis.getLimits()):
+ if (
+ xLimits != self._xAxis.getLimits()
+ or yLimits != self._yAxis.getLimits()
+ or y2Limits != self._yRightAxis.getLimits()
+ ):
self._notifyLimitsChanged()
# Coord conversion
@@ -3261,10 +3384,13 @@ class PlotWidget(qt.QMainWindow):
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 x: The X coordinate in data space. If None (default)
+ the middle position of the displayed data is used.
+ :type x: float or 1D numpy array of float
+ :param y: The Y coordinate in data space. If None (default)
+ the middle position of the displayed data is used.
+ :type y: float or 1D numpy array of float
+
: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,
@@ -3272,7 +3398,7 @@ class PlotWidget(qt.QMainWindow):
: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.
+ :rtype: A tuple of 2 floats or 2 arrays of float: (xPixel, yPixel) or None.
"""
assert axis in ("left", "right")
@@ -3285,12 +3411,26 @@ class PlotWidget(qt.QMainWindow):
if y is None:
y = 0.5 * (ymax + ymin)
+ if isinstance(x, numbers.Real) != isinstance(y, numbers.Real):
+ raise ValueError("x and y must be of the same type")
+ if not isinstance(x, numbers.Real) and (x.shape != y.shape or x.ndim != 1):
+ raise ValueError("x and y must be 1D arrays of the same length")
+
if check:
- if x > xmax or x < xmin:
- return None
+ isOutside = numpy.logical_or(
+ numpy.logical_or(x > xmax, x < xmin),
+ numpy.logical_or(y > ymax, y < ymin),
+ )
- if y > ymax or y < ymin:
- return None
+ if numpy.any(isOutside):
+ if isinstance(x, numbers.Real):
+ return None
+ else: # Filter-out points that are outside
+ x = numpy.array(x, copy=True, dtype=numpy.float64)
+ x[isOutside] = numpy.nan
+
+ y = numpy.array(y, copy=True, dtype=numpy.float64)
+ y[isOutside] = numpy.nan
return self._backend.dataToPixel(x, y, axis=axis)
@@ -3318,7 +3458,11 @@ class PlotWidget(qt.QMainWindow):
if check:
left, top, width, height = self.getPlotBoundsInPixels()
- if not (left <= x <= left + width and top <= y <= top + height):
+ isOutside = numpy.logical_or(
+ numpy.logical_or(x < left, x > left + width),
+ numpy.logical_or(y < top, y > top + height),
+ )
+ if numpy.any(isOutside):
return None
return self._backend.pixelToData(x, y, axis)
@@ -3347,14 +3491,6 @@ class PlotWidget(qt.QMainWindow):
self.__graphCursorShape = cursor
self._backend.setGraphCursorShape(cursor)
- @deprecated(replacement='getItems', since_version='0.13')
- def _getAllMarkers(self, just_legend=False):
- markers = [item for item in self.getItems() if isinstance(item, items.MarkerBase)]
- if just_legend:
- return [marker.getName() for marker in markers]
- else:
- return markers
-
def _getMarkerAt(self, x, y):
"""Return the most interactive marker at a location, else None
@@ -3362,10 +3498,13 @@ class PlotWidget(qt.QMainWindow):
:param float y: Y position in pixels
:rtype: None of marker object
"""
+
def checkDraggable(item):
return isinstance(item, items.MarkerBase) and item.isDraggable()
+
def checkSelectable(item):
return isinstance(item, items.MarkerBase) and item.isSelectable()
+
def check(item):
return isinstance(item, items.MarkerBase)
@@ -3385,7 +3524,7 @@ class PlotWidget(qt.QMainWindow):
:param str legend: The legend of the marker to retrieve
:rtype: None of marker object
"""
- return self._getItem(kind='marker', legend=legend)
+ return self._getItem(kind="marker", legend=legend)
def pickItems(self, x, y, condition=None):
"""Generator of picked items in the plot at given position.
@@ -3400,7 +3539,9 @@ class PlotWidget(qt.QMainWindow):
:return: Iterable of :class:`PickingResult` objects at picked position.
Items are ordered from front to back.
"""
- for item in reversed(self._backend.getItemsFromBackToFront(condition=condition)):
+ for item in reversed(
+ self._backend.getItemsFromBackToFront(condition=condition)
+ ):
result = item.pick(x, y)
if result is not None:
yield result
@@ -3446,7 +3587,7 @@ class PlotWidget(qt.QMainWindow):
"""
if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
self._pressedButtons.append(btn)
- self._eventHandler.handleEvent('press', xPixel, yPixel, btn)
+ self._eventHandler.handleEvent("press", xPixel, yPixel, btn)
def onMouseMove(self, xPixel, yPixel):
"""Handle mouse move event.
@@ -3459,8 +3600,7 @@ class PlotWidget(qt.QMainWindow):
if self._cursorInPlot != isCursorInPlot:
self._cursorInPlot = isCursorInPlot
- self._eventHandler.handleEvent(
- 'enter' if self._cursorInPlot else 'leave')
+ self._eventHandler.handleEvent("enter" if self._cursorInPlot else "leave")
if isCursorInPlot:
# Signal mouse move event
@@ -3469,12 +3609,13 @@ class PlotWidget(qt.QMainWindow):
btn = self._pressedButtons[-1] if self._pressedButtons else None
event = PlotEvents.prepareMouseSignal(
- 'mouseMoved', btn, dataPos[0], dataPos[1], xPixel, yPixel)
+ "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)
+ self._eventHandler.handleEvent("move", inXPixel, inYPixel)
def onMouseRelease(self, xPixel, yPixel, btn):
"""Handle mouse release event.
@@ -3489,7 +3630,7 @@ class PlotWidget(qt.QMainWindow):
pass
else:
xPixel, yPixel = self._isPositionInPlotArea(xPixel, yPixel)
- self._eventHandler.handleEvent('release', xPixel, yPixel, btn)
+ self._eventHandler.handleEvent("release", xPixel, yPixel, btn)
def onMouseWheel(self, xPixel, yPixel, angleInDegrees):
"""Handle mouse wheel event.
@@ -3501,17 +3642,25 @@ class PlotWidget(qt.QMainWindow):
negative for movement toward the user.
"""
if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
- self._eventHandler.handleEvent(
- 'wheel', xPixel, yPixel, angleInDegrees)
+ 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')
+ self._eventHandler.handleEvent("leave")
# Interaction modes #
+ def interaction(self) -> PlotInteraction:
+ """Returns the interaction handler for this PlotWidget"""
+ return self._eventHandler
+
+ def __interactionChanged(self):
+ """Handle PlotInteraction updates"""
+ if self.__isInteractionSignalForwarded:
+ self.sigInteractiveModeChanged.emit(None)
+
def getInteractiveMode(self):
"""Returns the current interactive mode as a dict.
@@ -3520,7 +3669,7 @@ class PlotWidget(qt.QMainWindow):
It can also contains extra keys (e.g., 'color') specific to a mode
as provided to :meth:`setInteractiveMode`.
"""
- return self._eventHandler.getInteractiveMode()
+ return self.interaction()._getInteractiveMode()
def resetInteractiveMode(self):
"""Reset the interactive mode to use the previous basic interactive
@@ -3531,36 +3680,47 @@ class PlotWidget(qt.QMainWindow):
mode, zoomOnWheel = self._previousDefaultMode
self.setInteractiveMode(mode=mode, zoomOnWheel=zoomOnWheel)
- def setInteractiveMode(self, mode, color='black',
- shape='polygon', label=None,
- zoomOnWheel=True, source=None, width=None):
+ def setInteractiveMode(
+ self,
+ mode: str,
+ color: Union[str, Sequence[numbers.Real]] = "black",
+ shape: str = "polygon",
+ label: Optional[str] = None,
+ zoomOnWheel: bool = True,
+ source=None,
+ width: Optional[float] = None,
+ ):
"""Switch the interactive mode.
- :param str mode: The name of the interactive mode.
- In 'draw', 'pan', 'select', 'select-draw', 'zoom'.
+ :param mode: The name of the interactive mode.
+ In 'draw', 'pan', 'select', 'select-draw', '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 shape: Only for 'draw' mode. The kind of shape to draw.
+ In 'polygon', 'rectangle', 'line', 'vline', 'hline',
+ 'freeline'.
+ Default is 'polygon'.
+ :param label: Only for 'draw' mode, sent in drawing events.
+ :param 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.
+ :param width: Width of the pencil. Only for draw pencil mode.
"""
- self._eventHandler.setInteractiveMode(mode, color, shape, label, width)
- self._eventHandler.zoomOnWheel = zoomOnWheel
+ self.__isInteractionSignalForwarded = False
+ try:
+ self._eventHandler._setInteractiveMode(mode, color, shape, label, width)
+ self._eventHandler.setZoomOnWheelEnabled(zoomOnWheel)
+ finally:
+ self.__isInteractionSignalForwarded = True
+
if mode in ["pan", "zoom"]:
self._previousDefaultMode = mode, zoomOnWheel
- self.notify(
- 'interactiveModeChanged', source=source)
+ self.notify("interactiveModeChanged", source=source)
# Panning with arrow keys
@@ -3593,20 +3753,21 @@ class PlotWidget(qt.QMainWindow):
# Dict to convert Qt arrow key code to direction str.
_ARROWS_TO_PAN_DIRECTION = {
- qt.Qt.Key_Left: 'left',
- qt.Qt.Key_Right: 'right',
- qt.Qt.Key_Up: 'up',
- qt.Qt.Key_Down: 'down'
+ qt.Qt.Key_Left: "left",
+ qt.Qt.Key_Right: "right",
+ qt.Qt.Key_Up: "up",
+ qt.Qt.Key_Down: "down",
}
def __simulateMouseMove(self):
qapp = qt.QApplication.instance()
event = qt.QMouseEvent(
qt.QEvent.MouseMove,
- self.getWidgetHandle().mapFromGlobal(qt.QCursor.pos()),
+ qt.QPointF(self.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())),
qt.Qt.NoButton,
qapp.mouseButtons(),
- qapp.keyboardModifiers())
+ qapp.keyboardModifiers(),
+ )
qapp.sendEvent(self.getWidgetHandle(), event)
def keyPressEvent(self, event):
diff --git a/src/silx/gui/plot/PlotWindow.py b/src/silx/gui/plot/PlotWindow.py
index 0349585..9aa8c78 100644
--- a/src/silx/gui/plot/PlotWindow.py
+++ b/src/silx/gui/plot/PlotWindow.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,16 +30,12 @@ __authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "12/04/2019"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import logging
import weakref
import silx
from silx.utils.weakref import WeakMethodProxy
-from silx.utils.deprecation import deprecated
from silx.utils.proxy import docstring
from . import PlotWidget
@@ -58,6 +53,7 @@ from .CurvesROIWidget import CurvesROIDockWidget
from .MaskToolsWidget import MaskToolsDockWidget
from .StatsWidget import BasicStatsWidget
from .ColorBar import ColorBarWidget
+
try:
from ..console import IPythonDockWidget
except ImportError:
@@ -104,16 +100,30 @@ class PlotWindow(PlotWidget):
:param bool fit: Toggle visibilty of fit action.
"""
- def __init__(self, parent=None, backend=None,
- resetzoom=True, autoScale=True, logScale=True, grid=True,
- curveStyle=True, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=False,
- roi=True, mask=True, fit=False):
+ def __init__(
+ self,
+ parent=None,
+ backend=None,
+ resetzoom=True,
+ autoScale=True,
+ logScale=True,
+ grid=True,
+ curveStyle=True,
+ colormap=True,
+ aspectRatio=True,
+ yInverted=True,
+ copy=True,
+ save=True,
+ print_=True,
+ control=False,
+ position=False,
+ roi=True,
+ mask=True,
+ fit=False,
+ ):
super(PlotWindow, self).__init__(parent=parent, backend=backend)
if parent is None:
- self.setWindowTitle('PlotWindow')
+ self.setWindowTitle("PlotWindow")
self._dockWidgets = []
@@ -132,63 +142,80 @@ class PlotWindow(PlotWidget):
self.group.setExclusive(False)
self.resetZoomAction = self.group.addAction(
- actions.control.ResetZoomAction(self, parent=self))
+ actions.control.ResetZoomAction(self, parent=self)
+ )
self.resetZoomAction.setVisible(resetzoom)
self.addAction(self.resetZoomAction)
- self.zoomInAction = actions.control.ZoomInAction(self, parent=self)
+ self.zoomInAction = self.group.addAction(
+ actions.control.ZoomInAction(self, parent=self)
+ )
+ self.zoomInAction.setVisible(False)
self.addAction(self.zoomInAction)
- self.zoomOutAction = actions.control.ZoomOutAction(self, parent=self)
+ self.zoomOutAction = self.group.addAction(
+ actions.control.ZoomOutAction(self, parent=self)
+ )
+ self.zoomOutAction.setVisible(False)
self.addAction(self.zoomOutAction)
self.xAxisAutoScaleAction = self.group.addAction(
- actions.control.XAxisAutoScaleAction(self, parent=self))
+ actions.control.XAxisAutoScaleAction(self, parent=self)
+ )
self.xAxisAutoScaleAction.setVisible(autoScale)
self.addAction(self.xAxisAutoScaleAction)
self.yAxisAutoScaleAction = self.group.addAction(
- actions.control.YAxisAutoScaleAction(self, parent=self))
+ actions.control.YAxisAutoScaleAction(self, parent=self)
+ )
self.yAxisAutoScaleAction.setVisible(autoScale)
self.addAction(self.yAxisAutoScaleAction)
self.xAxisLogarithmicAction = self.group.addAction(
- actions.control.XAxisLogarithmicAction(self, parent=self))
+ actions.control.XAxisLogarithmicAction(self, parent=self)
+ )
self.xAxisLogarithmicAction.setVisible(logScale)
self.addAction(self.xAxisLogarithmicAction)
self.yAxisLogarithmicAction = self.group.addAction(
- actions.control.YAxisLogarithmicAction(self, parent=self))
+ actions.control.YAxisLogarithmicAction(self, parent=self)
+ )
self.yAxisLogarithmicAction.setVisible(logScale)
self.addAction(self.yAxisLogarithmicAction)
self.gridAction = self.group.addAction(
- actions.control.GridAction(self, gridMode='both', parent=self))
+ actions.control.GridAction(self, gridMode="both", parent=self)
+ )
self.gridAction.setVisible(grid)
self.addAction(self.gridAction)
self.curveStyleAction = self.group.addAction(
- actions.control.CurveStyleAction(self, parent=self))
+ actions.control.CurveStyleAction(self, parent=self)
+ )
self.curveStyleAction.setVisible(curveStyle)
self.addAction(self.curveStyleAction)
self.colormapAction = self.group.addAction(
- actions.control.ColormapAction(self, parent=self))
+ actions.control.ColormapAction(self, parent=self)
+ )
self.colormapAction.setVisible(colormap)
self.addAction(self.colormapAction)
self.colorbarAction = self.group.addAction(
- actions_control.ColorBarAction(self, parent=self))
+ actions_control.ColorBarAction(self, parent=self)
+ )
self.colorbarAction.setVisible(False)
self.addAction(self.colorbarAction)
self._colorbar.setVisible(False)
self.keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=self)
+ parent=self, plot=self
+ )
self.keepDataAspectRatioButton.setVisible(aspectRatio)
self.yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton(
- parent=self, plot=self)
+ parent=self, plot=self
+ )
self.yAxisInvertedButton.setVisible(yInverted)
self.group.addAction(self.getRoiAction())
@@ -198,15 +225,18 @@ class PlotWindow(PlotWidget):
self.getMaskAction().setVisible(mask)
self._intensityHistoAction = self.group.addAction(
- actions_histogram.PixelIntensitiesHistoAction(self, parent=self))
+ actions_histogram.PixelIntensitiesHistoAction(self, parent=self)
+ )
self._intensityHistoAction.setVisible(False)
self._medianFilter2DAction = self.group.addAction(
- actions_medfilt.MedianFilter2DAction(self, parent=self))
+ actions_medfilt.MedianFilter2DAction(self, parent=self)
+ )
self._medianFilter2DAction.setVisible(False)
self._medianFilter1DAction = self.group.addAction(
- actions_medfilt.MedianFilter1DAction(self, parent=self))
+ actions_medfilt.MedianFilter1DAction(self, parent=self)
+ )
self._medianFilter1DAction.setVisible(False)
self.fitAction = self.group.addAction(actions_fit.FitAction(self, parent=self))
@@ -215,7 +245,6 @@ class PlotWindow(PlotWidget):
# lazy loaded actions needed by the controlButton menu
self._consoleAction = None
- self._statsAction = None
self._panWithArrowKeysAction = None
self._crosshairAction = None
@@ -241,24 +270,25 @@ class PlotWindow(PlotWidget):
converters = position
else:
converters = None
- self._positionWidget = tools.PositionInfo(
- plot=self, converters=converters)
+ self._positionWidget = tools.PositionInfo(plot=self, converters=converters)
# Set a snapping mode that is consistent with legacy one
self._positionWidget.setSnappingMode(
- tools.PositionInfo.SNAPPING_CROSSHAIR |
- tools.PositionInfo.SNAPPING_ACTIVE_ONLY |
- tools.PositionInfo.SNAPPING_SYMBOLS_ONLY |
- tools.PositionInfo.SNAPPING_CURVE |
- tools.PositionInfo.SNAPPING_SCATTER)
+ tools.PositionInfo.SNAPPING_CROSSHAIR
+ | tools.PositionInfo.SNAPPING_ACTIVE_ONLY
+ | tools.PositionInfo.SNAPPING_SYMBOLS_ONLY
+ | tools.PositionInfo.SNAPPING_CURVE
+ | tools.PositionInfo.SNAPPING_SCATTER
+ )
self.__setCentralWidget()
# Creating the toolbar also create actions for toolbuttons
self._interactiveModeToolBar = tools.InteractiveModeToolBar(
- parent=self, plot=self)
+ parent=self, plot=self
+ )
self.addToolBar(self._interactiveModeToolBar)
- self._toolbar = self._createToolBar(title='Plot', parent=self)
+ self._toolbar = self._createToolBar(title="Plot", parent=self)
self.addToolBar(self._toolbar)
self._outputToolBar = tools.OutputToolBar(parent=self, plot=self)
@@ -354,11 +384,6 @@ class PlotWindow(PlotWidget):
"""
return self._outputToolBar
- @property
- @deprecated(replacement="getPositionInfoWidget()", since_version="0.8.0")
- def positionWidget(self):
- return self.getPositionInfoWidget()
-
def getPositionInfoWidget(self):
"""Returns the widget displaying current cursor position information
@@ -397,18 +422,23 @@ class PlotWindow(PlotWidget):
banner = "The variable 'plt' is available. Use the 'whos' "
banner += "and 'help(plt)' commands for more information.\n\n"
self._consoleDockWidget = IPythonDockWidget(
- available_vars=available_vars,
- custom_banner=banner,
- parent=self)
+ available_vars=available_vars, custom_banner=banner, parent=self
+ )
self.addTabbedDockWidget(self._consoleDockWidget)
- # self._consoleDockWidget.setVisible(True)
self._consoleDockWidget.toggleViewAction().toggled.connect(
- self.getConsoleAction().setChecked)
+ self._consoleDockWidgetToggled
+ )
self._consoleDockWidget.setVisible(isChecked)
- def _toggleStatsVisibility(self, isChecked=False):
- self.getStatsWidget().parent().setVisible(isChecked)
+ def _consoleVisibilityTriggered(self, isChecked):
+ if isChecked and self.isVisible():
+ self._consoleDockWidget.show()
+ self._consoleDockWidget.raise_()
+
+ def _consoleDockWidgetToggled(self, isChecked):
+ if self.isVisible():
+ self.getConsoleAction().setChecked(isChecked)
def _createToolBar(self, title, parent):
"""Create a QToolBar from the QAction of the PlotWindow.
@@ -437,15 +467,14 @@ class PlotWindow(PlotWidget):
elif obj is self.yAxisInvertedButton:
self.yAxisInvertedAction = toolbar.addWidget(obj)
else:
- raise RuntimeError()
+ raise RuntimeError("unknow action to be defined")
return toolbar
def toolBar(self):
- """Return a QToolBar from the QAction of the PlotWindow.
- """
+ """Return a QToolBar from the QAction of the PlotWindow."""
return self._toolbar
- def menu(self, title='Plot', parent=None):
+ def menu(self, title="Plot", parent=None):
"""Return a QMenu from the QAction of the PlotWindow.
:param str title: The title of the QMenu
@@ -492,8 +521,7 @@ class PlotWindow(PlotWidget):
self.addDockWidget(area, dock_widget)
else:
# Other dock widgets are added as tabs to the same widget area
- self.tabifyDockWidget(self._dockWidgets[0],
- dock_widget)
+ self.tabifyDockWidget(self._dockWidgets[0], dock_widget)
def removeDockWidget(self, dockwidget):
"""Removes the *dockwidget* from the main window layout and hides it.
@@ -518,10 +546,20 @@ class PlotWindow(PlotWidget):
"""
if visible:
dockWidget = self.sender()
- dockWidget.visibilityChanged.disconnect(
- self._handleFirstDockWidgetShow)
+ dockWidget.visibilityChanged.disconnect(self._handleFirstDockWidgetShow)
self.addTabbedDockWidget(dockWidget)
+ def _handleDockWidgetViewActionTriggered(self, checked):
+ if checked:
+ action = self.sender()
+ if action is None:
+ return
+ dockWidget = action.parent()
+ if dockWidget is None:
+ return
+ dockWidget.show() # Show needed here for raise to have an effect
+ dockWidget.raise_()
+
def getColorBarWidget(self):
"""Returns the embedded :class:`ColorBarWidget` widget.
@@ -536,8 +574,12 @@ class PlotWindow(PlotWidget):
if self._legendsDockWidget is None:
self._legendsDockWidget = LegendsDockWidget(plot=self)
self._legendsDockWidget.hide()
+ self._legendsDockWidget.toggleViewAction().triggered.connect(
+ self._handleDockWidgetViewActionTriggered
+ )
self._legendsDockWidget.visibilityChanged.connect(
- self._handleFirstDockWidgetShow)
+ self._handleFirstDockWidgetShow
+ )
return self._legendsDockWidget
def getCurvesRoiDockWidget(self):
@@ -545,10 +587,15 @@ class PlotWindow(PlotWidget):
# (still used internally for lazy loading)
if self._curvesROIDockWidget is None:
self._curvesROIDockWidget = CurvesROIDockWidget(
- plot=self, name='Regions Of Interest')
+ plot=self, name="Regions Of Interest"
+ )
self._curvesROIDockWidget.hide()
+ self._curvesROIDockWidget.toggleViewAction().triggered.connect(
+ self._handleDockWidgetViewActionTriggered
+ )
self._curvesROIDockWidget.visibilityChanged.connect(
- self._handleFirstDockWidgetShow)
+ self._handleFirstDockWidgetShow
+ )
return self._curvesROIDockWidget
def getCurvesRoiWidget(self):
@@ -565,11 +612,14 @@ class PlotWindow(PlotWidget):
def getMaskToolsDockWidget(self):
"""DockWidget with image mask panel (lazy-loaded)."""
if self._maskToolsDockWidget is None:
- self._maskToolsDockWidget = MaskToolsDockWidget(
- plot=self, name='Mask')
+ self._maskToolsDockWidget = MaskToolsDockWidget(plot=self, name="Mask")
self._maskToolsDockWidget.hide()
+ self._maskToolsDockWidget.toggleViewAction().triggered.connect(
+ self._handleDockWidgetViewActionTriggered
+ )
self._maskToolsDockWidget.visibilityChanged.connect(
- self._handleFirstDockWidgetShow)
+ self._handleFirstDockWidgetShow
+ )
return self._maskToolsDockWidget
def getStatsWidget(self):
@@ -583,25 +633,16 @@ class PlotWindow(PlotWidget):
self._statsDockWidget.layout().setContentsMargins(0, 0, 0, 0)
statsWidget = BasicStatsWidget(parent=self, plot=self)
self._statsDockWidget.setWidget(statsWidget)
- statsWidget.sigVisibilityChanged.connect(
- self.getStatsAction().setChecked)
self._statsDockWidget.hide()
+ self._statsDockWidget.toggleViewAction().triggered.connect(
+ self._handleDockWidgetViewActionTriggered
+ )
self._statsDockWidget.visibilityChanged.connect(
- self._handleFirstDockWidgetShow)
+ self._handleFirstDockWidgetShow
+ )
return self._statsDockWidget.widget()
# getters for actions
- @property
- @deprecated(replacement="getInteractiveModeToolBar().getZoomModeAction()",
- since_version="0.8.0")
- def zoomModeAction(self):
- return self.getInteractiveModeToolBar().getZoomModeAction()
-
- @property
- @deprecated(replacement="getInteractiveModeToolBar().getPanModeAction()",
- since_version="0.8.0")
- def panModeAction(self):
- return self.getInteractiveModeToolBar().getPanModeAction()
def getConsoleAction(self):
"""QAction handling the IPython console activation.
@@ -614,10 +655,12 @@ class PlotWindow(PlotWidget):
:rtype: QAction
"""
if self._consoleAction is None:
- self._consoleAction = qt.QAction('Console', self)
+ self._consoleAction = qt.QAction("Console", self)
self._consoleAction.setCheckable(True)
if IPythonDockWidget is not None:
self._consoleAction.toggled.connect(self._toggleConsoleVisibility)
+ self._consoleAction.triggered.connect(self._consoleVisibilityTriggered)
+
else:
self._consoleAction.setEnabled(False)
return self._consoleAction
@@ -628,7 +671,7 @@ class PlotWindow(PlotWidget):
:rtype: actions.PlotAction
"""
if self._crosshairAction is None:
- self._crosshairAction = actions.control.CrosshairAction(self, color='red')
+ self._crosshairAction = actions.control.CrosshairAction(self, color="red")
return self._crosshairAction
def getMaskAction(self):
@@ -648,12 +691,7 @@ class PlotWindow(PlotWidget):
return self._panWithArrowKeysAction
def getStatsAction(self):
- if self._statsAction is None:
- self._statsAction = qt.QAction('Curves stats', self)
- self._statsAction.setCheckable(True)
- self._statsAction.setChecked(self.getStatsWidget().parent().isVisible())
- self._statsAction.toggled.connect(self._toggleStatsVisibility)
- return self._statsAction
+ return self.getStatsWidget().parent().toggleViewAction()
def getRoiAction(self):
"""QAction toggling curve ROI dock widget
@@ -837,22 +875,36 @@ class Plot1D(PlotWindow):
"""
def __init__(self, parent=None, backend=None):
- super(Plot1D, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=True,
- logScale=True, grid=True,
- curveStyle=True, colormap=False,
- aspectRatio=False, yInverted=False,
- copy=True, save=True, print_=True,
- control=True, position=True,
- roi=True, mask=False, fit=True)
+ super(Plot1D, self).__init__(
+ parent=parent,
+ backend=backend,
+ resetzoom=True,
+ autoScale=True,
+ logScale=True,
+ grid=True,
+ curveStyle=True,
+ colormap=False,
+ aspectRatio=False,
+ yInverted=False,
+ copy=True,
+ save=True,
+ print_=True,
+ control=True,
+ position=True,
+ roi=True,
+ mask=False,
+ fit=True,
+ )
if parent is None:
- self.setWindowTitle('Plot1D')
- self.getXAxis().setLabel('X')
- self.getYAxis().setLabel('Y')
+ self.setWindowTitle("Plot1D")
+ self.getXAxis().setLabel("X")
+ self.getYAxis().setLabel("Y")
action = self.getFitAction()
action.setXRangeUpdatedOnZoom(True)
action.setFittedItemUpdatedFromActiveCurve(True)
+ self.getInteractiveModeToolBar().getZoomModeAction().setAxesMenuEnabled(True)
+
class Plot2D(PlotWindow):
"""PlotWindow with a toolbar specific for images.
@@ -868,26 +920,37 @@ class Plot2D(PlotWindow):
def __init__(self, parent=None, backend=None):
# List of information to display at the bottom of the plot
posInfo = [
- ('X', lambda x, y: x),
- ('Y', lambda x, y: y),
- ('Data', WeakMethodProxy(self._getImageValue)),
- ('Dims', WeakMethodProxy(self._getImageDims)),
+ ("X", lambda x, y: x),
+ ("Y", lambda x, y: y),
+ ("Data", WeakMethodProxy(self._getImageValue)),
+ ("Dims", WeakMethodProxy(self._getImageDims)),
]
- super(Plot2D, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=False,
- logScale=False, grid=False,
- curveStyle=False, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=posInfo,
- roi=False, mask=True)
+ super(Plot2D, self).__init__(
+ parent=parent,
+ backend=backend,
+ resetzoom=True,
+ autoScale=False,
+ logScale=False,
+ grid=False,
+ curveStyle=False,
+ colormap=True,
+ aspectRatio=True,
+ yInverted=True,
+ copy=True,
+ save=True,
+ print_=True,
+ control=False,
+ position=posInfo,
+ roi=False,
+ mask=True,
+ )
if parent is None:
- self.setWindowTitle('Plot2D')
- self.getXAxis().setLabel('Columns')
- self.getYAxis().setLabel('Rows')
+ self.setWindowTitle("Plot2D")
+ self.getXAxis().setLabel("Columns")
+ self.getYAxis().setLabel("Rows")
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
+ if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
self.getYAxis().setInverted(True)
self.profile = ProfileToolBar(plot=self)
@@ -942,8 +1005,9 @@ class Plot2D(PlotWindow):
"""
pickedMask = None
for picked in self.pickItems(
- *self.dataToPixel(x, y, check=False),
- lambda item: isinstance(item, items.ImageBase)):
+ *self.dataToPixel(x, y, check=False),
+ lambda item: isinstance(item, items.ImageBase),
+ ):
if isinstance(picked.getItem(), items.MaskImageData):
if pickedMask is None: # Use top-most if many masks
pickedMask = picked
@@ -963,16 +1027,15 @@ class Plot2D(PlotWindow):
return value, "Masked"
return value
- return '-' # No image picked
+ return "-" # No image picked
def _getImageDims(self, *args):
activeImage = self.getActiveImage()
- if (activeImage is not None and
- activeImage.getData(copy=False) is not None):
+ if activeImage is not None and activeImage.getData(copy=False) is not None:
dims = activeImage.getData(copy=False).shape[1::-1]
- return 'x'.join(str(dim) for dim in dims)
+ return "x".join(str(dim) for dim in dims)
else:
- return '-'
+ return "-"
def getProfileToolbar(self):
"""Profile tools attached to this plot
@@ -981,10 +1044,6 @@ class Plot2D(PlotWindow):
"""
return self.profile
- @deprecated(replacement="getProfilePlot", since_version="0.5.0")
- def getProfileWindow(self):
- return self.getProfilePlot()
-
def getProfilePlot(self):
"""Return plot window used to display profile curve.
diff --git a/src/silx/gui/plot/PrintPreviewToolButton.py b/src/silx/gui/plot/PrintPreviewToolButton.py
index 30967e4..0812420 100644
--- a/src/silx/gui/plot/PrintPreviewToolButton.py
+++ b/src/silx/gui/plot/PrintPreviewToolButton.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -101,7 +100,6 @@ plots on the same page. The plots all instantiate a
app.exec()
"""
-from __future__ import absolute_import
import logging
from io import StringIO
@@ -111,7 +109,6 @@ from .. import icons
from . import PlotWidget
from ..widgets.PrintPreview import PrintPreviewDialog, SingletonPrintPreviewDialog
from ..widgets.PrintGeometryDialog import PrintGeometryDialog
-from silx.utils.deprecation import deprecated
__authors__ = ["P. Knobel"]
__license__ = "MIT"
@@ -128,6 +125,7 @@ class PrintPreviewToolButton(qt.QToolButton):
:param parent: See :class:`QAction`
:param plot: :class:`.PlotWidget` instance on which to operate
"""
+
def __init__(self, parent=None, plot=None):
super(PrintPreviewToolButton, self).__init__(parent)
@@ -135,17 +133,19 @@ class PrintPreviewToolButton(qt.QToolButton):
raise TypeError("plot parameter must be a PlotWidget")
self._plot = plot
- self.setIcon(icons.getQIcon('document-print'))
+ self.setIcon(icons.getQIcon("document-print"))
printGeomAction = qt.QAction("Print geometry", self)
- printGeomAction.setToolTip("Define a print geometry prior to sending "
- "the plot to the print preview dialog")
- printGeomAction.setIcon(icons.getQIcon('shape-rectangle'))
+ printGeomAction.setToolTip(
+ "Define a print geometry prior to sending "
+ "the plot to the print preview dialog"
+ )
+ printGeomAction.setIcon(icons.getQIcon("shape-rectangle"))
printGeomAction.triggered.connect(self._setPrintConfiguration)
printPreviewAction = qt.QAction("Print preview", self)
printPreviewAction.setToolTip("Send plot to the print preview dialog")
- printPreviewAction.setIcon(icons.getQIcon('document-print'))
+ printPreviewAction.setIcon(icons.getQIcon("document-print"))
printPreviewAction.triggered.connect(self._plotToPrintPreview)
menu = qt.QMenu(self)
@@ -157,12 +157,14 @@ class PrintPreviewToolButton(qt.QToolButton):
self._printPreviewDialog = None
self._printConfigurationDialog = None
- self._printGeometry = {"xOffset": 0.1,
- "yOffset": 0.1,
- "width": 0.9,
- "height": 0.9,
- "units": "page",
- "keepAspectRatio": True}
+ self._printGeometry = {
+ "xOffset": 0.1,
+ "yOffset": 0.1,
+ "width": 0.9,
+ "height": 0.9,
+ "units": "page",
+ "keepAspectRatio": True,
+ }
@property
def printPreviewDialog(self):
@@ -191,12 +193,6 @@ class PrintPreviewToolButton(qt.QToolButton):
"""
return None, None
- @property
- @deprecated(since_version="0.10",
- replacement="getPlot()")
- def plot(self):
- return self._plot
-
def getPlot(self):
"""Return the :class:`.PlotWidget` associated with this tool button.
@@ -214,19 +210,23 @@ class PrintPreviewToolButton(qt.QToolButton):
if qt.HAS_SVG:
svgRenderer, viewBox = self._getSvgRendererAndViewbox()
- self.printPreviewDialog.addSvgItem(svgRenderer,
- title=self.getTitle(),
- comment=comment,
- commentPosition=commentPosition,
- viewBox=viewBox,
- keepRatio=self._printGeometry["keepAspectRatio"])
+ self.printPreviewDialog.addSvgItem(
+ svgRenderer,
+ title=self.getTitle(),
+ comment=comment,
+ commentPosition=commentPosition,
+ viewBox=viewBox,
+ keepRatio=self._printGeometry["keepAspectRatio"],
+ )
else:
_logger.warning("Missing QtSvg library, using a raster image")
pixmap = self._plot.centralWidget().grab()
- self.printPreviewDialog.addPixmap(pixmap,
- title=self.getTitle(),
- comment=comment,
- commentPosition=commentPosition)
+ self.printPreviewDialog.addPixmap(
+ pixmap,
+ title=self.getTitle(),
+ comment=comment,
+ commentPosition=commentPosition,
+ )
self.printPreviewDialog.show()
self.printPreviewDialog.raise_()
@@ -238,8 +238,7 @@ class PrintPreviewToolButton(qt.QToolButton):
and to the geometry configuration (width, height, ratio) specified
by the user."""
imgData = StringIO()
- assert self._plot.saveGraph(imgData, fileFormat="svg"), \
- "Unable to save graph"
+ assert self._plot.saveGraph(imgData, fileFormat="svg"), "Unable to save graph"
imgData.flush()
imgData.seek(0)
svgData = imgData.read()
@@ -263,8 +262,7 @@ class PrintPreviewToolButton(qt.QToolButton):
return svgRenderer, viewbox
def _getViewBox(self):
- """
- """
+ """ """
printer = self.printPreviewDialog.printer
dpix = printer.logicalDpiX()
dpiy = printer.logicalDpiY()
@@ -272,23 +270,23 @@ class PrintPreviewToolButton(qt.QToolButton):
availableHeight = printer.height()
config = self._printGeometry
- width = config['width']
- height = config['height']
- xOffset = config['xOffset']
- yOffset = config['yOffset']
- units = config['units']
- keepAspectRatio = config['keepAspectRatio']
+ width = config["width"]
+ height = config["height"]
+ xOffset = config["xOffset"]
+ yOffset = config["yOffset"]
+ units = config["units"]
+ keepAspectRatio = config["keepAspectRatio"]
aspectRatio = self._getPlotAspectRatio()
# convert the offsets to dots
- if units.lower() in ['inch', 'inches']:
+ if units.lower() in ["inch", "inches"]:
xOffset = xOffset * dpix
yOffset = yOffset * dpiy
if width is not None:
width = width * dpix
if height is not None:
height = height * dpiy
- elif units.lower() in ['cm', 'centimeters']:
+ elif units.lower() in ["cm", "centimeters"]:
xOffset = (xOffset / 2.54) * dpix
yOffset = (yOffset / 2.54) * dpiy
if width is not None:
@@ -309,13 +307,17 @@ class PrintPreviewToolButton(qt.QToolButton):
if width is not None:
if (availableWidth + 0.1) < width:
- txt = "Available width %f is less than requested width %f" % \
- (availableWidth, width)
+ txt = "Available width %f is less than requested width %f" % (
+ availableWidth,
+ width,
+ )
raise ValueError(txt)
if height is not None:
if (availableHeight + 0.1) < height:
- txt = "Available height %f is less than requested height %f" % \
- (availableHeight, height)
+ txt = "Available height %f is less than requested height %f" % (
+ availableHeight,
+ height,
+ )
raise ValueError(txt)
if keepAspectRatio:
@@ -330,10 +332,7 @@ class PrintPreviewToolButton(qt.QToolButton):
bodyWidth = width or availableWidth
bodyHeight = height or availableHeight
- return qt.QRectF(xOffset,
- yOffset,
- bodyWidth,
- bodyHeight)
+ return qt.QRectF(xOffset, yOffset, bodyWidth, bodyHeight)
def _setPrintConfiguration(self):
"""Open a dialog to prompt the user to adjust print
@@ -359,6 +358,7 @@ class SingletonPrintPreviewToolButton(PrintPreviewToolButton):
This allows for several plots to send their content to the
same print page, and for users to arrange them."""
+
def __init__(self, parent=None, plot=None):
PrintPreviewToolButton.__init__(self, parent, plot)
@@ -369,14 +369,14 @@ class SingletonPrintPreviewToolButton(PrintPreviewToolButton):
return self._printPreviewDialog
-if __name__ == '__main__':
+if __name__ == "__main__":
import numpy
+
app = qt.QApplication([])
pw = PlotWidget()
toolbar = qt.QToolBar(pw)
- toolbutton = PrintPreviewToolButton(parent=toolbar,
- plot=pw)
+ toolbutton = PrintPreviewToolButton(parent=toolbar, plot=pw)
pw.addToolBar(toolbar)
toolbar.addWidget(toolbutton)
pw.show()
diff --git a/src/silx/gui/plot/Profile.py b/src/silx/gui/plot/Profile.py
index 7565155..f89f780 100644
--- a/src/silx/gui/plot/Profile.py
+++ b/src/silx/gui/plot/Profile.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -35,27 +34,18 @@ import weakref
from .. import qt
from . import actions
-from .tools.profile import core
from .tools.profile import manager
from .tools.profile import rois
from silx.gui.widgets.MultiModeAction import MultiModeAction
-from silx.utils.deprecation import deprecated
-from silx.utils.deprecation import deprecated_warning
from .tools import roi as roi_mdl
from silx.gui.plot import items
-@deprecated(replacement="silx.gui.plot.tools.profile.createProfile", since_version="0.13.0")
-def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
- return core.createProfile(roiInfo, currentData, origin,
- scale, lineWidth, method)
-
-
class _CustomProfileManager(manager.ProfileManager):
"""This custom profile manager uses a single predefined profile window
if it is specified. Else the behavior is the same as the default
- ProfileManager """
+ ProfileManager"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -79,7 +69,10 @@ class _CustomProfileManager(manager.ProfileManager):
self.__profileWindow = profileWindow
def createProfileWindow(self, plot, roi):
- for roiClass, specializedProfileWindow in self.__specializedProfileWindows.items():
+ for (
+ roiClass,
+ specializedProfileWindow,
+ ) in self.__specializedProfileWindows.items():
if isinstance(roi, roiClass):
return specializedProfileWindow
@@ -122,23 +115,13 @@ class ProfileToolBar(qt.QToolBar):
:param plot: :class:`PlotWindow` instance on which to operate.
:param profileWindow: Plot widget instance where to
display the profile curve or None to create one.
- :param str title: See :class:`QToolBar`.
:param parent: See :class:`QToolBar`.
"""
- def __init__(self, parent=None, plot=None, profileWindow=None,
- title=None):
- super(ProfileToolBar, self).__init__(title, parent)
+ def __init__(self, parent=None, plot=None, profileWindow=None):
+ super(ProfileToolBar, self).__init__(parent)
assert plot is not None
- if title is not None:
- deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
-
self._plotRef = weakref.ref(plot)
# If a profileWindow is defined,
@@ -186,22 +169,27 @@ class ProfileToolBar(qt.QToolBar):
return _CustomProfileManager(parent, plot)
def _createProfileActions(self):
- self.hLineAction = self._manager.createProfileAction(rois.ProfileImageHorizontalLineROI, self)
- self.vLineAction = self._manager.createProfileAction(rois.ProfileImageVerticalLineROI, self)
- self.lineAction = self._manager.createProfileAction(rois.ProfileImageLineROI, self)
- self.freeLineAction = self._manager.createProfileAction(rois.ProfileImageDirectedLineROI, self)
- self.crossAction = self._manager.createProfileAction(rois.ProfileImageCrossROI, self)
+ self.hLineAction = self._manager.createProfileAction(
+ rois.ProfileImageHorizontalLineROI, self
+ )
+ self.vLineAction = self._manager.createProfileAction(
+ rois.ProfileImageVerticalLineROI, self
+ )
+ self.lineAction = self._manager.createProfileAction(
+ rois.ProfileImageLineROI, self
+ )
+ self.freeLineAction = self._manager.createProfileAction(
+ rois.ProfileImageDirectedLineROI, self
+ )
+ self.crossAction = self._manager.createProfileAction(
+ rois.ProfileImageCrossROI, self
+ )
self.clearAction = self._manager.createClearAction(self)
def getPlotWidget(self):
"""The :class:`.PlotWidget` associated to the toolbar."""
return self._plotRef()
- @property
- @deprecated(since_version="0.13.0", replacement="getPlotWidget()")
- def plot(self):
- return self.getPlotWidget()
-
def _setRoiActionEnabled(self, itemKind, enabled):
for action in self.__multiAction.getMenu().actions():
if not isinstance(action, roi_mdl.CreateRoiModeAction):
@@ -222,16 +210,6 @@ class ProfileToolBar(qt.QToolBar):
enabled = image.getData(copy=False).size > 0
self._setRoiActionEnabled(type(image), enabled)
- @property
- @deprecated(since_version="0.6.0")
- def browseAction(self):
- return self._browseAction
-
- @property
- @deprecated(replacement="getProfilePlot", since_version="0.5.0")
- def profileWindow(self):
- return self.getProfilePlot()
-
def getProfileManager(self):
"""Return the manager of the profiles.
@@ -239,114 +217,38 @@ class ProfileToolBar(qt.QToolBar):
"""
return self._manager
- @deprecated(since_version="0.13.0")
- def getProfilePlot(self):
- """Return plot widget in which the profile curve or the
- profile image is plotted.
- """
- window = self.getProfileMainWindow()
- if window is None:
- return None
- return window.getCurrentPlotWidget()
-
- @deprecated(replacement="getProfileManager().getCurrentRoi().getProfileWindow()", since_version="0.13.0")
- def getProfileMainWindow(self):
- """Return window containing the profile curve widget.
-
- This can return None if no profile was computed.
- """
- roi = self._manager.getCurrentRoi()
- if roi is None:
- return None
- return roi.getProfileWindow()
-
- @property
- @deprecated(since_version="0.13.0")
- def overlayColor(self):
- """This method does nothing anymore. But could be implemented if needed.
-
- It was used to set color to use for the ROI.
-
- If set to None (the default), the overlay color is adapted to the
- active image colormap and changes if the active image colormap changes.
- """
- pass
-
- @overlayColor.setter
- @deprecated(since_version="0.13.0")
- def overlayColor(self, color):
- """This method does nothing anymore. But could be implemented if needed.
- """
- pass
-
def clearProfile(self):
"""Remove profile curve and profile area."""
self._manager.clearProfile()
- @deprecated(since_version="0.13.0")
- def updateProfile(self):
- """This method does nothing anymore. But could be implemented if needed.
-
- It was used to update the displayed profile and profile ROI.
-
- This uses the current active image of the plot and the current ROI.
- """
- pass
-
- @deprecated(replacement="clearProfile()", since_version="0.13.0")
- def hideProfileWindow(self):
- """Hide profile window.
- """
- self.clearProfile()
-
- @deprecated(since_version="0.13.0")
- def setProfileMethod(self, method):
- assert method in ('sum', 'mean')
- roi = self._manager.getCurrentRoi()
- if roi is None:
- raise RuntimeError("No profile ROI selected")
- roi.setProfileMethod(method)
-
- @deprecated(since_version="0.13.0")
- def getProfileMethod(self):
- roi = self._manager.getCurrentRoi()
- if roi is None:
- raise RuntimeError("No profile ROI selected")
- return roi.getProfileMethod()
-
- @deprecated(since_version="0.13.0")
- def getProfileOptionToolAction(self):
- return self._editor
-
class Profile3DToolBar(ProfileToolBar):
- def __init__(self, parent=None, stackview=None,
- title=None):
+ def __init__(self, parent=None, stackview=None):
"""QToolBar providing profile tools for an image or a stack of images.
:param parent: the parent QWidget
:param stackview: :class:`StackView` instance on which to operate.
- :param str title: See :class:`QToolBar`.
:param parent: See :class:`QToolBar`.
"""
# TODO: add param profileWindow (specify the plot used for profiles)
- super(Profile3DToolBar, self).__init__(parent=parent,
- plot=stackview.getPlotWidget())
-
- if title is not None:
- deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
+ super(Profile3DToolBar, self).__init__(
+ parent=parent, plot=stackview.getPlotWidget()
+ )
self.stackView = stackview
""":class:`StackView` instance"""
def _createProfileActions(self):
- self.hLineAction = self._manager.createProfileAction(rois.ProfileImageStackHorizontalLineROI, self)
- self.vLineAction = self._manager.createProfileAction(rois.ProfileImageStackVerticalLineROI, self)
- self.lineAction = self._manager.createProfileAction(rois.ProfileImageStackLineROI, self)
- self.crossAction = self._manager.createProfileAction(rois.ProfileImageStackCrossROI, self)
+ self.hLineAction = self._manager.createProfileAction(
+ rois.ProfileImageStackHorizontalLineROI, self
+ )
+ self.vLineAction = self._manager.createProfileAction(
+ rois.ProfileImageStackVerticalLineROI, self
+ )
+ self.lineAction = self._manager.createProfileAction(
+ rois.ProfileImageStackLineROI, self
+ )
+ self.crossAction = self._manager.createProfileAction(
+ rois.ProfileImageStackCrossROI, self
+ )
self.clearAction = self._manager.createClearAction(self)
diff --git a/src/silx/gui/plot/ProfileMainWindow.py b/src/silx/gui/plot/ProfileMainWindow.py
deleted file mode 100644
index ce56cfd..0000000
--- a/src/silx/gui/plot/ProfileMainWindow.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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 contains a QMainWindow class used to display profile plots.
-"""
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "21/02/2017"
-
-import silx.utils.deprecation
-from silx.gui import qt
-from .tools.profile.manager import ProfileWindow
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.ProfileMainWindow",
- reason="moved",
- replacement="silx.gui.plot.tools.profile.manager.ProfileWindow",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
-
-class ProfileMainWindow(ProfileWindow):
- """QMainWindow providing 2 plot widgets specialized in
- 1D and 2D plotting, with different toolbars.
-
- Only one of the plots is visible at any given time.
-
- :param qt.QWidget parent: The parent of this widget or None (default).
- :param Union[str,Class] backend: The backend to use, in:
- 'matplotlib' (default), 'mpl', 'opengl', 'gl', 'none'
- or a :class:`BackendBase.BackendBase` class
- """
-
- sigProfileDimensionsChanged = qt.Signal(int)
- """This signal is emitted when :meth:`setProfileDimensions` is called.
- It carries the number of dimensions for the profile data (1 or 2).
- It can be used to be notified that the profile plot widget has changed.
-
- Note: This signal should be removed.
- """
-
- sigProfileMethodChanged = qt.Signal(str)
- """Emitted when the method to compute the profile changed (for now can be
- sum or mean)
-
- Note: This signal should be removed.
- """
-
- def __init__(self, parent=None, backend=None):
- ProfileWindow.__init__(self, parent=parent, backend=backend)
- # by default, profile is assumed to be a 1D curve
- self._profileType = None
-
- def setProfileType(self, profileType):
- """Set which profile plot widget (1D or 2D) is to be used
-
- Note: This method should be removed.
-
- :param str profileType: Type of profile data,
- "1D" for a curve or "2D" for an image
- """
- self._profileType = profileType
- if self._profileType == "1D":
- self._showPlot1D()
- elif self._profileType == "2D":
- self._showPlot2D()
- else:
- raise ValueError("Profile type must be '1D' or '2D'")
- self.sigProfileDimensionsChanged.emit(profileType)
-
- def getPlot(self):
- """Return the profile plot widget which is currently in use.
- This can be the 2D profile plot or the 1D profile plot.
-
- Note: This method should be removed.
- """
- return self.getCurrentPlotWidget()
-
- def setProfileMethod(self, method):
- """
- Note: This method should be removed.
-
- :param str method: method to manage the 'width' in the profile
- (computing mean or sum).
- """
- assert method in ('sum', 'mean')
- self._method = method
- self.sigProfileMethodChanged.emit(self._method)
diff --git a/src/silx/gui/plot/ROIStatsWidget.py b/src/silx/gui/plot/ROIStatsWidget.py
index 32a1395..36f3391 100644
--- a/src/silx/gui/plot/ROIStatsWidget.py
+++ b/src/silx/gui/plot/ROIStatsWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -35,8 +34,8 @@ __date__ = "22/07/2019"
from contextlib import contextmanager
from silx.gui import qt
from silx.gui import icons
-from silx.gui.plot.StatsWidget import _StatsWidgetBase, StatsTable, _Container
-from silx.gui.plot.StatsWidget import UpdateModeWidget, UpdateMode
+from silx.gui.plot.StatsWidget import _StatsWidgetBase, _Container
+from silx.gui.plot.StatsWidget import UpdateMode
from silx.gui.widgets.TableWidget import TableWidget
from silx.gui.plot.items.roi import RegionOfInterest
from silx.gui.plot import items as plotitems
@@ -44,7 +43,6 @@ from silx.gui.plot.items.core import ItemChangedType
from silx.gui.plot3d import items as plot3ditems
from silx.gui.plot.CurvesROIWidget import ROI
from silx.gui.plot import stats as statsmdl
-from collections import OrderedDict
from silx.utils.proxy import docstring
import silx.gui.plot.items.marker
import silx.gui.plot.items.shape
@@ -58,7 +56,8 @@ class _GetROIItemCoupleDialog(qt.QDialog):
"""
Dialog used to know which plot item and which roi he wants
"""
- _COMPATIBLE_KINDS = ('curve', 'image', 'scatter', 'histogram')
+
+ _COMPATIBLE_KINDS = ("curve", "image", "scatter", "histogram")
def __init__(self, parent=None, plot=None, rois=None):
qt.QDialog.__init__(self, parent=parent)
@@ -93,13 +92,15 @@ class _GetROIItemCoupleDialog(qt.QDialog):
def _getCompatibleRois(self, kind):
"""Return compatible rois for the given item kind"""
+
def is_compatible(roi, kind):
if isinstance(roi, RegionOfInterest):
- return kind in ('image', 'scatter')
+ return kind in ("image", "scatter")
elif isinstance(roi, ROI):
- return kind in ('curve', 'histogram')
+ return kind in ("curve", "histogram")
else:
- raise ValueError('kind not managed')
+ raise ValueError("kind not managed")
+
return list(filter(lambda x: is_compatible(x, kind), self._rois))
def exec(self):
@@ -115,6 +116,7 @@ class _GetROIItemCoupleDialog(qt.QDialog):
self._kind_name_to_item = {}
# key is (kind, legend name) value is item
for kind in _GetROIItemCoupleDialog._COMPATIBLE_KINDS:
+
def getItems(kind):
output = []
for item in self._plot.getItems():
@@ -136,7 +138,7 @@ class _GetROIItemCoupleDialog(qt.QDialog):
# filter roi according to kinds
if len(self._valid_kinds) == 0:
- _logger.warning('no couple item/roi detected for displaying stats')
+ _logger.warning("no couple item/roi detected for displaying stats")
return self.reject()
for kind in self._valid_kinds:
@@ -174,10 +176,11 @@ class ROIStatsItemHelper(object):
Display on one row statistics regarding the couple
(Item (plot item) / roi).
- :param Item plot_item: item for which we want statistics
+ :param Item plot_item: item for which we want statistics
:param Union[ROI,RegionOfInterest]: region of interest to use for
statistics.
"""
+
def __init__(self, plot_item, roi):
self._plot_item = plot_item
self._roi = roi
@@ -193,7 +196,7 @@ class ROIStatsItemHelper(object):
elif isinstance(self._roi, RegionOfInterest):
return self._roi.getName()
else:
- raise TypeError('Unmanaged roi type')
+ raise TypeError("Unmanaged roi type")
@property
def roi_kind(self):
@@ -204,19 +207,21 @@ class ROIStatsItemHelper(object):
def item_kind(self):
"""item kind"""
if isinstance(self._plot_item, plotitems.Curve):
- return 'curve'
+ return "curve"
elif isinstance(self._plot_item, plotitems.ImageData):
- return 'image'
+ return "image"
elif isinstance(self._plot_item, plotitems.Scatter):
- return 'scatter'
+ return "scatter"
elif isinstance(self._plot_item, plotitems.Histogram):
- return 'histogram'
- elif isinstance(self._plot_item, (plot3ditems.ImageData,
- plot3ditems.ScalarField3D)):
- return 'image'
- elif isinstance(self._plot_item, (plot3ditems.Scatter2D,
- plot3ditems.Scatter3D)):
- return 'scatter'
+ return "histogram"
+ elif isinstance(
+ self._plot_item, (plot3ditems.ImageData, plot3ditems.ScalarField3D)
+ ):
+ return "image"
+ elif isinstance(
+ self._plot_item, (plot3ditems.Scatter2D, plot3ditems.Scatter3D)
+ ):
+ return "scatter"
@property
def item_legend(self):
@@ -225,27 +230,28 @@ class ROIStatsItemHelper(object):
def id_key(self):
"""unique key to represent the couple (item, roi)"""
- return (self.item_kind(), self.item_legend, self.roi_kind,
- self.roi_name())
+ return (self.item_kind(), self.item_legend, self.roi_kind, self.roi_name())
class _StatsROITable(_StatsWidgetBase, TableWidget):
"""
Table sued to display some statistics regarding a couple (item/roi)
"""
- _LEGEND_HEADER_DATA = 'legend'
- _KIND_HEADER_DATA = 'kind'
+ _LEGEND_HEADER_DATA = "legend"
+
+ _KIND_HEADER_DATA = "kind"
- _ROI_HEADER_DATA = 'roi'
+ _ROI_HEADER_DATA = "roi"
sigUpdateModeChanged = qt.Signal(object)
"""Signal emitted when the update mode changed"""
def __init__(self, parent, plot):
TableWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self, statsOnVisibleData=False,
- displayOnlyActItem=False)
+ _StatsWidgetBase.__init__(
+ self, statsOnVisibleData=False, displayOnlyActItem=False
+ )
self.__region_edition_callback = {}
"""We need to keep trace of the roi signals connection because
the roi emits the sigChanged during roi edition"""
@@ -285,8 +291,8 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
def _addItem(self, item):
"""
Add a _RoiStatsItemWidget item to the table.
-
- :param item:
+
+ :param item:
:return: True if successfully added.
"""
if not isinstance(item, ROIStatsItemHelper):
@@ -308,7 +314,8 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
tableItems = [
qt.QTableWidgetItem(), # Legend
qt.QTableWidgetItem(), # Kind
- qt.QTableWidgetItem()] # roi
+ qt.QTableWidgetItem(),
+ ] # roi
for column in range(3, self.columnCount()):
header = self.horizontalHeaderItem(column)
@@ -335,8 +342,7 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
row = self.rowCount() - 1
for column, tableItem in enumerate(tableItems):
tableItem.setData(qt.Qt.UserRole, _Container(item))
- tableItem.setFlags(
- qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
+ tableItem.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
self.setItem(row, column, tableItem)
# Update table items content
@@ -345,8 +351,9 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
# Listen for item changes
# Using queued connection to avoid issue with sender
# being that of the signal calling the signal
- item._plot_item.sigItemChanged.connect(self._plotItemChanged,
- qt.Qt.QueuedConnection)
+ item._plot_item.sigItemChanged.connect(
+ self._plotItemChanged, qt.Qt.QueuedConnection
+ )
return True
def _removeAllItems(self):
@@ -370,7 +377,9 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
_StatsWidgetBase.setStats(self, statsHandler)
self.setRowCount(0)
- self.setColumnCount(len(self._statsHandler.stats) + 3) # + legend, kind and roi # noqa
+ self.setColumnCount(
+ len(self._statsHandler.stats) + 3
+ ) # + legend, kind and roi # noqa
for index, stat in enumerate(self._statsHandler.stats.values()):
headerItem = qt.QTableWidgetItem(stat.name.capitalize())
@@ -408,10 +417,14 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
statsHandler = self.getStatsHandler()
if statsHandler is not None:
- stats = statsHandler.calculate(plotItem, plot,
- onlimits=self._statsOnVisibleData,
- roi=roi, data_changed=data_changed,
- roi_changed=roi_changed)
+ stats = statsHandler.calculate(
+ plotItem,
+ plot,
+ onlimits=self._statsOnVisibleData,
+ roi=roi,
+ data_changed=data_changed,
+ roi_changed=roi_changed,
+ )
else:
stats = {}
@@ -429,7 +442,7 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
value = stats.get(name)
if value is None:
_logger.error("Value not found for: %s", name)
- tableItem.setText('-')
+ tableItem.setText("-")
else:
tableItem.setText(str(value))
@@ -474,9 +487,9 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
:param item: The plot item
:return: An ordered dict of column name to QTableWidgetItem mapping
for the given plot item.
- :rtype: OrderedDict
+ :rtype: dict
"""
- result = OrderedDict()
+ result = {}
row = self._itemToRow(item)
if row is not None:
for column in range(self.columnCount()):
@@ -520,15 +533,21 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
# item connection within sigRegionChanged should only be
# stopped during the region edition
self.__region_edition_callback[item._roi] = functools.partial(
- self._updateAllStats, False, True)
- item._roi.sigRegionChanged.connect(self.__region_edition_callback[item._roi])
- item._roi.sigEditingStarted.connect(functools.partial(
- self._startFiltering, item._roi))
- item._roi.sigEditingFinished.connect(functools.partial(
- self._endFiltering, item._roi))
+ self._updateAllStats, False, True
+ )
+ item._roi.sigRegionChanged.connect(
+ self.__region_edition_callback[item._roi]
+ )
+ item._roi.sigEditingStarted.connect(
+ functools.partial(self._startFiltering, item._roi)
+ )
+ item._roi.sigEditingFinished.connect(
+ functools.partial(self._endFiltering, item._roi)
+ )
else:
- item._roi.sigChanged.connect(functools.partial(
- self._updateAllStats, False, True))
+ item._roi.sigChanged.connect(
+ functools.partial(self._updateAllStats, False, True)
+ )
self.__roiToItems[item._roi].add(item)
def _startFiltering(self, roi):
@@ -542,10 +561,12 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
if roi in self.__roiToItems:
del self.__roiToItems[roi]
if isinstance(roi, RegionOfInterest):
- roi.sigRegionEditionStarted.disconnect(functools.partial(
- self._startFiltering, roi))
- roi.sigRegionEditionFinished.disconnect(functools.partial(
- self._startFiltering, roi))
+ roi.sigRegionEditionStarted.disconnect(
+ functools.partial(self._startFiltering, roi)
+ )
+ roi.sigRegionEditionFinished.disconnect(
+ functools.partial(self._startFiltering, roi)
+ )
try:
roi.sigRegionChanged.disconnect(self._updateAllStats)
except:
@@ -576,11 +597,13 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
self.setRowHidden(row_index, not item.isVisible())
def _removeItem(self, itemKey):
- if isinstance(itemKey, (silx.gui.plot.items.marker.Marker,
- silx.gui.plot.items.shape.Shape)):
+ if isinstance(
+ itemKey,
+ (silx.gui.plot.items.marker.Marker, silx.gui.plot.items.shape.Shape),
+ ):
return
if itemKey not in self._items:
- _logger.warning('key not recognized. Won\'t remove any item')
+ _logger.warning("key not recognized. Won't remove any item")
return
item = self._items[itemKey]
row = self._itemToRow(item)
@@ -598,16 +621,20 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
:param bool is_request: True if come from a manual request
"""
- if (self.getUpdateMode() is UpdateMode.MANUAL and
- not is_request and not roi_changed):
+ if (
+ self.getUpdateMode() is UpdateMode.MANUAL
+ and not is_request
+ and not roi_changed
+ ):
return
with self._disableSorting():
for row in range(self.rowCount()):
tableItem = self.item(row, 0)
item = self._tableItemToItem(tableItem)
- self._updateStats(item, roi_changed=roi_changed,
- data_changed=is_request)
+ self._updateStats(
+ item, roi_changed=roi_changed, data_changed=is_request
+ )
def _plotCurrentChanged(self, *args):
pass
@@ -625,7 +652,10 @@ class _StatsROITable(_StatsWidgetBase, TableWidget):
"""return the plotItem fitting the requirement kind, legend.
This information is enough to be sure it is unique (in the widget)"""
for plotItem in self.__plotItemToItems:
- if legend == plotItem.getLegend() and self._plotWrapper.getKind(plotItem) == kind:
+ if (
+ legend == plotItem.getLegend()
+ and self._plotWrapper.getKind(plotItem) == kind
+ ):
return plotItem
return None
@@ -669,12 +699,12 @@ class ROIStatsWidget(qt.QMainWindow):
qt.QMainWindow.__init__(self, parent)
toolbar = qt.QToolBar(self)
- icon = icons.getQIcon('add')
+ icon = icons.getQIcon("add")
self._rois = list(rois) if rois is not None else []
- self._addAction = qt.QAction(icon, 'add item/roi', toolbar)
+ self._addAction = qt.QAction(icon, "add item/roi", toolbar)
self._addAction.triggered.connect(self._addRoiStatsItem)
- icon = icons.getQIcon('rm')
- self._removeAction = qt.QAction(icon, 'remove item/roi', toolbar)
+ icon = icons.getQIcon("rm")
+ self._removeAction = qt.QAction(icon, "remove item/roi", toolbar)
self._removeAction.triggered.connect(self._removeCurrentRow)
toolbar.addAction(self._addAction)
@@ -718,15 +748,14 @@ class ROIStatsWidget(qt.QMainWindow):
@docstring(_StatsROITable)
def getStatsHandler(self):
"""
-
- :return:
+
+ :return:
"""
return self._statsROITable.getStatsHandler()
def _addRoiStatsItem(self):
"""Ask the user what couple ROI / item he want to display"""
- dialog = _GetROIItemCoupleDialog(parent=self, plot=self._plot,
- rois=self._rois)
+ dialog = _GetROIItemCoupleDialog(parent=self, plot=self._plot, rois=self._rois)
if dialog.exec():
self.addItem(roi=dialog.getROI(), plotItem=dialog.getItem())
@@ -756,7 +785,7 @@ class ROIStatsWidget(qt.QMainWindow):
def _removeCurrentRow(self):
def is1DKind(kind):
- if kind in ('curve', 'histogram', 'scatter'):
+ if kind in ("curve", "histogram", "scatter"):
return True
else:
return False
@@ -769,12 +798,10 @@ class ROIStatsWidget(qt.QMainWindow):
roi_kind = ROI if is1DKind(item_kind) else RegionOfInterest
roi = self._statsROITable._getRoi(kind=roi_kind, name=roi_name)
if roi is None:
- _logger.warning('failed to retrieve the roi you want to remove')
+ _logger.warning("failed to retrieve the roi you want to remove")
return False
- plot_item = self._statsROITable._getPlotItem(kind=item_kind,
- legend=item_legend)
+ plot_item = self._statsROITable._getPlotItem(kind=item_kind, legend=item_legend)
if plot_item is None:
- _logger.warning('failed to retrieve the plot item you want to'
- 'remove')
+ _logger.warning("failed to retrieve the plot item you want to" "remove")
return False
return self.removeItem(plotItem=plot_item, roi=roi)
diff --git a/src/silx/gui/plot/ScatterMaskToolsWidget.py b/src/silx/gui/plot/ScatterMaskToolsWidget.py
index c242dfc..300f3a6 100644
--- a/src/silx/gui/plot/ScatterMaskToolsWidget.py
+++ b/src/silx/gui/plot/ScatterMaskToolsWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,8 +30,6 @@ This widget is meant to work with a modified :class:`silx.gui.plot.PlotWidget`
- :class:`ScatterMaskToolsDockWidget`: DockWidget to integrate in :class:`PlotWindow`
"""
-from __future__ import division
-
__authors__ = ["P. Knobel"]
__license__ = "MIT"
__date__ = "15/02/2019"
@@ -57,8 +54,8 @@ _logger = logging.getLogger(__name__)
class ScatterMask(BaseMask):
- """A 1D mask for scatter data.
- """
+ """A 1D mask for scatter data."""
+
def __init__(self, scatter=None):
"""
@@ -79,7 +76,7 @@ class ScatterMask(BaseMask):
return self._dataItem.getValueData(copy=False)
def save(self, filename, kind):
- if kind == 'npy':
+ if kind == "npy":
try:
numpy.save(filename, self.getMask(copy=False))
except IOError:
@@ -119,8 +116,9 @@ class ScatterMask(BaseMask):
x, y = self._getXY()
# TODO: this could be optimized if necessary
- indices_in_polygon = [idx for idx in range(len(x)) if
- polygon.is_inside(y[idx], x[idx])]
+ indices_in_polygon = [
+ idx for idx in range(len(x)) if polygon.is_inside(y[idx], x[idx])
+ ]
self.updatePoints(level, indices_in_polygon, mask)
@@ -134,10 +132,7 @@ class ScatterMask(BaseMask):
:param float width:
:param bool mask: True to mask (default), False to unmask.
"""
- vertices = [(y, x),
- (y + height, x),
- (y + height, x + width),
- (y, x + width)]
+ vertices = [(y, x), (y + height, x), (y + height, x + width), (y, x + width)]
self.updatePolygon(level, vertices, mask)
def updateDisk(self, level, cy, cx, radius, mask=True):
@@ -150,7 +145,7 @@ class ScatterMask(BaseMask):
:param bool mask: True to mask (default), False to unmask.
"""
x, y = self._getXY()
- stencil = (y - cy)**2 + (x - cx)**2 < radius**2
+ stencil = (y - cy) ** 2 + (x - cx) ** 2 < radius**2
self.updateStencil(level, stencil, mask)
def updateEllipse(self, level, crow, ccol, radius_r, radius_c, mask=True):
@@ -163,8 +158,12 @@ class ScatterMask(BaseMask):
:param float radius_c: Radius of the ellipse in the column
:param bool mask: True to mask (default), False to unmask.
"""
+
def is_inside(px, py):
- return (px - ccol)**2 / radius_c**2 + (py - crow)**2 / radius_r**2 <= 1.0
+ return (px - ccol) ** 2 / radius_c**2 + (
+ py - crow
+ ) ** 2 / radius_r**2 <= 1.0
+
x, y = self._getXY()
indices_inside = [idx for idx in range(len(x)) if is_inside(x[idx], y[idx])]
self.updatePoints(level, indices_inside, mask)
@@ -183,13 +182,15 @@ class ScatterMask(BaseMask):
"""
# theta is the angle between the horizontal and the line
theta = math.atan((y1 - y0) / (x1 - x0)) if x1 - x0 else 0
- w_over_2_sin_theta = width / 2. * math.sin(theta)
- w_over_2_cos_theta = width / 2. * math.cos(theta)
-
- vertices = [(y0 - w_over_2_cos_theta, x0 + w_over_2_sin_theta),
- (y0 + w_over_2_cos_theta, x0 - w_over_2_sin_theta),
- (y1 + w_over_2_cos_theta, x1 - w_over_2_sin_theta),
- (y1 - w_over_2_cos_theta, x1 + w_over_2_sin_theta)]
+ w_over_2_sin_theta = width / 2.0 * math.sin(theta)
+ w_over_2_cos_theta = width / 2.0 * math.cos(theta)
+
+ vertices = [
+ (y0 - w_over_2_cos_theta, x0 + w_over_2_sin_theta),
+ (y0 + w_over_2_cos_theta, x0 - w_over_2_sin_theta),
+ (y1 + w_over_2_cos_theta, x1 - w_over_2_sin_theta),
+ (y1 - w_over_2_cos_theta, x1 + w_over_2_sin_theta),
+ ]
self.updatePolygon(level, vertices, mask)
@@ -199,8 +200,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
:class:`PlotWidget`."""
def __init__(self, parent=None, plot=None):
- super(ScatterMaskToolsWidget, self).__init__(parent, plot,
- mask=ScatterMask())
+ super(ScatterMaskToolsWidget, self).__init__(parent, plot, mask=ScatterMask())
self._z = 2 # Mask layer in plot
self._data_scatter = None
"""plot Scatter item for data"""
@@ -226,7 +226,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
"""
if self._data_scatter is None:
# this can happen if the mask tools widget has never been shown
- self._data_scatter = self.plot._getActiveItem(kind="scatter")
+ self._data_scatter = self.plot.getActiveScatter()
if self._data_scatter is None:
return None
self._adjustColorAndBrushSize(self._data_scatter)
@@ -237,8 +237,10 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
mask = numpy.array(mask, copy=False, dtype=numpy.uint8)
- if self._data_scatter.getXData(copy=False).shape == (0,) \
- or mask.shape == self._data_scatter.getXData(copy=False).shape:
+ if (
+ self._data_scatter.getXData(copy=False).shape == (0,)
+ or mask.shape == self._data_scatter.getXData(copy=False).shape
+ ):
self._mask.setMask(mask, copy=copy)
self._mask.commit()
return mask.shape
@@ -251,25 +253,28 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
"""Update mask image in plot"""
mask = self.getSelectionMask(copy=False)
if mask is not None:
- self.plot.addScatter(self._data_scatter.getXData(),
- self._data_scatter.getYData(),
- mask,
- legend=self._maskName,
- colormap=self._colormap,
- z=self._z)
- self._mask_scatter = self.plot._getItem(kind="scatter",
- legend=self._maskName)
- self._mask_scatter.setSymbolSize(
- self._data_scatter.getSymbolSize() + 2.0)
+ self.plot.addScatter(
+ self._data_scatter.getXData(),
+ self._data_scatter.getYData(),
+ mask,
+ legend=self._maskName,
+ colormap=self._colormap,
+ z=self._z,
+ )
+ self._mask_scatter = self.plot._getItem(
+ kind="scatter", legend=self._maskName
+ )
+ self._mask_scatter.setSymbolSize(self._data_scatter.getSymbolSize() + 2.0)
self._mask_scatter.sigItemChanged.connect(self.__maskScatterChanged)
- elif self.plot._getItem(kind="scatter",
- legend=self._maskName) is not None:
- self.plot.remove(self._maskName, kind='scatter')
+ elif self.plot._getItem(kind="scatter", legend=self._maskName) is not None:
+ self.plot.remove(self._maskName, kind="scatter")
def __maskScatterChanged(self, event):
"""Handles update of mask scatter"""
- if (event is ItemChangedType.VISUALIZATION_MODE and
- self._mask_scatter is not None):
+ if (
+ event is ItemChangedType.VISUALIZATION_MODE
+ and self._mask_scatter is not None
+ ):
self._mask_scatter.setVisualization(Scatter.Visualization.POINTS)
# track widget visibility and plot active image changes
@@ -277,10 +282,11 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
def showEvent(self, event):
try:
self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
+ self._activeScatterChangedAfterCare
+ )
except (RuntimeError, TypeError):
pass
- self._activeScatterChanged(None, None) # Init mask + enable/disable widget
+ self._activeScatterChanged(None, None) # Init mask + enable/disable widget
self.plot.sigActiveScatterChanged.connect(self._activeScatterChanged)
def hideEvent(self, event):
@@ -290,19 +296,23 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
self.plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged)
except (RuntimeError, TypeError):
_logger.info(sys.exc_info()[1])
- if not self.browseAction.isChecked():
- self.browseAction.trigger() # Disable drawing tool
+
+ if self.isMaskInteractionActivated():
+ # Disable drawing tool
+ self.plot.resetInteractiveMode()
if self.getSelectionMask(copy=False) is not None:
self.plot.sigActiveScatterChanged.connect(
- self._activeScatterChangedAfterCare)
+ self._activeScatterChangedAfterCare
+ )
def _adjustColorAndBrushSize(self, activeScatter):
colormap = activeScatter.getColormap()
- self._defaultOverlayColor = rgba(cursorColorForColormap(colormap['name']))
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._defaultOverlayColor = rgba(cursorColorForColormap(colormap["name"]))
+ self._setMaskColors(
+ self.levelSpinBox.value(),
+ self.transparencySlider.value() / self.transparencySlider.maximum(),
+ )
self._z = activeScatter.getZValue() + 1
self._data_scatter = activeScatter
@@ -324,25 +334,30 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
removed, otherwise it is adjusted to z.
"""
# check that content changed was the active scatter
- activeScatter = self.plot._getActiveItem(kind="scatter")
+ activeScatter = self.plot.getActiveScatter()
if activeScatter is None or activeScatter.getName() == self._maskName:
# No active scatter or active scatter is the mask...
self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
+ self._activeScatterChangedAfterCare
+ )
self._data_extent = None
self._data_scatter = None
else:
self._adjustColorAndBrushSize(activeScatter)
- if self._data_scatter.getXData(copy=False).shape != self._mask.getMask(copy=False).shape:
+ if (
+ self._data_scatter.getXData(copy=False).shape
+ != self._mask.getMask(copy=False).shape
+ ):
# scatter has not the same size, remove mask and stop listening
if self.plot._getItem(kind="scatter", legend=self._maskName):
- self.plot.remove(self._maskName, kind='scatter')
+ self.plot.remove(self._maskName, kind="scatter")
self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
+ self._activeScatterChangedAfterCare
+ )
self._data_extent = None
self._data_scatter = None
@@ -353,7 +368,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
def _activeScatterChanged(self, previous, next):
"""Update widget and mask according to active scatter changes"""
- activeScatter = self.plot._getActiveItem(kind="scatter")
+ activeScatter = self.plot.getActiveScatter()
if activeScatter is None or activeScatter.getName() == self._maskName:
# No active scatter or active scatter is the mask...
@@ -369,7 +384,10 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
self._adjustColorAndBrushSize(activeScatter)
self._mask.setDataItem(self._data_scatter)
- if self._data_scatter.getXData(copy=False).shape != self._mask.getMask(copy=False).shape:
+ if (
+ self._data_scatter.getXData(copy=False).shape
+ != self._mask.getMask(copy=False).shape
+ ):
self._mask.reset(self._data_scatter.getXData(copy=False).shape)
self._mask.commit()
else:
@@ -396,16 +414,14 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
except IOError:
_logger.error("Can't load filename '%s'", filename)
_logger.debug("Backtrace", exc_info=True)
- raise RuntimeError('File "%s" is not a numpy file.',
- filename)
+ raise RuntimeError('File "%s" is not a numpy file.', filename)
elif extension in ["txt", "csv"]:
try:
mask = numpy.loadtxt(filename)
except IOError:
_logger.error("Can't load filename '%s'", filename)
_logger.debug("Backtrace", exc_info=True)
- raise RuntimeError('File "%s" is not a numpy txt file.',
- filename)
+ raise RuntimeError('File "%s" is not a numpy txt file.', filename)
else:
msg = "Extension '%s' is not supported."
raise RuntimeError(msg % extension)
@@ -418,8 +434,8 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
dialog.setWindowTitle("Load Mask")
dialog.setModal(1)
filters = [
- 'NumPy binary file (*.npy)',
- 'CSV text file (*.csv)',
+ "NumPy binary file (*.npy)",
+ "CSV text file (*.csv)",
]
dialog.setNameFilters(filters)
dialog.setFileMode(qt.QFileDialog.ExistingFile)
@@ -455,8 +471,8 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
dialog.setWindowTitle("Save Mask")
dialog.setModal(1)
filters = [
- 'NumPy binary file (*.npy)',
- 'CSV text file (*.csv)',
+ "NumPy binary file (*.npy)",
+ "CSV text file (*.csv)",
]
dialog.setNameFilters(filters)
dialog.setFileMode(qt.QFileDialog.AnyFile)
@@ -486,8 +502,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
strerror = e.strerror
else:
strerror = sys.exc_info()[1]
- msg.setText("Cannot save.\n"
- "Input Output Error: %s" % strerror)
+ msg.setText("Cannot save.\n" "Input Output Error: %s" % strerror)
msg.exec()
return
@@ -510,8 +525,7 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
def resetSelectionMask(self):
"""Reset the mask"""
- self._mask.reset(
- shape=self._data_scatter.getXData(copy=False).shape)
+ self._mask.reset(shape=self._data_scatter.getXData(copy=False).shape)
self._mask.commit()
def _getPencilWidth(self):
@@ -526,8 +540,10 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
def _plotDrawEvent(self, event):
"""Handle draw events from the plot"""
- if (self._drawingMode is None or
- event['event'] not in ('drawingProgress', 'drawingFinished')):
+ if self._drawingMode is None or event["event"] not in (
+ "drawingProgress",
+ "drawingFinished",
+ ):
return
if not len(self._data_scatter.getXData(copy=False)):
@@ -535,40 +551,42 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
level = self.levelSpinBox.value()
- if self._drawingMode == 'rectangle':
- if event['event'] == 'drawingFinished':
+ if self._drawingMode == "rectangle":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
self._mask.updateRectangle(
level,
- y=event['y'],
- x=event['x'],
- height=abs(event['height']),
- width=abs(event['width']),
- mask=doMask)
+ y=event["y"],
+ x=event["x"],
+ height=abs(event["height"]),
+ width=abs(event["width"]),
+ mask=doMask,
+ )
self._mask.commit()
- elif self._drawingMode == 'ellipse':
- if event['event'] == 'drawingFinished':
+ elif self._drawingMode == "ellipse":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
- center = event['points'][0]
- size = event['points'][1]
- self._mask.updateEllipse(level, center[1], center[0],
- size[1], size[0], doMask)
+ center = event["points"][0]
+ size = event["points"][1]
+ self._mask.updateEllipse(
+ level, center[1], center[0], size[1], size[0], doMask
+ )
self._mask.commit()
- elif self._drawingMode == 'polygon':
- if event['event'] == 'drawingFinished':
+ elif self._drawingMode == "polygon":
+ if event["event"] == "drawingFinished":
doMask = self._isMasking()
- vertices = event['points']
+ vertices = event["points"]
vertices = vertices[:, (1, 0)] # (y, x)
self._mask.updatePolygon(level, vertices, doMask)
self._mask.commit()
- elif self._drawingMode == 'pencil':
+ elif self._drawingMode == "pencil":
doMask = self._isMasking()
# convert from plot to array coords
- x, y = event['points'][-1]
+ x, y = event["points"][-1]
brushSize = self._getPencilWidth()
@@ -577,15 +595,18 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
# Draw the line
self._mask.updateLine(
level,
- self._lastPencilPos[0], self._lastPencilPos[1],
- y, x,
+ self._lastPencilPos[0],
+ self._lastPencilPos[1],
+ y,
+ x,
brushSize,
- doMask)
+ doMask,
+ )
# Draw the very first, or last point
- self._mask.updateDisk(level, y, x, brushSize / 2., doMask)
+ self._mask.updateDisk(level, y, x, brushSize / 2.0, doMask)
- if event['event'] == 'drawingFinished':
+ if event["event"] == "drawingFinished":
self._mask.commit()
self._lastPencilPos = None
else:
@@ -598,11 +619,11 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
if self._data_scatter is not None:
# Update thresholds according to colormap
colormap = self._data_scatter.getColormap()
- if colormap['autoscale']:
+ if colormap["autoscale"]:
min_ = numpy.nanmin(self._data_scatter.getValueData(copy=False))
max_ = numpy.nanmax(self._data_scatter.getValueData(copy=False))
else:
- min_, max_ = colormap['vmin'], colormap['vmax']
+ min_, max_ = colormap["vmin"], colormap["vmax"]
self.minLineEdit.setText(str(min_))
self.maxLineEdit.setText(str(max_))
@@ -616,6 +637,7 @@ class ScatterMaskToolsDockWidget(BaseMaskToolsDockWidget):
:param plot: The PlotWidget this widget is operating on
:paran str name: The title of this widget
"""
- def __init__(self, parent=None, plot=None, name='Mask'):
+
+ def __init__(self, parent=None, plot=None, name="Mask"):
widget = ScatterMaskToolsWidget(plot=plot)
super(ScatterMaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/src/silx/gui/plot/ScatterView.py b/src/silx/gui/plot/ScatterView.py
index d3fd2e0..06475e3 100644
--- a/src/silx/gui/plot/ScatterView.py
+++ b/src/silx/gui/plot/ScatterView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
@@ -64,7 +63,7 @@ class ScatterView(qt.QMainWindow):
:type backend: Union[str,~silx.gui.plot.backends.BackendBase.BackendBase]
"""
- _SCATTER_LEGEND = ' '
+ _SCATTER_LEGEND = " "
"""Legend used for the scatter item"""
def __init__(self, parent=None, backend=None):
@@ -73,7 +72,7 @@ class ScatterView(qt.QMainWindow):
# behave as a widget
self.setWindowFlags(qt.Qt.Widget)
else:
- self.setWindowTitle('ScatterView')
+ self.setWindowTitle("ScatterView")
# Create plot widget
plot = PlotWidget(parent=self, backend=backend)
@@ -94,10 +93,13 @@ class ScatterView(qt.QMainWindow):
self.__pickingCache = None
self._positionInfo = tools.PositionInfo(
plot=plot,
- converters=(('X', WeakMethodProxy(self._getPickedX)),
- ('Y', WeakMethodProxy(self._getPickedY)),
- ('Data', WeakMethodProxy(self._getPickedValue)),
- ('Index', WeakMethodProxy(self._getPickedIndex))))
+ converters=(
+ ("X", WeakMethodProxy(self._getPickedX)),
+ ("Y", WeakMethodProxy(self._getPickedY)),
+ ("Data", WeakMethodProxy(self._getPickedValue)),
+ ("Index", WeakMethodProxy(self._getPickedIndex)),
+ ),
+ )
# Combine plot, position info and colorbar into central widget
gridLayout = qt.QGridLayout()
@@ -115,23 +117,25 @@ class ScatterView(qt.QMainWindow):
# Create mask tool dock widget
self._maskToolsWidget = ScatterMaskToolsWidget(parent=self, plot=plot)
self._maskDock = BoxLayoutDockWidget()
- self._maskDock.setWindowTitle('Scatter Mask')
+ self._maskDock.setWindowTitle("Scatter Mask")
self._maskDock.setWidget(self._maskToolsWidget)
self._maskDock.setVisible(False)
self.addDockWidget(qt.Qt.BottomDockWidgetArea, self._maskDock)
self._maskAction = self._maskDock.toggleViewAction()
- self._maskAction.setIcon(icons.getQIcon('image-mask'))
+ self._maskAction.setIcon(icons.getQIcon("image-mask"))
self._maskAction.setToolTip("Display/hide mask tools")
- self._intensityHistoAction = actions_histogram.PixelIntensitiesHistoAction(plot=plot, parent=self)
+ self._intensityHistoAction = actions_histogram.PixelIntensitiesHistoAction(
+ plot=plot, parent=self
+ )
# Create toolbars
self._interactiveModeToolBar = tools.InteractiveModeToolBar(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
- self._scatterToolBar = tools.ScatterToolBar(
- parent=self, plot=plot)
+ self._scatterToolBar = tools.ScatterToolBar(parent=self, plot=plot)
self._scatterToolBar.addAction(self._maskAction)
self._scatterToolBar.addAction(self._intensityHistoAction)
@@ -140,15 +144,16 @@ class ScatterView(qt.QMainWindow):
self._outputToolBar = tools.OutputToolBar(parent=self, plot=plot)
# Activate shortcuts in PlotWindow widget:
- for toolbar in (self._interactiveModeToolBar,
- self._scatterToolBar,
- self._profileToolBar,
- self._outputToolBar):
+ for toolbar in (
+ self._interactiveModeToolBar,
+ self._scatterToolBar,
+ self._profileToolBar,
+ self._outputToolBar,
+ ):
self.addToolBar(toolbar)
for action in toolbar.actions():
self.addAction(action)
-
def __createEmptyScatter(self):
"""Create an empty scatter item that is used to display the data
@@ -156,8 +161,7 @@ class ScatterView(qt.QMainWindow):
"""
plot = self.getPlotWidget()
plot.addScatter(x=(), y=(), value=(), legend=self._SCATTER_LEGEND)
- scatter = plot._getItem(
- kind='scatter', legend=self._SCATTER_LEGEND)
+ scatter = plot._getItem(kind="scatter", legend=self._SCATTER_LEGEND)
# Profile is not selectable,
# so it does not interfere with profile interaction
scatter._setSelectable(False)
@@ -181,16 +185,24 @@ class ScatterView(qt.QMainWindow):
if pixelPos is not None:
# Start from top-most item
result = plot._pickTopMost(
- pixelPos[0], pixelPos[1],
- lambda item: isinstance(item, items.Scatter))
+ pixelPos[0],
+ pixelPos[1],
+ lambda item: isinstance(item, items.Scatter),
+ )
if result is not None:
item = result.getItem()
- if item.getVisualization() is items.Scatter.Visualization.BINNED_STATISTIC:
+ if (
+ item.getVisualization()
+ is items.Scatter.Visualization.BINNED_STATISTIC
+ ):
# Get highest index of closest points
selected = result.getIndices(copy=False)[::-1]
- dataIndex = selected[numpy.argmin(
- (item.getXData(copy=False)[selected] - x)**2 +
- (item.getYData(copy=False)[selected] - y)**2)]
+ dataIndex = selected[
+ numpy.argmin(
+ (item.getXData(copy=False)[selected] - x) ** 2
+ + (item.getYData(copy=False)[selected] - y) ** 2
+ )
+ ]
else:
# Get last index
# with matplotlib it should be the top-most point
@@ -199,7 +211,8 @@ class ScatterView(qt.QMainWindow):
dataIndex,
item.getXData(copy=False)[dataIndex],
item.getYData(copy=False)[dataIndex],
- item.getValueData(copy=False)[dataIndex])
+ item.getValueData(copy=False)[dataIndex],
+ )
return self.__pickingCache
@@ -211,7 +224,7 @@ class ScatterView(qt.QMainWindow):
:return: The data index at that point or '-'
"""
picking = self._pickScatterData(x, y)
- return '-' if picking is None else picking[0]
+ return "-" if picking is None else picking[0]
def _getPickedX(self, x, y):
"""Returns X position snapped to scatter plot when close enough
@@ -241,7 +254,7 @@ class ScatterView(qt.QMainWindow):
:return: The data value at that point or '-'
"""
picking = self._pickScatterData(x, y)
- return '-' if picking is None else picking[3]
+ return "-" if picking is None else picking[3]
def _mouseInPlotArea(self, x, y):
"""Clip mouse coordinates to plot area coordinates
@@ -345,7 +358,7 @@ class ScatterView(qt.QMainWindow):
:param yerror: Values with the uncertainties on the y values
:type yerror: A float, or a numpy.ndarray of float32. See xerror.
:param alpha: Values with the transparency (between 0 and 1)
- :type alpha: A float, or a numpy.ndarray of float32
+ :type alpha: A float, or a numpy.ndarray of float32
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
"""
@@ -354,7 +367,8 @@ class ScatterView(qt.QMainWindow):
value = () if value is None else value
self.getScatterItem().setData(
- x=x, y=y, value=value, xerror=xerror, yerror=yerror, alpha=alpha, copy=copy)
+ x=x, y=y, value=value, xerror=xerror, yerror=yerror, alpha=alpha, copy=copy
+ )
@docstring(items.Scatter)
def getData(self, *args, **kwargs):
@@ -368,7 +382,7 @@ class ScatterView(qt.QMainWindow):
:rtype: ~silx.gui.plot.items.Scatter
"""
plot = self.getPlotWidget()
- scatter = plot._getItem(kind='scatter', legend=self._SCATTER_LEGEND)
+ scatter = plot._getItem(kind="scatter", legend=self._SCATTER_LEGEND)
if scatter is None: # Resilient to call to PlotWidget API (e.g., clear)
scatter = self.__createEmptyScatter()
return scatter
diff --git a/src/silx/gui/plot/StackView.py b/src/silx/gui/plot/StackView.py
index 56793d7..36560fd 100644
--- a/src/silx/gui/plot/StackView.py
+++ b/src/silx/gui/plot/StackView.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -57,7 +56,7 @@ Example::
sv = StackViewMainWindow()
- sv.setColormap("jet", autoscale=True)
+ sv.setColormap("viridis", vmin=-4, vmax=4)
sv.setStack(mystack)
sv.setLabels(["1st dim (0-99)", "2nd dim (0-199)",
"3rd dim (0-299)"])
@@ -85,15 +84,11 @@ from .tools import LimitsToolBar
from .Profile import Profile3DToolBar
from ..widgets.FrameBrowser import HorizontalSliderWithBrowser
-from silx.gui.plot.actions import control as actions_control
from silx.gui.plot.actions import io as silx_io
from silx.io.nxdata import save_NXdata
from silx.utils.array_like import DatasetView, ListOfImages
from silx.math import calibration
-from silx.utils.deprecation import deprecated_warning
-from silx.utils.deprecation import deprecated
-import h5py
from silx.io.utils import is_dataset
_logger = logging.getLogger(__name__)
@@ -131,6 +126,7 @@ class StackView(qt.QMainWindow):
See :class:`silx.gui.plot.PlotTools.PositionInfo`.
:param bool mask: Toggle visibilty of mask action.
"""
+
# Qt signals
valueChanged = qt.Signal(object, object, object)
"""Signals that the data value under the cursor has changed.
@@ -164,20 +160,34 @@ class StackView(qt.QMainWindow):
This signal provides the current frame number.
"""
- IMAGE_STACK_FILTER_NXDATA = 'Stack of images as NXdata (%s)' % silx_io._NEXUS_HDF5_EXT_STR
-
+ IMAGE_STACK_FILTER_NXDATA = (
+ "Stack of images as NXdata (%s)" % silx_io._NEXUS_HDF5_EXT_STR
+ )
- def __init__(self, parent=None, resetzoom=True, backend=None,
- autoScale=False, logScale=False, grid=False,
- colormap=True, aspectRatio=True, yinverted=True,
- copy=True, save=True, print_=True, control=False,
- position=None, mask=True):
+ def __init__(
+ self,
+ parent=None,
+ resetzoom=True,
+ backend=None,
+ autoScale=False,
+ logScale=False,
+ grid=False,
+ colormap=True,
+ aspectRatio=True,
+ yinverted=True,
+ copy=True,
+ save=True,
+ print_=True,
+ control=False,
+ position=None,
+ mask=True,
+ ):
qt.QMainWindow.__init__(self, parent)
if parent is not None:
# behave as a widget
self.setWindowFlags(qt.Qt.Widget)
else:
- self.setWindowTitle('StackView')
+ self.setWindowTitle("StackView")
self._stack = None
"""Loaded stack, as a 3D array, a 3D dataset or a list of 2D arrays."""
@@ -189,14 +199,10 @@ class StackView(qt.QMainWindow):
self._stackItem = ImageStack()
"""Hold the item displaying the stack"""
- imageLegend = '__StackView__image' + str(id(self))
+ imageLegend = "__StackView__image" + str(id(self))
self._stackItem.setName(imageLegend)
- self.__autoscaleCmap = False
- """Flag to disable/enable colormap auto-scaling
- based on the min/max values of the entire 3D volume"""
- self.__dimensionsLabels = ["Dimension 0", "Dimension 1",
- "Dimension 2"]
+ self.__dimensionsLabels = ["Dimension 0", "Dimension 1", "Dimension 2"]
"""These labels are displayed on the X and Y axes.
:meth:`setLabels` updates this attribute."""
@@ -207,38 +213,56 @@ class StackView(qt.QMainWindow):
"""Function returning the plot title based on the frame index.
It can be set to a custom function using :meth:`setTitleCallback`"""
- self.calibrations3D = (calibration.NoCalibration(),
- calibration.NoCalibration(),
- calibration.NoCalibration())
+ self.calibrations3D = (
+ calibration.NoCalibration(),
+ calibration.NoCalibration(),
+ calibration.NoCalibration(),
+ )
central_widget = qt.QWidget(self)
- self._plot = PlotWindow(parent=central_widget, backend=backend,
- resetzoom=resetzoom, autoScale=autoScale,
- logScale=logScale, grid=grid,
- curveStyle=False, colormap=colormap,
- aspectRatio=aspectRatio, yInverted=yinverted,
- copy=copy, save=save, print_=print_,
- control=control, position=position,
- roi=False, mask=mask)
+ self._plot = PlotWindow(
+ parent=central_widget,
+ backend=backend,
+ resetzoom=resetzoom,
+ autoScale=autoScale,
+ logScale=logScale,
+ grid=grid,
+ curveStyle=False,
+ colormap=colormap,
+ aspectRatio=aspectRatio,
+ yInverted=yinverted,
+ copy=copy,
+ save=save,
+ print_=print_,
+ control=control,
+ position=position,
+ roi=False,
+ mask=mask,
+ )
self._plot.addItem(self._stackItem)
self._plot.getIntensityHistogramAction().setVisible(True)
self.sigInteractiveModeChanged = self._plot.sigInteractiveModeChanged
self.sigActiveImageChanged = self._plot.sigActiveImageChanged
self.sigPlotSignal = self._plot.sigPlotSignal
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
+ if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
self._plot.getYAxis().setInverted(True)
- self._addColorBarAction()
+ self._plot.getColorBarAction().setVisible(True)
+ self._plot.getColorBarWidget().setVisible(True)
- self._profileToolBar = Profile3DToolBar(parent=self._plot,
- stackview=self)
+ self._profileToolBar = Profile3DToolBar(parent=self._plot, stackview=self)
self._plot.addToolBar(self._profileToolBar)
- self._plot.getXAxis().setLabel('Columns')
- self._plot.getYAxis().setLabel('Rows')
+ self._plot.getXAxis().setLabel("Columns")
+ self._plot.getYAxis().setLabel("Rows")
self._plot.sigPlotSignal.connect(self._plotCallback)
- self._plot.getSaveAction().setFileFilter('image', self.IMAGE_STACK_FILTER_NXDATA, func=self._saveImageStack, appendToFile=True)
+ self._plot.getSaveAction().setFileFilter(
+ "image",
+ self.IMAGE_STACK_FILTER_NXDATA,
+ func=self._saveImageStack,
+ appendToFile=True,
+ )
self.__planeSelection = PlanesWidget(self._plot)
self.__planeSelection.sigPlaneSelectionChanged.connect(self.setPerspective)
@@ -262,7 +286,8 @@ class StackView(qt.QMainWindow):
# clear profile lines when the perspective changes (plane browsed changed)
self.__planeSelection.sigPlaneSelectionChanged.connect(
- self._profileToolBar.clearProfile)
+ self._profileToolBar.clearProfile
+ )
def _saveImageStack(self, plot, filename, nameFilter):
"""Save all images from the stack into a volume.
@@ -274,30 +299,25 @@ class StackView(qt.QMainWindow):
:raises: ValueError if nameFilter is invalid
"""
if not nameFilter == self.IMAGE_STACK_FILTER_NXDATA:
- raise ValueError('Wrong callback')
- entryPath = silx_io.SaveAction._selectWriteableOutputGroup(filename, parent=self)
+ raise ValueError("Wrong callback")
+ entryPath = silx_io.SaveAction._selectWriteableOutputGroup(
+ filename, parent=self
+ )
if entryPath is None:
return False
- return save_NXdata(filename,
- nxentry_name=entryPath,
- signal=self.getStack(copy=False, returnNumpyArray=True)[0],
- signal_name="image_stack")
-
- def _addColorBarAction(self):
- self._plot.getColorBarWidget().setVisible(True)
- actions = self._plot.toolBar().actions()
- for index, action in enumerate(actions):
- if action is self._plot.getColormapAction():
- break
- self._colorbarAction = actions_control.ColorBarAction(self._plot, self._plot)
- self._plot.toolBar().insertAction(actions[index + 1], self._colorbarAction)
+ return save_NXdata(
+ filename,
+ nxentry_name=entryPath,
+ signal=self.getStack(copy=False, returnNumpyArray=True)[0],
+ signal_name="image_stack",
+ )
def _plotCallback(self, eventDict):
"""Callback for plot events.
Emit :attr:`valueChanged` signal, with (x, y, value) tuple of the
cursor location in the plot."""
- if eventDict['event'] == 'mouseMoved':
+ if eventDict["event"] == "mouseMoved":
activeImage = self.getActiveImage()
if activeImage is not None:
data = activeImage.getData()
@@ -306,15 +326,13 @@ class StackView(qt.QMainWindow):
# Get corresponding coordinate in image
origin = activeImage.getOrigin()
scale = activeImage.getScale()
- x = int((eventDict['x'] - origin[0]) / scale[0])
- y = int((eventDict['y'] - origin[1]) / scale[1])
+ x = int((eventDict["x"] - origin[0]) / scale[0])
+ y = int((eventDict["y"] - origin[1]) / scale[1])
if 0 <= x < width and 0 <= y < height:
- self.valueChanged.emit(float(x), float(y),
- data[y][x])
+ self.valueChanged.emit(float(x), float(y), data[y][x])
else:
- self.valueChanged.emit(float(x), float(y),
- None)
+ self.valueChanged.emit(float(x), float(y), None)
def getPerspective(self):
"""Returns the index of the dimension the stack is browsed with
@@ -338,8 +356,7 @@ class StackView(qt.QMainWindow):
return
else:
if perspective > 2 or perspective < 0:
- raise ValueError(
- "Perspective must be 0, 1 or 2, not %s" % perspective)
+ raise ValueError("Perspective must be 0, 1 or 2, not %s" % perspective)
self._perspective = int(perspective)
self.__createTransposedView()
@@ -347,20 +364,29 @@ class StackView(qt.QMainWindow):
self._plot.resetZoom()
self.__updatePlotLabels()
self._updateTitle()
- self._browser_label.setText("Image index (Dim%d):" %
- (self._first_stack_dimension + perspective))
+ self._browser_label.setText(
+ "Image index (Dim%d):" % (self._first_stack_dimension + perspective)
+ )
self.sigPlaneSelectionChanged.emit(perspective)
- self.sigStackChanged.emit(self._stack.size if
- self._stack is not None else 0)
- self.__planeSelection.sigPlaneSelectionChanged.disconnect(self.setPerspective)
+ self.sigStackChanged.emit(
+ self._stack.size if self._stack is not None else 0
+ )
+ self.__planeSelection.sigPlaneSelectionChanged.disconnect(
+ self.setPerspective
+ )
self.__planeSelection.setPerspective(self._perspective)
self.__planeSelection.sigPlaneSelectionChanged.connect(self.setPerspective)
def __updatePlotLabels(self):
"""Update plot axes labels depending on perspective"""
- y, x = (1, 2) if self._perspective == 0 else \
- (0, 2) if self._perspective == 1 else (0, 1)
+ y, x = (
+ (1, 2)
+ if self._perspective == 0
+ else (0, 2)
+ if self._perspective == 1
+ else (0, 1)
+ )
self.setGraphXLabel(self.__dimensionsLabels[x])
self.setGraphYLabel(self.__dimensionsLabels[y])
@@ -418,9 +444,11 @@ class StackView(qt.QMainWindow):
See setStack for parameter documentation
"""
if calibrations is None:
- self.calibrations3D = (calibration.NoCalibration(),
- calibration.NoCalibration(),
- calibration.NoCalibration())
+ self.calibrations3D = (
+ calibration.NoCalibration(),
+ calibration.NoCalibration(),
+ calibration.NoCalibration(),
+ )
else:
self.calibrations3D = []
for i, calib in enumerate(calibrations):
@@ -429,17 +457,20 @@ class StackView(qt.QMainWindow):
elif calib is None:
calib = calibration.NoCalibration()
elif not isinstance(calib, calibration.AbstractCalibration):
- raise TypeError("calibration must be a 2-tuple, None or" +
- " an instance of an AbstractCalibration " +
- "subclass")
+ raise TypeError(
+ "calibration must be a 2-tuple, None or"
+ + " an instance of an AbstractCalibration "
+ + "subclass"
+ )
elif not calib.is_affine():
_logger.warning(
- "Calibration for dimension %d is not linear, "
- "it will be ignored for scaling the graph axes.",
- i)
+ "Calibration for dimension %d is not linear, "
+ "it will be ignored for scaling the graph axes.",
+ i,
+ )
self.calibrations3D.append(calib)
- def getCalibrations(self, order='array'):
+ def getCalibrations(self, order="array"):
"""Returns currently used calibrations for each axis
Returned calibrations might differ from the ones that were set as
@@ -451,7 +482,7 @@ class StackView(qt.QMainWindow):
:return: Calibrations ordered depending on order
:rtype: List[~silx.math.calibration.AbstractCalibration]
"""
- assert order in ('array', 'axes')
+ assert order in ("array", "axes")
calibs = []
# filter out non-linear calibration for graph axes
@@ -460,11 +491,13 @@ class StackView(qt.QMainWindow):
calib = calibration.NoCalibration()
calibs.append(calib)
- if order == 'axes': # Move 'z' axis to the end
+ if order == "axes": # Move 'z' axis to the end
xy_dims = [d for d in (0, 1, 2) if d != self._perspective]
- calibs = [calibs[max(xy_dims)],
- calibs[min(xy_dims)],
- calibs[self._perspective]]
+ calibs = [
+ calibs[max(xy_dims)],
+ calibs[min(xy_dims)],
+ calibs[self._perspective],
+ ]
return tuple(calibs)
@@ -472,14 +505,14 @@ class StackView(qt.QMainWindow):
"""
:return: 2-tuple (XScale, YScale) for current image view
"""
- xcalib, ycalib, _zcalib = self.getCalibrations(order='axes')
+ xcalib, ycalib, _zcalib = self.getCalibrations(order="axes")
return xcalib.get_slope(), ycalib.get_slope()
def _getImageOrigin(self):
"""
:return: 2-tuple (XOrigin, YOrigin) for current image view
"""
- xcalib, ycalib, _zcalib = self.getCalibrations(order='axes')
+ xcalib, ycalib, _zcalib = self.getCalibrations(order="axes")
return xcalib(0), ycalib(0)
def _getImageZ(self, index):
@@ -487,7 +520,7 @@ class StackView(qt.QMainWindow):
:param idx: 0-based image index in the stack
:return: calibrated Z value corresponding to the image idx
"""
- _xcalib, _ycalib, zcalib = self.getCalibrations(order='axes')
+ _xcalib, _ycalib, zcalib = self.getCalibrations(order="axes")
return zcalib(index)
def _updateTitle(self):
@@ -534,8 +567,8 @@ class StackView(qt.QMainWindow):
assert len(img.shape) == 2
except AssertionError:
raise ValueError(
- "Stack must be a 3D array/dataset or a list of " +
- "2D arrays.")
+ "Stack must be a 3D array/dataset or a list of " + "2D arrays."
+ )
stack = ListOfImages(stack)
assert len(stack.shape) == 3, "data must be 3D"
@@ -548,9 +581,6 @@ class StackView(qt.QMainWindow):
perspective_changed = True
self.setPerspective(perspective)
- if self.__autoscaleCmap:
- self.scaleColormapRangeToStack()
-
# init plot
self._stackItem.setStackData(self.__transposed_view, 0, copy=False)
self._stackItem.setColormap(self.getColormap())
@@ -563,7 +593,7 @@ class StackView(qt.QMainWindow):
if exists is None:
self._plot.addItem(self._stackItem)
- self._plot.setActiveImage(self._stackItem.getName())
+ self._plot.setActiveImage(self._stackItem)
self.__updatePlotLabels()
self._updateTitle()
@@ -573,7 +603,7 @@ class StackView(qt.QMainWindow):
# enable and init browser
self._browser.setEnabled(True)
- if not perspective_changed: # avoid double signal (see self.setPerspective)
+ if not perspective_changed: # avoid double signal (see self.setPerspective)
self.sigStackChanged.emit(stack.size)
def getStack(self, copy=True, returnNumpyArray=False):
@@ -599,15 +629,15 @@ class StackView(qt.QMainWindow):
colormap = image.getColormap()
params = {
- 'info': image.getInfo(),
- 'origin': image.getOrigin(),
- 'scale': image.getScale(),
- 'z': image.getZValue(),
- 'selectable': image.isSelectable(),
- 'draggable': image.isDraggable(),
- 'colormap': colormap,
- 'xlabel': image.getXLabel(),
- 'ylabel': image.getYLabel(),
+ "info": image.getInfo(),
+ "origin": image.getOrigin(),
+ "scale": image.getScale(),
+ "z": image.getZValue(),
+ "selectable": image.isSelectable(),
+ "draggable": image.isDraggable(),
+ "colormap": colormap,
+ "xlabel": image.getXLabel(),
+ "ylabel": image.getYLabel(),
}
if returnNumpyArray or copy:
return numpy.array(self._stack, copy=copy), params
@@ -650,15 +680,15 @@ class StackView(qt.QMainWindow):
colormap = None
params = {
- 'info': image.getInfo(),
- 'origin': image.getOrigin(),
- 'scale': image.getScale(),
- 'z': image.getZValue(),
- 'selectable': image.isSelectable(),
- 'draggable': image.isDraggable(),
- 'colormap': colormap,
- 'xlabel': image.getXLabel(),
- 'ylabel': image.getYLabel(),
+ "info": image.getInfo(),
+ "origin": image.getOrigin(),
+ "scale": image.getScale(),
+ "z": image.getZValue(),
+ "selectable": image.isSelectable(),
+ "draggable": image.isDraggable(),
+ "colormap": colormap,
+ "xlabel": image.getXLabel(),
+ "ylabel": image.getYLabel(),
}
if returnNumpyArray or copy:
return numpy.array(self.__transposed_view, copy=copy), params
@@ -727,8 +757,8 @@ class StackView(qt.QMainWindow):
def clear(self):
"""Clear the widget:
- - clear the plot
- - clear the loaded data volume
+ - clear the plot
+ - clear the loaded data volume
"""
self._stack = None
self.__transposed_view = None
@@ -751,9 +781,11 @@ class StackView(qt.QMainWindow):
of the data volumes.
"""
- default_labels = ["Dimension %d" % self._first_stack_dimension,
- "Dimension %d" % (self._first_stack_dimension + 1),
- "Dimension %d" % (self._first_stack_dimension + 2)]
+ default_labels = [
+ "Dimension %d" % self._first_stack_dimension,
+ "Dimension %d" % (self._first_stack_dimension + 1),
+ "Dimension %d" % (self._first_stack_dimension + 2),
+ ]
if labels is None:
new_labels = default_labels
else:
@@ -800,8 +832,9 @@ class StackView(qt.QMainWindow):
vmin, vmax = colormap.getColormapRange(data=stack[0])
colormap.setVRange(vmin=vmin, vmax=vmax)
- def setColormap(self, colormap=None, normalization=None,
- autoscale=None, vmin=None, vmax=None, colors=None):
+ def setColormap(
+ self, colormap=None, normalization=None, vmin=None, vmax=None, colors=None
+ ):
"""Set the colormap and update active image.
Parameters that are not provided are taken from the current colormap.
@@ -827,59 +860,33 @@ class StackView(qt.QMainWindow):
Or a :class`.Colormap` object.
:type colormap: dict or str.
:param str normalization: Colormap mapping: 'linear' or 'log'.
- :param bool autoscale: Whether to use autoscale or [vmin, vmax] range.
- Default value of autoscale is False. This option is not compatible
- with h5py datasets.
- :param float vmin: The minimum value of the range to use if
- 'autoscale' is False.
- :param float vmax: The maximum value of the range to use if
- 'autoscale' is False.
+ :param float vmin: The minimum value of the range to use.
+ :param float vmax: The maximum value of the range to use.
:param numpy.ndarray colors: Only used if name is None.
Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
"""
# if is a colormap object or a dictionary
if isinstance(colormap, Colormap) or isinstance(colormap, dict):
# Support colormap parameter as a dict
- errmsg = "If colormap is provided as a Colormap object, all other parameters"
+ errmsg = (
+ "If colormap is provided as a Colormap object, all other parameters"
+ )
errmsg += " must not be specified when calling setColormap"
assert normalization is None, errmsg
- assert autoscale is None, errmsg
assert vmin is None, errmsg
assert vmax is None, errmsg
assert colors is None, errmsg
- if isinstance(colormap, dict):
- reason = 'colormap parameter should now be an object'
- replacement = 'Colormap()'
- since_version = '0.6'
- deprecated_warning(type_='function',
- name='setColormap',
- reason=reason,
- replacement=replacement,
- since_version=since_version)
- _colormap = Colormap._fromDict(colormap)
- else:
- _colormap = colormap
+ _colormap = colormap
else:
- norm = normalization if normalization is not None else 'linear'
- name = colormap if colormap is not None else 'gray'
- _colormap = Colormap(name=name,
- normalization=norm,
- vmin=vmin,
- vmax=vmax,
- colors=colors)
-
- if autoscale is not None:
- deprecated_warning(
- type_='function',
- name='setColormap',
- reason='autoscale argument is replaced by a method',
- replacement='scaleColormapRangeToStack',
- since_version='0.14')
- self.__autoscaleCmap = bool(autoscale)
+ norm = normalization if normalization is not None else "linear"
+ name = colormap if colormap is not None else "gray"
+ _colormap = Colormap(
+ name=name, normalization=norm, vmin=vmin, vmax=vmax, colors=colors
+ )
cursorColor = cursorColorForColormap(_colormap.getName())
- self._plot.setInteractiveMode('zoom', color=cursorColor)
+ self._plot.setInteractiveMode("zoom", color=cursorColor)
self._plot.setDefaultColormap(_colormap)
@@ -888,16 +895,6 @@ class StackView(qt.QMainWindow):
if isinstance(activeImage, items.ColormapMixIn):
activeImage.setColormap(self.getColormap())
- if self.__autoscaleCmap:
- # scaleColormapRangeToStack needs to be called **after**
- # setDefaultColormap so getColormap returns the right colormap
- self.scaleColormapRangeToStack()
-
-
- @deprecated(replacement="getPlotWidget", since_version="0.13")
- def getPlot(self):
- return self.getPlotWidget()
-
def getPlotWidget(self):
"""Return the :class:`PlotWidget`.
@@ -921,13 +918,11 @@ class StackView(qt.QMainWindow):
# proxies to PlotWidget or PlotWindow methods
def getProfileToolbar(self):
- """Profile tools attached to this plot
- """
+ """Profile tools attached to this plot"""
return self._profileToolBar
def getGraphTitle(self):
- """Return the plot main title as a str.
- """
+ """Return the plot main title as a str."""
return self._plot.getGraphTitle()
def setGraphTitle(self, title=""):
@@ -938,8 +933,7 @@ class StackView(qt.QMainWindow):
return self._plot.setGraphTitle(title)
def getGraphXLabel(self):
- """Return the current horizontal axis label as a str.
- """
+ """Return the current horizontal axis label as a str."""
return self._plot.getXAxis().getLabel()
def setGraphXLabel(self, label=None):
@@ -951,14 +945,14 @@ class StackView(qt.QMainWindow):
label = self.__dimensionsLabels[1 if self._perspective == 2 else 2]
self._plot.getXAxis().setLabel(label)
- def getGraphYLabel(self, axis='left'):
+ def getGraphYLabel(self, axis="left"):
"""Return the current vertical axis label as a str.
:param str axis: The Y axis for which to get the label (left or right)
"""
return self._plot.getYAxis().getLabel(axis)
- def setGraphYLabel(self, label=None, axis='left'):
+ def setGraphYLabel(self, label=None, axis="left"):
"""Set the vertical axis label on the plot.
:param str label: The Y axis label
@@ -1042,8 +1036,7 @@ class StackView(qt.QMainWindow):
# kind of private methods, but needed by Profile
def getActiveImage(self, just_legend=False):
- """Returns the stack image object.
- """
+ """Returns the stack image object."""
if just_legend:
return self._stackItem.getName()
return self._stackItem
@@ -1056,10 +1049,9 @@ class StackView(qt.QMainWindow):
:rtype: QAction
"""
- return self._colorbarAction
+ return self._plot.getColorBarAction()
- def remove(self, legend=None,
- kind=('curve', 'image', 'item', 'marker')):
+ def remove(self, legend=None, kind=("curve", "image", "item", "marker")):
"""See :meth:`Plot.Plot.remove`"""
self._plot.remove(legend, kind)
@@ -1069,10 +1061,6 @@ class StackView(qt.QMainWindow):
"""
self._plot.setInteractiveMode(*args, **kwargs)
- @deprecated(replacement="addShape", since_version="0.13")
- def addItem(self, *args, **kwargs):
- self.addShape(*args, **kwargs)
-
def addShape(self, *args, **kwargs):
"""
See :meth:`Plot.Plot.addShape`
@@ -1085,6 +1073,7 @@ class PlanesWidget(qt.QWidget):
:param parent: the parent QWidget
"""
+
sigPlaneSelectionChanged = qt.Signal(int)
def __init__(self, parent):
@@ -1107,7 +1096,8 @@ class PlanesWidget(qt.QWidget):
self.qcbAxisSelection = qt.QComboBox(self)
self._setCBChoices(first_stack_dimension=0)
self.qcbAxisSelection.currentIndexChanged[int].connect(
- self.__planeSelectionChanged)
+ self.__planeSelectionChanged
+ )
layout0.addWidget(self.qcbAxisSelection)
@@ -1126,12 +1116,12 @@ class PlanesWidget(qt.QWidget):
def _setCBChoices(self, first_stack_dimension):
self.qcbAxisSelection.clear()
- dim1dim2 = 'Dim%d-Dim%d' % (first_stack_dimension + 1,
- first_stack_dimension + 2)
- dim0dim2 = 'Dim%d-Dim%d' % (first_stack_dimension,
- first_stack_dimension + 2)
- dim0dim1 = 'Dim%d-Dim%d' % (first_stack_dimension,
- first_stack_dimension + 1)
+ dim1dim2 = "Dim%d-Dim%d" % (
+ first_stack_dimension + 1,
+ first_stack_dimension + 2,
+ )
+ dim0dim2 = "Dim%d-Dim%d" % (first_stack_dimension, first_stack_dimension + 2)
+ dim0dim1 = "Dim%d-Dim%d" % (first_stack_dimension, first_stack_dimension + 1)
self.qcbAxisSelection.addItem(icons.getQIcon("cube-front"), dim1dim2)
self.qcbAxisSelection.addItem(icons.getQIcon("cube-bottom"), dim0dim2)
@@ -1169,25 +1159,25 @@ class StackViewMainWindow(StackView):
:param QWidget parent: Parent widget, or None
"""
+
def __init__(self, parent=None):
self._dataInfo = None
super(StackViewMainWindow, self).__init__(parent)
self.setWindowFlags(qt.Qt.Window)
# Add toolbars and status bar
- self.addToolBar(qt.Qt.BottomToolBarArea,
- LimitsToolBar(plot=self._plot))
+ self.addToolBar(qt.Qt.BottomToolBarArea, LimitsToolBar(plot=self._plot))
self.statusBar()
- menu = self.menuBar().addMenu('File')
+ menu = self.menuBar().addMenu("File")
menu.addAction(self._plot.getOutputToolBar().getSaveAction())
menu.addAction(self._plot.getOutputToolBar().getPrintAction())
menu.addSeparator()
- action = menu.addAction('Quit')
+ action = menu.addAction("Quit")
action.triggered[bool].connect(qt.QApplication.instance().quit)
- menu = self.menuBar().addMenu('Edit')
+ menu = self.menuBar().addMenu("Edit")
menu.addAction(self._plot.getOutputToolBar().getCopyAction())
menu.addSeparator()
menu.addAction(self._plot.getResetZoomAction())
@@ -1197,7 +1187,7 @@ class StackViewMainWindow(StackView):
menu.addAction(actions.control.KeepAspectRatioAction(self._plot, self))
menu.addAction(actions.control.YAxisInvertedAction(self._plot, self))
- menu = self.menuBar().addMenu('Profile')
+ menu = self.menuBar().addMenu("Profile")
profileToolBar = self._profileToolBar
menu.addAction(profileToolBar.hLineAction)
menu.addAction(profileToolBar.vLineAction)
@@ -1227,11 +1217,11 @@ class StackViewMainWindow(StackView):
elif self._perspective == 2:
dim0, dim1, dim2 = int(y), int(x), img_idx
- msg = 'Position: (%d, %d, %d)' % (dim0, dim1, dim2)
+ msg = "Position: (%d, %d, %d)" % (dim0, dim1, dim2)
if value is not None:
- msg += ', Value: %g' % value
+ msg += ", Value: %g" % value
if self._dataInfo is not None:
- msg = self._dataInfo + ', ' + msg
+ msg = self._dataInfo + ", " + msg
self.statusBar().showMessage(msg)
@@ -1240,11 +1230,15 @@ class StackViewMainWindow(StackView):
See :meth:`StackView.setStack` for details.
"""
- if hasattr(stack, 'dtype') and hasattr(stack, 'shape'):
+ if hasattr(stack, "dtype") and hasattr(stack, "shape"):
assert len(stack.shape) == 3
nframes, height, width = stack.shape
- self._dataInfo = 'Data: %dx%dx%d (%s)' % (nframes, height, width,
- str(stack.dtype))
+ self._dataInfo = "Data: %dx%dx%d (%s)" % (
+ nframes,
+ height,
+ width,
+ str(stack.dtype),
+ )
self.statusBar().showMessage(self._dataInfo)
else:
self._dataInfo = None
diff --git a/src/silx/gui/plot/StatsWidget.py b/src/silx/gui/plot/StatsWidget.py
index 00f78d0..0c37f52 100644
--- a/src/silx/gui/plot/StatsWidget.py
+++ b/src/silx/gui/plot/StatsWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,7 +30,6 @@ __license__ = "MIT"
__date__ = "24/07/2018"
-from collections import OrderedDict
from contextlib import contextmanager
import logging
import weakref
@@ -56,8 +54,8 @@ _logger = logging.getLogger(__name__)
@enum.unique
class UpdateMode(_Enum):
- AUTO = 'auto'
- MANUAL = 'manual'
+ AUTO = "auto"
+ MANUAL = "manual"
# Helper class to handle specific calls to PlotWidget and SceneWidget
@@ -127,7 +125,7 @@ class _Wrapper(qt.QObject):
:param item:
:rtype: str
"""
- return ''
+ return ""
def getKind(self, item):
"""Returns the kind of an item or None if not supported
@@ -165,18 +163,18 @@ class _PlotWidgetWrapper(_Wrapper):
self.sigCurrentChanged.emit(item)
def _activeCurveChanged(self, previous, current):
- self._activeChanged(kind='curve')
+ self._activeChanged(kind="curve")
def _activeImageChanged(self, previous, current):
- self._activeChanged(kind='image')
+ self._activeChanged(kind="image")
def _activeScatterChanged(self, previous, current):
- self._activeChanged(kind='scatter')
+ self._activeChanged(kind="scatter")
def _limitsChanged(self, event):
"""Handle change of plot area limits."""
- if event['event'] == 'limitsChanged':
- self.sigVisibleDataChanged.emit()
+ if event["event"] == "limitsChanged":
+ self.sigVisibleDataChanged.emit()
def getItems(self):
plot = self.getPlot()
@@ -201,20 +199,20 @@ class _PlotWidgetWrapper(_Wrapper):
kind = self.getKind(item)
if kind in plot._ACTIVE_ITEM_KINDS:
if plot._getActiveItem(kind) != item:
- plot._setActiveItem(kind, item.getName())
+ plot._setActiveItem(kind, item)
def getLabel(self, item):
return item.getName()
def getKind(self, item):
if isinstance(item, plotitems.Curve):
- return 'curve'
+ return "curve"
elif isinstance(item, plotitems.ImageData):
- return 'image'
+ return "image"
elif isinstance(item, plotitems.Scatter):
- return 'scatter'
+ return "scatter"
elif isinstance(item, plotitems.Histogram):
- return 'histogram'
+ return "histogram"
else:
return None
@@ -260,12 +258,10 @@ class _SceneWidgetWrapper(_Wrapper):
def getKind(self, item):
from ..plot3d import items as plot3ditems
- if isinstance(item, (plot3ditems.ImageData,
- plot3ditems.ScalarField3D)):
- return 'image'
- elif isinstance(item, (plot3ditems.Scatter2D,
- plot3ditems.Scatter3D)):
- return 'scatter'
+ if isinstance(item, (plot3ditems.ImageData, plot3ditems.ScalarField3D)):
+ return "image"
+ elif isinstance(item, (plot3ditems.Scatter2D, plot3ditems.Scatter3D)):
+ return "scatter"
else:
return None
@@ -307,10 +303,10 @@ class _ScalarFieldViewWrapper(_Wrapper):
pass
def getLabel(self, item):
- return 'Data'
+ return "Data"
def getKind(self, item):
- return 'image'
+ return "image"
class _Container(object):
@@ -320,6 +316,7 @@ class _Container(object):
:param QObject obj:
"""
+
def __init__(self, obj):
self._obj = obj
@@ -384,7 +381,10 @@ class _StatsWidgetBase(object):
else: # Expect a ScalarFieldView
self._plotWrapper = _ScalarFieldViewWrapper(plot)
else:
- _logger.warning('OpenGL not installed, %s not managed' % ('SceneWidget qnd ScalarFieldView'))
+ _logger.warning(
+ "OpenGL not installed, %s not managed"
+ % ("SceneWidget qnd ScalarFieldView")
+ )
self._dealWithPlotConnection(create=True)
def setStats(self, statsHandler):
@@ -423,16 +423,19 @@ class _StatsWidgetBase(object):
connections = [] # List of (signal, slot) to connect/disconnect
if self._statsOnVisibleData:
connections.append(
- (self._plotWrapper.sigVisibleDataChanged, self._updateAllStats))
+ (self._plotWrapper.sigVisibleDataChanged, self._updateAllStats)
+ )
if self._displayOnlyActItem:
connections.append(
- (self._plotWrapper.sigCurrentChanged, self._updateCurrentItem))
+ (self._plotWrapper.sigCurrentChanged, self._updateCurrentItem)
+ )
else:
connections += [
(self._plotWrapper.sigItemAdded, self._addItem),
(self._plotWrapper.sigItemRemoved, self._removeItem),
- (self._plotWrapper.sigCurrentChanged, self._plotCurrentChanged)]
+ (self._plotWrapper.sigCurrentChanged, self._plotCurrentChanged),
+ ]
for signal, slot in connections:
if create:
@@ -442,12 +445,12 @@ class _StatsWidgetBase(object):
def _updateItemObserve(self, *args):
"""Reload table depending on mode"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _updateCurrentItem(self, *args):
"""specific callback for the sigCurrentChanged and with the
_displayOnlyActItem option."""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _updateStats(self, item, data_changed=False, roi_changed=False):
"""Update displayed information for given plot item
@@ -456,11 +459,11 @@ class _StatsWidgetBase(object):
:param bool data_changed: is the item data changed.
:param bool roi_changed: is the associated roi changed.
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _updateAllStats(self):
"""Update stats for all rows in the table"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def setDisplayOnlyActiveItem(self, displayOnlyActItem):
"""Toggle display off all items or only the active/selected one
@@ -495,21 +498,21 @@ class _StatsWidgetBase(object):
:returns: True if the item is added to the widget.
:rtype: bool
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _removeItem(self, item):
"""Remove table items corresponding to given plot item from the table.
:param item: The plot item
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _plotCurrentChanged(self, current):
"""Handle change of current item and update selection in table
:param current:
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def clear(self):
"""clear GUI"""
@@ -563,16 +566,17 @@ class StatsTable(_StatsWidgetBase, TableWidget):
:class:`PlotWidget` or :class:`SceneWidget` instance on which to operate
"""
- _LEGEND_HEADER_DATA = 'legend'
- _KIND_HEADER_DATA = 'kind'
+ _LEGEND_HEADER_DATA = "legend"
+ _KIND_HEADER_DATA = "kind"
sigUpdateModeChanged = qt.Signal(object)
"""Signal emitted when the update mode changed"""
def __init__(self, parent=None, plot=None):
TableWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self, statsOnVisibleData=False,
- displayOnlyActItem=False)
+ _StatsWidgetBase.__init__(
+ self, statsOnVisibleData=False, displayOnlyActItem=False
+ )
# Init for _displayOnlyActItem == False
assert self._displayOnlyActItem is False
@@ -670,7 +674,15 @@ class StatsTable(_StatsWidgetBase, TableWidget):
If exists, update it only when we are in 'auto' mode"""
if self.getUpdateMode() is UpdateMode.MANUAL:
# when sigCurrentChanged is giving the current item
- if len(args) > 0 and isinstance(args[0], (plotitems.Curve, plotitems.Histogram, plotitems.ImageData, plotitems.Scatter)):
+ if len(args) > 0 and isinstance(
+ args[0],
+ (
+ plotitems.Curve,
+ plotitems.Histogram,
+ plotitems.ImageData,
+ plotitems.Scatter,
+ ),
+ ):
item = args[0]
tableItems = self._itemToTableItems(item)
# if the table does not exists yet
@@ -723,9 +735,9 @@ class StatsTable(_StatsWidgetBase, TableWidget):
:param item: The plot item
:return: An ordered dict of column name to QTableWidgetItem mapping
for the given plot item.
- :rtype: OrderedDict
+ :rtype: dict
"""
- result = OrderedDict()
+ result = {}
row = self._itemToRow(item)
if row is not None:
for column in range(self.columnCount()):
@@ -777,9 +789,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
return False
# Prepare table items
- tableItems = [
- qt.QTableWidgetItem(), # Legend
- qt.QTableWidgetItem()] # Kind
+ tableItems = [qt.QTableWidgetItem(), qt.QTableWidgetItem()] # Legend # Kind
for column in range(2, self.columnCount()):
header = self.horizontalHeaderItem(column)
@@ -806,8 +816,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
row = self.rowCount() - 1
for column, tableItem in enumerate(tableItems):
tableItem.setData(qt.Qt.UserRole, _Container(item))
- tableItem.setFlags(
- qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
+ tableItem.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
self.setItem(row, column, tableItem)
# Update table items content
@@ -816,8 +825,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
# Listen for item changes
# Using queued connection to avoid issue with sender
# being that of the signal calling the signal
- item.sigItemChanged.connect(self._plotItemChanged,
- qt.Qt.QueuedConnection)
+ item.sigItemChanged.connect(self._plotItemChanged, qt.Qt.QueuedConnection)
return True
@@ -872,8 +880,12 @@ class StatsTable(_StatsWidgetBase, TableWidget):
else:
roi_changed = False
stats = statsHandler.calculate(
- item, plot, self._statsOnVisibleData,
- data_changed=data_changed, roi_changed=roi_changed)
+ item,
+ plot,
+ self._statsOnVisibleData,
+ data_changed=data_changed,
+ roi_changed=roi_changed,
+ )
else:
stats = {}
@@ -888,7 +900,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
value = stats.get(name)
if value is None:
_logger.error("Value not found for: %s", name)
- tableItem.setText('-')
+ tableItem.setText("-")
else:
tableItem.setText(str(value))
@@ -944,6 +956,7 @@ class StatsTable(_StatsWidgetBase, TableWidget):
class UpdateModeWidget(qt.QWidget):
"""Widget used to select the mode of update"""
+
sigUpdateModeChanged = qt.Signal(object)
"""signal emitted when the mode for update changed"""
sigUpdateRequested = qt.Signal()
@@ -955,22 +968,22 @@ class UpdateModeWidget(qt.QWidget):
self._buttonGrp = qt.QButtonGroup(parent=self)
self._buttonGrp.setExclusive(True)
- spacer = qt.QSpacerItem(20, 20,
- qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Minimum)
+ spacer = qt.QSpacerItem(
+ 20, 20, qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum
+ )
self.layout().addItem(spacer)
- self._autoRB = qt.QRadioButton('auto', parent=self)
+ self._autoRB = qt.QRadioButton("auto", parent=self)
self.layout().addWidget(self._autoRB)
self._buttonGrp.addButton(self._autoRB)
- self._manualRB = qt.QRadioButton('manual', parent=self)
+ self._manualRB = qt.QRadioButton("manual", parent=self)
self.layout().addWidget(self._manualRB)
self._buttonGrp.addButton(self._manualRB)
self._manualRB.setChecked(True)
- refresh_icon = icons.getQIcon('view-refresh')
- self._updatePB = qt.QPushButton(refresh_icon, '', parent=self)
+ refresh_icon = icons.getQIcon("view-refresh")
+ self._updatePB = qt.QPushButton(refresh_icon, "", parent=self)
self.layout().addWidget(self._updatePB)
# connect signal / SLOT
@@ -1007,7 +1020,7 @@ class UpdateModeWidget(qt.QWidget):
if not self._manualRB.isChecked():
self._manualRB.setChecked(True)
else:
- raise ValueError('mode', mode, 'is not recognized')
+ raise ValueError("mode", mode, "is not recognized")
def getUpdateMode(self):
"""Returns update mode (See :meth:`setUpdateMode`).
@@ -1032,7 +1045,6 @@ class UpdateModeWidget(qt.QWidget):
class _OptionsWidget(qt.QToolBar):
-
def __init__(self, parent=None, updateMode=None, displayOnlyActItem=False):
assert updateMode is not None
qt.QToolBar.__init__(self, parent)
@@ -1056,12 +1068,14 @@ class _OptionsWidget(qt.QToolBar):
action = qt.QAction(self)
action.setIcon(icons.getQIcon("stats-visible-data"))
action.setText("Use the visible data range")
- action.setToolTip("Use the visible data range.<br/>"
- "If activated the data is filtered to only use"
- "visible data of the plot."
- "The filtering is a data sub-sampling."
- "No interpolation is made to fit data to"
- "boundaries.")
+ action.setToolTip(
+ "Use the visible data range.<br/>"
+ "If activated the data is filtered to only use"
+ "visible data of the plot."
+ "The filtering is a data sub-sampling."
+ "No interpolation is made to fit data to"
+ "boundaries."
+ )
action.setCheckable(True)
self.__useVisibleData = action
@@ -1157,7 +1171,7 @@ class StatsWidget(qt.QWidget):
It Provides the visibility of the widget.
"""
- NUMBER_FORMAT = '{0:.3f}'
+ NUMBER_FORMAT = "{0:.3f}"
def __init__(self, parent=None, plot=None, stats=None):
qt.QWidget.__init__(self, parent)
@@ -1173,15 +1187,15 @@ class StatsWidget(qt.QWidget):
self.layout().addWidget(self._statsTable)
old = self._statsTable.blockSignals(True)
- self._options.itemSelection.triggered.connect(
- self._optSelectionChanged)
- self._options.dataRangeSelection.triggered.connect(
- self._optDataRangeChanged)
+ self._options.itemSelection.triggered.connect(self._optSelectionChanged)
+ self._options.dataRangeSelection.triggered.connect(self._optDataRangeChanged)
self._optDataRangeChanged()
self._statsTable.blockSignals(old)
self._statsTable.sigUpdateModeChanged.connect(self._options._setUpdateMode)
- callback = functools.partial(self._getStatsTable()._updateAllStats, is_request=True)
+ callback = functools.partial(
+ self._getStatsTable()._updateAllStats, is_request=True
+ )
self._options.sigUpdateStats.connect(callback)
def _getStatsTable(self):
@@ -1200,12 +1214,12 @@ class StatsWidget(qt.QWidget):
qt.QWidget.hideEvent(self, event)
def _optSelectionChanged(self, action=None):
- self._getStatsTable().setDisplayOnlyActiveItem(
- self._options.isActiveItemMode())
+ self._getStatsTable().setDisplayOnlyActiveItem(self._options.isActiveItemMode())
def _optDataRangeChanged(self, action=None):
self._getStatsTable().setStatsOnVisibleData(
- self._options.isVisibleDataRangeMode())
+ self._options.isVisibleDataRangeMode()
+ )
# Proxy methods
@@ -1216,7 +1230,8 @@ class StatsWidget(qt.QWidget):
@docstring(StatsTable)
def setPlot(self, plot):
self._options.setVisibleDataRangeModeEnabled(
- plot is None or isinstance(plot, PlotWidget))
+ plot is None or isinstance(plot, PlotWidget)
+ )
return self._getStatsTable().setPlot(plot=plot)
@docstring(StatsTable)
@@ -1230,7 +1245,8 @@ class StatsWidget(qt.QWidget):
self._options.setDisplayActiveItems(displayOnlyActItem)
self._options.blockSignals(old)
return self._getStatsTable().setDisplayOnlyActiveItem(
- displayOnlyActItem=displayOnlyActItem)
+ displayOnlyActItem=displayOnlyActItem
+ )
@docstring(StatsTable)
def setStatsOnVisibleData(self, b):
@@ -1245,15 +1261,17 @@ class StatsWidget(qt.QWidget):
self._statsTable.setUpdateMode(mode)
-DEFAULT_STATS = StatsHandler((
- (statsmdl.StatMin(), StatFormatter()),
- statsmdl.StatCoordMin(),
- (statsmdl.StatMax(), StatFormatter()),
- statsmdl.StatCoordMax(),
- statsmdl.StatCOM(),
- (('mean', numpy.mean), StatFormatter()),
- (('std', numpy.std), StatFormatter()),
-))
+DEFAULT_STATS = StatsHandler(
+ (
+ (statsmdl.StatMin(), StatFormatter()),
+ statsmdl.StatCoordMin(),
+ (statsmdl.StatMax(), StatFormatter()),
+ statsmdl.StatCoordMax(),
+ statsmdl.StatCOM(),
+ (("mean", numpy.mean), StatFormatter()),
+ (("std", numpy.std), StatFormatter()),
+ )
+)
class BasicStatsWidget(StatsWidget):
@@ -1283,9 +1301,9 @@ class BasicStatsWidget(StatsWidget):
widget = BasicStatsWidget(plot=plot)
widget.show()
"""
+
def __init__(self, parent=None, plot=None):
- StatsWidget.__init__(self, parent=parent, plot=plot,
- stats=DEFAULT_STATS)
+ StatsWidget.__init__(self, parent=parent, plot=plot, stats=DEFAULT_STATS)
class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
@@ -1307,8 +1325,9 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
sigUpdateModeChanged = qt.Signal(object)
"""Signal emitted when the update mode changed"""
- def __init__(self, parent=None, plot=None, kind='curve', stats=None,
- statsOnVisibleData=False):
+ def __init__(
+ self, parent=None, plot=None, kind="curve", stats=None, statsOnVisibleData=False
+ ):
self._item_kind = kind
"""The item displayed"""
self._statQlineEdit = {}
@@ -1316,9 +1335,9 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
self._n_statistics_per_line = 4
"""number of statistics displayed per line in the grid layout"""
qt.QWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self,
- statsOnVisibleData=statsOnVisibleData,
- displayOnlyActItem=True)
+ _StatsWidgetBase.__init__(
+ self, statsOnVisibleData=statsOnVisibleData, displayOnlyActItem=True
+ )
self.setLayout(self._createLayout())
self.setPlot(plot)
if stats is not None:
@@ -1337,8 +1356,8 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
widget = qt.QWidget(parent=self)
parent = widget
- qLabel = qt.QLabel(statistic.name + ':', parent=parent)
- qLineEdit = qt.QLineEdit('', parent=parent)
+ qLabel = qt.QLabel(statistic.name + ":", parent=parent)
+ qLineEdit = qt.QLineEdit("", parent=parent)
qLineEdit.setReadOnly(True)
self._addStatsWidgetsToLayout(qLabel=qLabel, qLineEdit=qLineEdit)
@@ -1354,7 +1373,7 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
self._updateAllStats()
def _addStatsWidgetsToLayout(self, qLabel, qLineEdit):
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def setStats(self, statsHandler):
"""Set which stats to display and the associated formatting.
@@ -1380,6 +1399,7 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
def kind_filter(_item):
return self._plotWrapper.getKind(_item) == self.getKind()
+
items = list(filter(kind_filter, _items))
assert len(items) in (0, 1)
if len(items) == 1:
@@ -1403,15 +1423,13 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
def _setItem(self, item, data_changed=True):
if item is None:
for stat_name, stat_widget in self._statQlineEdit.items():
- stat_widget.setText('')
- elif (self._statsHandler is not None and len(
- self._statsHandler.stats) > 0):
+ stat_widget.setText("")
+ elif self._statsHandler is not None and len(self._statsHandler.stats) > 0:
plot = self.getPlot()
if plot is not None:
- statsValDict = self._statsHandler.calculate(item,
- plot,
- self._statsOnVisibleData,
- data_changed=data_changed)
+ statsValDict = self._statsHandler.calculate(
+ item, plot, self._statsOnVisibleData, data_changed=data_changed
+ )
for statName, statVal in list(statsValDict.items()):
self._statQlineEdit[statName].setText(statVal)
@@ -1423,6 +1441,7 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
def kind_filter(_item):
return self._plotWrapper.getKind(_item) == self.getKind()
+
items = list(filter(kind_filter, _items))
assert len(items) in (0, 1)
_item = items[0] if len(items) == 1 else None
@@ -1433,27 +1452,38 @@ class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
def _createLayout(self):
"""create an instance of the main QLayout"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def _addItem(self, item):
- raise NotImplementedError('Display only the active item')
+ raise NotImplementedError("Display only the active item")
def _removeItem(self, item):
- raise NotImplementedError('Display only the active item')
+ raise NotImplementedError("Display only the active item")
def _plotCurrentChanged(self, current):
- raise NotImplementedError('Display only the active item')
+ raise NotImplementedError("Display only the active item")
def _updateModeHasChanged(self):
self.sigUpdateModeChanged.emit(self._updateMode)
class _BasicLineStatsWidget(_BaseLineStatsWidget):
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
- _BaseLineStatsWidget.__init__(self, parent=parent, kind=kind,
- plot=plot, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ kind="curve",
+ stats=DEFAULT_STATS,
+ statsOnVisibleData=False,
+ ):
+ _BaseLineStatsWidget.__init__(
+ self,
+ parent=parent,
+ kind=kind,
+ plot=plot,
+ stats=stats,
+ statsOnVisibleData=statsOnVisibleData,
+ )
def _createLayout(self):
return FlowLayout()
@@ -1489,15 +1519,26 @@ class BasicLineStatsWidget(qt.QWidget):
:param bool statsOnVisibleData: compute statistics for the whole data or
only visible ones.
"""
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
+
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ kind="curve",
+ stats=DEFAULT_STATS,
+ statsOnVisibleData=False,
+ ):
qt.QWidget.__init__(self, parent)
self.setLayout(qt.QHBoxLayout())
self.layout().setSpacing(0)
self.layout().setContentsMargins(0, 0, 0, 0)
- self._lineStatsWidget = _BasicLineStatsWidget(parent=self, plot=plot,
- kind=kind, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
+ self._lineStatsWidget = _BasicLineStatsWidget(
+ parent=self,
+ plot=plot,
+ kind=kind,
+ stats=stats,
+ statsOnVisibleData=statsOnVisibleData,
+ )
self.layout().addWidget(self._lineStatsWidget)
self._options = UpdateModeWidget()
@@ -1549,12 +1590,23 @@ class BasicLineStatsWidget(qt.QWidget):
class _BasicGridStatsWidget(_BaseLineStatsWidget):
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False,
- statsPerLine=4):
- _BaseLineStatsWidget.__init__(self, parent=parent, kind=kind,
- plot=plot, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ kind="curve",
+ stats=DEFAULT_STATS,
+ statsOnVisibleData=False,
+ statsPerLine=4,
+ ):
+ _BaseLineStatsWidget.__init__(
+ self,
+ parent=parent,
+ kind=kind,
+ plot=plot,
+ stats=stats,
+ statsOnVisibleData=statsOnVisibleData,
+ )
self._n_statistics_per_line = statsPerLine
def _addStatsWidgetsToLayout(self, qLabel, qLineEdit):
@@ -1598,8 +1650,14 @@ class BasicGridStatsWidget(qt.QWidget):
widget.show()
"""
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ kind="curve",
+ stats=DEFAULT_STATS,
+ statsOnVisibleData=False,
+ ):
qt.QWidget.__init__(self, parent)
self.setLayout(qt.QVBoxLayout())
self.layout().setSpacing(0)
@@ -1609,9 +1667,13 @@ class BasicGridStatsWidget(qt.QWidget):
self._options.showRadioButtons(False)
self.layout().addWidget(self._options)
- self._lineStatsWidget = _BasicGridStatsWidget(parent=self, plot=plot,
- kind=kind, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
+ self._lineStatsWidget = _BasicGridStatsWidget(
+ parent=self,
+ plot=plot,
+ kind=kind,
+ stats=stats,
+ statsOnVisibleData=statsOnVisibleData,
+ )
self.layout().addWidget(self._lineStatsWidget)
# tune options
diff --git a/src/silx/gui/plot/_BaseMaskToolsWidget.py b/src/silx/gui/plot/_BaseMaskToolsWidget.py
index 407ab11..6b98289 100644
--- a/src/silx/gui/plot/_BaseMaskToolsWidget.py
+++ b/src/silx/gui/plot/_BaseMaskToolsWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2022 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 +24,6 @@
"""This module is a collection of base classes used in modules
:mod:`.MaskToolsWidget` (images) and :mod:`.ScatterMaskToolsWidget`
"""
-from __future__ import division
__authors__ = ["T. Vincent", "P. Knobel"]
__license__ = "MIT"
@@ -136,7 +134,7 @@ class BaseMask(qt.QObject):
:param bool copy: True (the default) to copy the array,
False to use it as is if possible.
"""
- self._mask = numpy.array(mask, copy=copy, order='C', dtype=numpy.uint8)
+ self._mask = numpy.array(mask, copy=copy, order="C", dtype=numpy.uint8)
self._notify()
# History control
@@ -149,8 +147,11 @@ class BaseMask(qt.QObject):
def commit(self):
"""Append the current mask to history if changed"""
- if (not self._history or self._redo or
- not numpy.array_equal(self._mask, self._history[-1])):
+ if (
+ not self._history
+ or self._redo
+ or not numpy.array_equal(self._mask, self._history[-1])
+ ):
if self._redo:
self._redo = [] # Reset redo as a new action as been performed
self.sigRedoable[bool].emit(False)
@@ -224,7 +225,7 @@ class BaseMask(qt.QObject):
if shape is None:
# assume dimensionality never changes
shape = (0,) * len(self._mask.shape) # empty array
- shapeChanged = (shape != self._mask.shape)
+ shapeChanged = shape != self._mask.shape
self._mask = numpy.zeros(shape, dtype=numpy.uint8)
if shapeChanged:
self.resetHistory()
@@ -265,9 +266,7 @@ class BaseMask(qt.QObject):
:param float threshold: Threshold
:param bool mask: True to mask (default), False to unmask.
"""
- self.updateStencil(level,
- self.getDataValues() < threshold,
- mask)
+ self.updateStencil(level, self.getDataValues() < threshold, mask)
def updateBetweenThresholds(self, level, min_, max_, mask=True):
"""Mask/unmask all points whose values are in a range.
@@ -277,8 +276,9 @@ class BaseMask(qt.QObject):
:param float max_: Upper threshold
:param bool mask: True to mask (default), False to unmask.
"""
- stencil = numpy.logical_and(min_ <= self.getDataValues(),
- self.getDataValues() <= max_)
+ stencil = numpy.logical_and(
+ min_ <= self.getDataValues(), self.getDataValues() <= max_
+ )
self.updateStencil(level, stencil, mask)
def updateAboveThreshold(self, level, threshold, mask=True):
@@ -288,9 +288,7 @@ class BaseMask(qt.QObject):
:param float threshold: Threshold.
:param bool mask: True to mask (default), False to unmask.
"""
- self.updateStencil(level,
- self.getDataValues() > threshold,
- mask)
+ self.updateStencil(level, self.getDataValues() > threshold, mask)
def updateNotFinite(self, level, mask=True):
"""Mask/unmask all points whose values are not finite.
@@ -298,9 +296,9 @@ class BaseMask(qt.QObject):
:param int level: Mask level to update.
:param bool mask: True to mask (default), False to unmask.
"""
- self.updateStencil(level,
- numpy.logical_not(numpy.isfinite(self.getDataValues())),
- mask)
+ self.updateStencil(
+ level, numpy.logical_not(numpy.isfinite(self.getDataValues())), mask
+ )
# Drawing operations:
def updateRectangle(self, level, row, col, height, width, mask=True):
@@ -392,18 +390,20 @@ class BaseMaskToolsWidget(qt.QWidget):
# register if the user as force a color for the corresponding mask level
self._defaultColors = numpy.ones((self._maxLevelNumber + 1), dtype=bool)
# overlays colors set by the user
- self._overlayColors = numpy.zeros((self._maxLevelNumber + 1, 3), dtype=numpy.float32)
+ self._overlayColors = numpy.zeros(
+ (self._maxLevelNumber + 1, 3), dtype=numpy.float32
+ )
# as parent have to be the first argument of the widget to fit
# QtDesigner need but here plot can't be None by default.
assert plot is not None
self._plotRef = weakref.ref(plot)
- self._maskName = '__MASK_TOOLS_%d' % id(self) # Legend of the mask
+ self._maskName = "__MASK_TOOLS_%d" % id(self) # Legend of the mask
- self._colormap = Colormap(normalization='linear',
- vmin=0,
- vmax=self._maxLevelNumber)
- self._defaultOverlayColor = rgba('gray') # Color of the mask
+ self._colormap = Colormap(
+ normalization="linear", vmin=0, vmax=self._maxLevelNumber
+ )
+ self._defaultOverlayColor = rgba("gray") # Color of the mask
self._setMaskColors(1, 0.5) # Set the colormap LUT
if not isinstance(mask, BaseMask):
@@ -415,11 +415,10 @@ class BaseMaskToolsWidget(qt.QWidget):
self._drawingMode = None # Store current drawing mode
self._lastPencilPos = None
- self._multipleMasks = 'exclusive'
+ self._multipleMasks = "exclusive"
- self._maskFileDir = qt.QDir.home().absolutePath()
- self.plot.sigInteractiveModeChanged.connect(
- self._interactiveModeChanged)
+ self._maskFileDir = qt.QDir.current().absolutePath()
+ self.plot.sigInteractiveModeChanged.connect(self._interactiveModeChanged)
self._initWidgets()
@@ -472,11 +471,11 @@ class BaseMaskToolsWidget(qt.QWidget):
:param str mode: The mode to use
"""
- assert mode in ('exclusive', 'single')
+ assert mode in ("exclusive", "single")
if mode != self._multipleMasks:
self._multipleMasks = mode
- self._levelWidget.setVisible(self._multipleMasks != 'single')
- self._clearAllBtn.setVisible(self._multipleMasks != 'single')
+ self._levelWidget.setVisible(self._multipleMasks != "single")
+ self._clearAllBtn.setVisible(self._multipleMasks != "single")
def setMaskFileDirectory(self, path):
"""Set the default directory to use by load/save GUI tools
@@ -494,7 +493,7 @@ class BaseMaskToolsWidget(qt.QWidget):
def maskFileDir(self):
"""The directory from which to load/save mask from/to files."""
if not os.path.isdir(self._maskFileDir):
- self._maskFileDir = qt.QDir.home().absolutePath()
+ self._maskFileDir = qt.QDir.current().absolutePath()
return self._maskFileDir
@maskFileDir.setter
@@ -507,7 +506,8 @@ class BaseMaskToolsWidget(qt.QWidget):
plot = self._plotRef()
if plot is None:
raise RuntimeError(
- 'Mask widget attached to a PlotWidget that no longer exists')
+ "Mask widget attached to a PlotWidget that no longer exists"
+ )
return plot
def setDirection(self, direction=qt.QBoxLayout.LeftToRight):
@@ -536,7 +536,7 @@ class BaseMaskToolsWidget(qt.QWidget):
False for no trailing stretch
:return: A QWidget with a QHBoxLayout
"""
- stretch = kwargs.get('stretch', True)
+ stretch = kwargs.get("stretch", True)
layout = qt.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
@@ -549,20 +549,27 @@ class BaseMaskToolsWidget(qt.QWidget):
return widget
def _initTransparencyWidget(self):
- """ Init the mask transparency widget """
+ """Init the mask transparency widget"""
transparencyWidget = qt.QWidget(parent=self)
grid = qt.QGridLayout()
grid.setContentsMargins(0, 0, 0, 0)
- self.transparencySlider = qt.QSlider(qt.Qt.Horizontal, parent=transparencyWidget)
+ self.transparencySlider = qt.QSlider(
+ qt.Qt.Horizontal, parent=transparencyWidget
+ )
self.transparencySlider.setRange(3, 10)
self.transparencySlider.setValue(8)
- self.transparencySlider.setToolTip(
- 'Set the transparency of the mask display')
+ self.transparencySlider.setToolTip("Set the transparency of the mask display")
self.transparencySlider.valueChanged.connect(self._updateColors)
- grid.addWidget(qt.QLabel('Display:', parent=transparencyWidget), 0, 0)
+ grid.addWidget(qt.QLabel("Display:", parent=transparencyWidget), 0, 0)
grid.addWidget(self.transparencySlider, 0, 1, 1, 3)
- grid.addWidget(qt.QLabel('<small><b>Transparent</b></small>', parent=transparencyWidget), 1, 1)
- grid.addWidget(qt.QLabel('<small><b>Opaque</b></small>', parent=transparencyWidget), 1, 3)
+ grid.addWidget(
+ qt.QLabel("<small><b>Transparent</b></small>", parent=transparencyWidget),
+ 1,
+ 1,
+ )
+ grid.addWidget(
+ qt.QLabel("<small><b>Opaque</b></small>", parent=transparencyWidget), 1, 3
+ )
transparencyWidget.setLayout(grid)
return transparencyWidget
@@ -573,11 +580,13 @@ class BaseMaskToolsWidget(qt.QWidget):
self.levelSpinBox = qt.QSpinBox()
self.levelSpinBox.setRange(1, self._maxLevelNumber)
self.levelSpinBox.setToolTip(
- 'Choose which mask level is edited.\n'
- 'A mask can have up to 255 non-overlapping levels.')
+ "Choose which mask level is edited.\n"
+ "A mask can have up to 255 non-overlapping levels."
+ )
self.levelSpinBox.valueChanged[int].connect(self._updateColors)
- self._levelWidget = self._hboxWidget(qt.QLabel('Mask level:'),
- self.levelSpinBox)
+ self._levelWidget = self._hboxWidget(
+ qt.QLabel("Mask level:"), self.levelSpinBox
+ )
# Transparency
self._transparencyWidget = self._initTransparencyWidget()
@@ -595,62 +604,66 @@ class BaseMaskToolsWidget(qt.QWidget):
return qt.QIcon()
undoAction = qt.QAction(self)
- undoAction.setText('Undo')
+ undoAction.setText("Undo")
icon = getIcon("edit-undo", qt.QStyle.SP_ArrowBack)
undoAction.setIcon(icon)
undoAction.setShortcut(qt.QKeySequence.Undo)
- undoAction.setToolTip('Undo last mask change <b>%s</b>' %
- undoAction.shortcut().toString())
+ undoAction.setToolTip(
+ "Undo last mask change <b>%s</b>" % undoAction.shortcut().toString()
+ )
self._mask.sigUndoable.connect(undoAction.setEnabled)
undoAction.triggered.connect(self._mask.undo)
redoAction = qt.QAction(self)
- redoAction.setText('Redo')
+ redoAction.setText("Redo")
icon = getIcon("edit-redo", qt.QStyle.SP_ArrowForward)
redoAction.setIcon(icon)
redoAction.setShortcut(qt.QKeySequence.Redo)
- redoAction.setToolTip('Redo last undone mask change <b>%s</b>' %
- redoAction.shortcut().toString())
+ redoAction.setToolTip(
+ "Redo last undone mask change <b>%s</b>" % redoAction.shortcut().toString()
+ )
self._mask.sigRedoable.connect(redoAction.setEnabled)
redoAction.triggered.connect(self._mask.redo)
loadAction = qt.QAction(self)
- loadAction.setText('Load...')
+ loadAction.setText("Load...")
icon = icons.getQIcon("document-open")
loadAction.setIcon(icon)
- loadAction.setToolTip('Load mask from file')
+ loadAction.setToolTip("Load mask from file")
loadAction.triggered.connect(self._loadMask)
saveAction = qt.QAction(self)
- saveAction.setText('Save...')
+ saveAction.setText("Save...")
icon = icons.getQIcon("document-save")
saveAction.setIcon(icon)
- saveAction.setToolTip('Save mask to file')
+ saveAction.setToolTip("Save mask to file")
saveAction.triggered.connect(self._saveMask)
invertAction = qt.QAction(self)
- invertAction.setText('Invert')
+ invertAction.setText("Invert")
icon = icons.getQIcon("mask-invert")
invertAction.setIcon(icon)
- invertAction.setShortcut(qt.Qt.CTRL + qt.Qt.Key_I)
- invertAction.setToolTip('Invert current mask <b>%s</b>' %
- invertAction.shortcut().toString())
+ invertAction.setShortcut(qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_I))
+ invertAction.setToolTip(
+ "Invert current mask <b>%s</b>" % invertAction.shortcut().toString()
+ )
invertAction.triggered.connect(self._handleInvertMask)
clearAction = qt.QAction(self)
- clearAction.setText('Clear')
+ clearAction.setText("Clear")
icon = icons.getQIcon("mask-clear")
clearAction.setIcon(icon)
clearAction.setShortcut(qt.QKeySequence.Delete)
- clearAction.setToolTip('Clear current mask level <b>%s</b>' %
- clearAction.shortcut().toString())
+ clearAction.setToolTip(
+ "Clear current mask level <b>%s</b>" % clearAction.shortcut().toString()
+ )
clearAction.triggered.connect(self._handleClearMask)
clearAllAction = qt.QAction(self)
- clearAllAction.setText('Clear all')
+ clearAllAction.setText("Clear all")
icon = icons.getQIcon("mask-clear-all")
clearAllAction.setIcon(icon)
- clearAllAction.setToolTip('Clear all mask levels')
+ clearAllAction.setToolTip("Clear all mask levels")
clearAllAction.triggered.connect(self.resetSelectionMask)
# Buttons group
@@ -659,9 +672,17 @@ class BaseMaskToolsWidget(qt.QWidget):
margin2 = qt.QWidget(self)
margin2.setMinimumWidth(6)
- actions = (loadAction, saveAction, margin1,
- undoAction, redoAction, margin2,
- invertAction, clearAction, clearAllAction)
+ actions = (
+ loadAction,
+ saveAction,
+ margin1,
+ undoAction,
+ redoAction,
+ margin2,
+ invertAction,
+ clearAction,
+ clearAllAction,
+ )
widgets = []
for action in actions:
if isinstance(action, qt.QWidget):
@@ -681,7 +702,7 @@ class BaseMaskToolsWidget(qt.QWidget):
layout.addWidget(self._transparencyWidget)
layout.addStretch(1)
- maskGroup = qt.QGroupBox('Mask')
+ maskGroup = qt.QGroupBox("Mask")
maskGroup.setLayout(layout)
return maskGroup
@@ -697,44 +718,46 @@ class BaseMaskToolsWidget(qt.QWidget):
self.addAction(self.browseAction)
# Draw tools
- self.rectAction = qt.QAction(icons.getQIcon('shape-rectangle'),
- 'Rectangle selection',
- self)
+ self.rectAction = qt.QAction(
+ icons.getQIcon("shape-rectangle"), "Rectangle selection", self
+ )
self.rectAction.setToolTip(
- 'Rectangle selection tool: (Un)Mask a rectangular region <b>R</b>')
+ "Rectangle selection tool: (Un)Mask a rectangular region <b>R</b>"
+ )
self.rectAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
self.rectAction.setCheckable(True)
self.rectAction.triggered.connect(self._activeRectMode)
self.addAction(self.rectAction)
- self.ellipseAction = qt.QAction(icons.getQIcon('shape-ellipse'),
- 'Circle selection',
- self)
+ self.ellipseAction = qt.QAction(
+ icons.getQIcon("shape-ellipse"), "Circle selection", self
+ )
self.ellipseAction.setToolTip(
- 'Rectangle selection tool: (Un)Mask a circle region <b>R</b>')
+ "Rectangle selection tool: (Un)Mask a circle region <b>R</b>"
+ )
self.ellipseAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
self.ellipseAction.setCheckable(True)
self.ellipseAction.triggered.connect(self._activeEllipseMode)
self.addAction(self.ellipseAction)
- self.polygonAction = qt.QAction(icons.getQIcon('shape-polygon'),
- 'Polygon selection',
- self)
+ self.polygonAction = qt.QAction(
+ icons.getQIcon("shape-polygon"), "Polygon selection", self
+ )
self.polygonAction.setShortcut(qt.QKeySequence(qt.Qt.Key_S))
self.polygonAction.setToolTip(
- 'Polygon selection tool: (Un)Mask a polygonal region <b>S</b><br>'
- 'Left-click to place new polygon corners<br>'
- 'Left-click on first corner to close the polygon')
+ "Polygon selection tool: (Un)Mask a polygonal region <b>S</b><br>"
+ "Left-click to place new polygon corners<br>"
+ "Left-click on first corner to close the polygon"
+ )
self.polygonAction.setCheckable(True)
self.polygonAction.triggered.connect(self._activePolygonMode)
self.addAction(self.polygonAction)
- self.pencilAction = qt.QAction(icons.getQIcon('draw-pencil'),
- 'Pencil tool',
- self)
+ self.pencilAction = qt.QAction(
+ icons.getQIcon("draw-pencil"), "Pencil tool", self
+ )
self.pencilAction.setShortcut(qt.QKeySequence(qt.Qt.Key_P))
- self.pencilAction.setToolTip(
- 'Pencil tool: (Un)Mask using a pencil <b>P</b>')
+ self.pencilAction.setToolTip("Pencil tool: (Un)Mask using a pencil <b>P</b>")
self.pencilAction.setCheckable(True)
self.pencilAction.triggered.connect(self._activePencilMode)
self.addAction(self.pencilAction)
@@ -746,8 +769,13 @@ class BaseMaskToolsWidget(qt.QWidget):
self.drawActionGroup.addAction(self.polygonAction)
self.drawActionGroup.addAction(self.pencilAction)
- actions = (self.browseAction, self.rectAction, self.ellipseAction,
- self.polygonAction, self.pencilAction)
+ actions = (
+ self.browseAction,
+ self.rectAction,
+ self.ellipseAction,
+ self.polygonAction,
+ self.pencilAction,
+ )
drawButtons = []
for action in actions:
btn = qt.QToolButton()
@@ -757,14 +785,16 @@ class BaseMaskToolsWidget(qt.QWidget):
layout.addWidget(container)
# Mask/Unmask radio buttons
- maskRadioBtn = qt.QRadioButton('Mask')
+ maskRadioBtn = qt.QRadioButton("Mask")
maskRadioBtn.setToolTip(
- 'Drawing masks with current level. Press <b>Ctrl</b> to unmask')
+ "Drawing masks with current level. Press <b>Ctrl</b> to unmask"
+ )
maskRadioBtn.setChecked(True)
- unmaskRadioBtn = qt.QRadioButton('Unmask')
+ unmaskRadioBtn = qt.QRadioButton("Unmask")
unmaskRadioBtn.setToolTip(
- 'Drawing unmasks with current level. Press <b>Ctrl</b> to mask')
+ "Drawing unmasks with current level. Press <b>Ctrl</b> to mask"
+ )
self.maskStateGroup = qt.QButtonGroup()
self.maskStateGroup.addButton(maskRadioBtn, 1)
@@ -782,7 +812,7 @@ class BaseMaskToolsWidget(qt.QWidget):
layout.addStretch(1)
- drawGroup = qt.QGroupBox('Draw tools')
+ drawGroup = qt.QGroupBox("Draw tools")
drawGroup.setLayout(layout)
return drawGroup
@@ -799,7 +829,7 @@ class BaseMaskToolsWidget(qt.QWidget):
self.pencilSlider.setRange(1, 50)
self.pencilSlider.setToolTip(pencilToolTip)
- pencilLabel = qt.QLabel('Pencil size:', parent=pencilSetting)
+ pencilLabel = qt.QLabel("Pencil size:", parent=pencilSetting)
layout = qt.QGridLayout()
layout.addWidget(pencilLabel, 0, 0)
@@ -815,26 +845,29 @@ class BaseMaskToolsWidget(qt.QWidget):
def _initThresholdGroupBox(self):
"""Init thresholding widgets"""
- self.belowThresholdAction = qt.QAction(icons.getQIcon('plot-roi-below'),
- 'Mask below threshold',
- self)
+ self.belowThresholdAction = qt.QAction(
+ icons.getQIcon("plot-roi-below"), "Mask below threshold", self
+ )
self.belowThresholdAction.setToolTip(
- 'Mask image where values are below given threshold')
+ "Mask image where values are below given threshold"
+ )
self.belowThresholdAction.setCheckable(True)
self.belowThresholdAction.setChecked(True)
- self.betweenThresholdAction = qt.QAction(icons.getQIcon('plot-roi-between'),
- 'Mask within range',
- self)
+ self.betweenThresholdAction = qt.QAction(
+ icons.getQIcon("plot-roi-between"), "Mask within range", self
+ )
self.betweenThresholdAction.setToolTip(
- 'Mask image where values are within given range')
+ "Mask image where values are within given range"
+ )
self.betweenThresholdAction.setCheckable(True)
- self.aboveThresholdAction = qt.QAction(icons.getQIcon('plot-roi-above'),
- 'Mask above threshold',
- self)
+ self.aboveThresholdAction = qt.QAction(
+ icons.getQIcon("plot-roi-above"), "Mask above threshold", self
+ )
self.aboveThresholdAction.setToolTip(
- 'Mask image where values are above given threshold')
+ "Mask image where values are above given threshold"
+ )
self.aboveThresholdAction.setCheckable(True)
self.thresholdActionGroup = qt.QActionGroup(self)
@@ -842,17 +875,18 @@ class BaseMaskToolsWidget(qt.QWidget):
self.thresholdActionGroup.addAction(self.belowThresholdAction)
self.thresholdActionGroup.addAction(self.betweenThresholdAction)
self.thresholdActionGroup.addAction(self.aboveThresholdAction)
- self.thresholdActionGroup.triggered.connect(
- self._thresholdActionGroupTriggered)
+ self.thresholdActionGroup.triggered.connect(self._thresholdActionGroupTriggered)
- self.loadColormapRangeAction = qt.QAction(icons.getQIcon('view-refresh'),
- 'Set min-max from colormap',
- self)
+ self.loadColormapRangeAction = qt.QAction(
+ icons.getQIcon("view-refresh"), "Set min-max from colormap", self
+ )
self.loadColormapRangeAction.setToolTip(
- 'Set min and max values from current colormap range')
+ "Set min and max values from current colormap range"
+ )
self.loadColormapRangeAction.setCheckable(False)
self.loadColormapRangeAction.triggered.connect(
- self._loadRangeFromColormapTriggered)
+ self._loadRangeFromColormapTriggered
+ )
widgets = []
for action in self.thresholdActionGroup.actions():
@@ -861,8 +895,7 @@ class BaseMaskToolsWidget(qt.QWidget):
widgets.append(btn)
spacer = qt.QWidget(parent=self)
- spacer.setSizePolicy(qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Preferred)
+ spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Preferred)
widgets.append(spacer)
loadColormapRangeBtn = qt.QToolButton()
@@ -884,7 +917,7 @@ class BaseMaskToolsWidget(qt.QWidget):
config.addWidget(self.maxLineLabel, 1, 0)
config.addWidget(self.maxLineEdit, 1, 1)
- self.applyMaskBtn = qt.QPushButton('Apply mask')
+ self.applyMaskBtn = qt.QPushButton("Apply mask")
self.applyMaskBtn.clicked.connect(self._maskBtnClicked)
layout = qt.QVBoxLayout()
@@ -893,7 +926,7 @@ class BaseMaskToolsWidget(qt.QWidget):
layout.addWidget(self.applyMaskBtn)
layout.addStretch(1)
- self.thresholdGroup = qt.QGroupBox('Threshold')
+ self.thresholdGroup = qt.QGroupBox("Threshold")
self.thresholdGroup.setLayout(layout)
# Init widget state
@@ -905,23 +938,25 @@ class BaseMaskToolsWidget(qt.QWidget):
def _initOtherToolsGroupBox(self):
layout = qt.QVBoxLayout()
- self.maskNanBtn = qt.QPushButton('Mask not finite values')
- self.maskNanBtn.setToolTip('Mask Not a Number and infinite values')
+ self.maskNanBtn = qt.QPushButton("Mask not finite values")
+ self.maskNanBtn.setToolTip("Mask Not a Number and infinite values")
self.maskNanBtn.clicked.connect(self._maskNotFiniteBtnClicked)
layout.addWidget(self.maskNanBtn)
layout.addStretch(1)
- self.otherToolGroup = qt.QGroupBox('Other tools')
+ self.otherToolGroup = qt.QGroupBox("Other tools")
self.otherToolGroup.setLayout(layout)
return self.otherToolGroup
def changeEvent(self, event):
"""Reset drawing action when disabling widget"""
- if (event.type() == qt.QEvent.EnabledChange and
- not self.isEnabled() and
- self.drawActionGroup.checkedAction()):
- # Disable drawing tool by setting interaction to zoom
- self.browseAction.trigger()
+ if (
+ event.type() == qt.QEvent.EnabledChange
+ and not self.isEnabled()
+ and self.drawActionGroup.checkedAction()
+ ):
+ # Disable drawing tool by reseting interaction to pan or zoom
+ self.plot.resetInteractiveMode()
def save(self, filename, kind):
"""Save current mask in a file
@@ -954,20 +989,20 @@ class BaseMaskToolsWidget(qt.QWidget):
colors = numpy.empty((self._maxLevelNumber + 1, 4), dtype=numpy.float32)
# Set color
- colors[:,:3] = self._defaultOverlayColor[:3]
+ colors[:, :3] = self._defaultOverlayColor[:3]
# check if some colors has been directly set by the user
mask = numpy.equal(self._defaultColors, False)
- colors[mask,:3] = self._overlayColors[mask,:3]
+ colors[mask, :3] = self._overlayColors[mask, :3]
# Set alpha
- colors[:, -1] = alpha / 2.
+ colors[:, -1] = alpha / 2.0
# Set highlighted level color
colors[level, 3] = alpha
# Set no mask level
- colors[0] = (0., 0., 0., 0.)
+ colors[0] = (0.0, 0.0, 0.0, 0.0)
self._colormap.setColormapLUT(colors)
@@ -1009,14 +1044,14 @@ class BaseMaskToolsWidget(qt.QWidget):
def _updateColors(self, *args):
"""Rebuild mask colormap when selected level or transparency change"""
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
+ self._setMaskColors(
+ self.levelSpinBox.value(),
+ self.transparencySlider.value() / self.transparencySlider.maximum(),
+ )
self._updatePlotMask()
self._updateInteractiveMode()
def _pencilWidthChanged(self, width):
-
old = self.pencilSpinBox.blockSignals(True)
try:
self.pencilSpinBox.setValue(width)
@@ -1034,13 +1069,13 @@ class BaseMaskToolsWidget(qt.QWidget):
"""Update the current mode to the same if some cached data have to be
updated. It is the case for the color for example.
"""
- if self._drawingMode == 'rectangle':
+ if self._drawingMode == "rectangle":
self._activeRectMode()
- elif self._drawingMode == 'ellipse':
+ elif self._drawingMode == "ellipse":
self._activeEllipseMode()
- elif self._drawingMode == 'polygon':
+ elif self._drawingMode == "polygon":
self._activePolygonMode()
- elif self._drawingMode == 'pencil':
+ elif self._drawingMode == "pencil":
self._activePencilMode()
def _handleClearMask(self):
@@ -1077,30 +1112,30 @@ class BaseMaskToolsWidget(qt.QWidget):
def _activeRectMode(self):
"""Handle rect action mode triggering"""
self._releaseDrawingMode()
- self._drawingMode = 'rectangle'
+ self._drawingMode = "rectangle"
self.plot.sigPlotSignal.connect(self._plotDrawEvent)
color = self.getCurrentMaskColor()
self.plot.setInteractiveMode(
- 'draw', shape='rectangle', source=self, color=color)
+ "draw", shape="rectangle", source=self, color=color
+ )
self._updateDrawingModeWidgets()
def _activeEllipseMode(self):
"""Handle circle action mode triggering"""
self._releaseDrawingMode()
- self._drawingMode = 'ellipse'
+ self._drawingMode = "ellipse"
self.plot.sigPlotSignal.connect(self._plotDrawEvent)
color = self.getCurrentMaskColor()
- self.plot.setInteractiveMode(
- 'draw', shape='ellipse', source=self, color=color)
+ self.plot.setInteractiveMode("draw", shape="ellipse", source=self, color=color)
self._updateDrawingModeWidgets()
def _activePolygonMode(self):
"""Handle polygon action mode triggering"""
self._releaseDrawingMode()
- self._drawingMode = 'polygon'
+ self._drawingMode = "polygon"
self.plot.sigPlotSignal.connect(self._plotDrawEvent)
color = self.getCurrentMaskColor()
- self.plot.setInteractiveMode('draw', shape='polygon', source=self, color=color)
+ self.plot.setInteractiveMode("draw", shape="polygon", source=self, color=color)
self._updateDrawingModeWidgets()
def _getPencilWidth(self):
@@ -1113,17 +1148,18 @@ class BaseMaskToolsWidget(qt.QWidget):
def _activePencilMode(self):
"""Handle pencil action mode triggering"""
self._releaseDrawingMode()
- self._drawingMode = 'pencil'
+ self._drawingMode = "pencil"
self.plot.sigPlotSignal.connect(self._plotDrawEvent)
color = self.getCurrentMaskColor()
width = self._getPencilWidth()
self.plot.setInteractiveMode(
- 'draw', shape='pencil', source=self, color=color, width=width)
+ "draw", shape="pencil", source=self, color=color, width=width
+ )
self._updateDrawingModeWidgets()
def _updateDrawingModeWidgets(self):
self.maskStateWidget.setVisible(self._drawingMode is not None)
- self.pencilSetting.setVisible(self._drawingMode == 'pencil')
+ self.pencilSetting.setVisible(self._drawingMode == "pencil")
# Handle plot drawing events
@@ -1133,7 +1169,7 @@ class BaseMaskToolsWidget(qt.QWidget):
:rtype: bool"""
# First draw event, use current modifiers for all draw sequence
- doMask = (self.maskStateGroup.checkedId() == 1)
+ doMask = self.maskStateGroup.checkedId() == 1
if qt.QApplication.keyboardModifiers() & qt.Qt.ControlModifier:
doMask = not doMask
return doMask
@@ -1165,29 +1201,29 @@ class BaseMaskToolsWidget(qt.QWidget):
def _maskBtnClicked(self):
if self.belowThresholdAction.isChecked():
if self.minLineEdit.text():
- self._mask.updateBelowThreshold(self.levelSpinBox.value(),
- self.minLineEdit.value())
+ self._mask.updateBelowThreshold(
+ self.levelSpinBox.value(), self.minLineEdit.value()
+ )
self._mask.commit()
elif self.betweenThresholdAction.isChecked():
if self.minLineEdit.text() and self.maxLineEdit.text():
min_ = self.minLineEdit.value()
max_ = self.maxLineEdit.value()
- self._mask.updateBetweenThresholds(self.levelSpinBox.value(),
- min_, max_)
+ self._mask.updateBetweenThresholds(
+ self.levelSpinBox.value(), min_, max_
+ )
self._mask.commit()
elif self.aboveThresholdAction.isChecked():
if self.maxLineEdit.text():
max_ = float(self.maxLineEdit.value())
- self._mask.updateAboveThreshold(self.levelSpinBox.value(),
- max_)
+ self._mask.updateAboveThreshold(self.levelSpinBox.value(), max_)
self._mask.commit()
def _maskNotFiniteBtnClicked(self):
"""Handle not finite mask button clicked: mask NaNs and inf"""
- self._mask.updateNotFinite(
- self.levelSpinBox.value())
+ self._mask.updateNotFinite(self.levelSpinBox.value())
self._mask.commit()
@@ -1203,7 +1239,7 @@ class BaseMaskToolsDockWidget(qt.QDockWidget):
sigMaskChanged = qt.Signal()
- def __init__(self, parent=None, name='Mask', widget=None):
+ def __init__(self, parent=None, name="Mask", widget=None):
super(BaseMaskToolsDockWidget, self).__init__(parent)
self.setWindowTitle(name)
@@ -1257,7 +1293,7 @@ class BaseMaskToolsDockWidget(qt.QDockWidget):
See :class:`QMainWindow`.
"""
action = super(BaseMaskToolsDockWidget, self).toggleViewAction()
- action.setIcon(icons.getQIcon('image-mask'))
+ action.setIcon(icons.getQIcon("image-mask"))
action.setToolTip("Display/hide mask tools")
return action
@@ -1273,10 +1309,3 @@ class BaseMaskToolsDockWidget(qt.QDockWidget):
self.widget().setDirection(qt.QBoxLayout.LeftToRight)
self.resize(self.widget().minimumSize())
self.adjustSize()
-
- def showEvent(self, event):
- """Make sure this widget is raised when it is shown
- (when it is first created as a tab in PlotWindow or when it is shown
- again after hiding).
- """
- self.raise_()
diff --git a/src/silx/gui/plot/__init__.py b/src/silx/gui/plot/__init__.py
index 3a141b3..2a1587f 100644
--- a/src/silx/gui/plot/__init__.py
+++ b/src/silx/gui/plot/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
@@ -67,5 +66,13 @@ from .ImageView import ImageView # noqa
from .StackView import StackView # noqa
from .ScatterView import ScatterView # noqa
-__all__ = ['ImageView', 'PlotWidget', 'PlotWindow', 'Plot1D', 'Plot2D',
- 'StackView', 'ScatterView', 'TickMode']
+__all__ = [
+ "ImageView",
+ "PlotWidget",
+ "PlotWindow",
+ "Plot1D",
+ "Plot2D",
+ "StackView",
+ "ScatterView",
+ "TickMode",
+]
diff --git a/src/silx/gui/plot/_utils/__init__.py b/src/silx/gui/plot/_utils/__init__.py
index ed87b18..3075007 100644
--- a/src/silx/gui/plot/_utils/__init__.py
+++ b/src/silx/gui/plot/_utils/__init__.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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,11 +31,12 @@ __date__ = "21/03/2017"
import numpy
from .panzoom import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX
-from .panzoom import applyZoomToPlot, applyPan, checkAxisLimits
+from .panzoom import applyZoomToPlot, applyPan, checkAxisLimits, EnabledAxes
-def addMarginsToLimits(margins, isXLog, isYLog,
- xMin, xMax, yMin, yMax, y2Min=None, y2Max=None):
+def addMarginsToLimits(
+ margins, isXLog, isYLog, xMin, xMax, yMin, yMax, y2Min=None, y2Max=None
+):
"""Returns updated limits by extending them with margins.
:param margins: The ratio of the margins to add or None for no margins.
@@ -56,35 +56,35 @@ def addMarginsToLimits(margins, isXLog, isYLog,
xMin -= xMinMargin * xRange
xMax += xMaxMargin * xRange
- elif xMin > 0. and xMax > 0.: # Log scale
+ elif xMin > 0.0 and xMax > 0.0: # Log scale
# Do not apply margins if limits < 0
xMinLog, xMaxLog = numpy.log10(xMin), numpy.log10(xMax)
xRangeLog = xMaxLog - xMinLog
- xMin = pow(10., xMinLog - xMinMargin * xRangeLog)
- xMax = pow(10., xMaxLog + xMaxMargin * xRangeLog)
+ xMin = pow(10.0, xMinLog - xMinMargin * xRangeLog)
+ xMax = pow(10.0, xMaxLog + xMaxMargin * xRangeLog)
if not isYLog:
yRange = yMax - yMin
yMin -= yMinMargin * yRange
yMax += yMaxMargin * yRange
- elif yMin > 0. and yMax > 0.: # Log scale
+ elif yMin > 0.0 and yMax > 0.0: # Log scale
# Do not apply margins if limits < 0
yMinLog, yMaxLog = numpy.log10(yMin), numpy.log10(yMax)
yRangeLog = yMaxLog - yMinLog
- yMin = pow(10., yMinLog - yMinMargin * yRangeLog)
- yMax = pow(10., yMaxLog + yMaxMargin * yRangeLog)
+ yMin = pow(10.0, yMinLog - yMinMargin * yRangeLog)
+ yMax = pow(10.0, yMaxLog + yMaxMargin * yRangeLog)
if y2Min is not None and y2Max is not None:
if not isYLog:
yRange = y2Max - y2Min
y2Min -= yMinMargin * yRange
y2Max += yMaxMargin * yRange
- elif y2Min > 0. and y2Max > 0.: # Log scale
+ elif y2Min > 0.0 and y2Max > 0.0: # Log scale
# Do not apply margins if limits < 0
yMinLog, yMaxLog = numpy.log10(y2Min), numpy.log10(y2Max)
yRangeLog = yMaxLog - yMinLog
- y2Min = pow(10., yMinLog - yMinMargin * yRangeLog)
- y2Max = pow(10., yMaxLog + yMaxMargin * yRangeLog)
+ y2Min = pow(10.0, yMinLog - yMinMargin * yRangeLog)
+ y2Max = pow(10.0, yMaxLog + yMaxMargin * yRangeLog)
if y2Min is None or y2Max is None:
return xMin, xMax, yMin, yMax
diff --git a/src/silx/gui/plot/_utils/delaunay.py b/src/silx/gui/plot/_utils/delaunay.py
deleted file mode 100644
index 49ad05f..0000000
--- a/src/silx/gui/plot/_utils/delaunay.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2019 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Wrapper over Delaunay implementation"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "02/05/2019"
-
-
-import logging
-import sys
-
-import numpy
-
-
-_logger = logging.getLogger(__name__)
-
-
-def delaunay(x, y):
- """Returns Delaunay instance for x, y points
-
- :param numpy.ndarray x:
- :param numpy.ndarray y:
- :rtype: Union[None,scipy.spatial.Delaunay]
- """
- # Lazy-loading of Delaunay
- try:
- from scipy.spatial import Delaunay as _Delaunay
- except ImportError: # Fallback using local Delaunay
- from silx.third_party.scipy_spatial import Delaunay as _Delaunay
-
- points = numpy.array((x, y)).T
- try:
- delaunay = _Delaunay(points)
- except (RuntimeError, ValueError):
- _logger.error("Delaunay tesselation failed: %s",
- sys.exc_info()[1])
- delaunay = None
-
- return delaunay
diff --git a/src/silx/gui/plot/_utils/dtime_ticklayout.py b/src/silx/gui/plot/_utils/dtime_ticklayout.py
index ebf775b..ba0fda7 100644
--- a/src/silx/gui/plot/_utils/dtime_ticklayout.py
+++ b/src/silx/gui/plot/_utils/dtime_ticklayout.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,15 +21,16 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This module implements date-time labels layout on graph axes."""
+from __future__ import annotations
-from __future__ import absolute_import, division, unicode_literals
+"""This module implements date-time labels layout on graph axes."""
__authors__ = ["P. Kenter"]
__license__ = "MIT"
__date__ = "04/04/2018"
+from collections.abc import Sequence
import datetime as dt
import enum
import logging
@@ -51,14 +51,15 @@ SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
SECONDS_PER_YEAR = 365.25 * SECONDS_PER_DAY
-SECONDS_PER_MONTH_AVERAGE = SECONDS_PER_YEAR / 12 # Seconds per average month
+SECONDS_PER_MONTH_AVERAGE = SECONDS_PER_YEAR / 12 # Seconds per average month
# No dt.timezone in Python 2.7 so we use dateutil.tz.tzutc
_EPOCH = dt.datetime(1970, 1, 1, tzinfo=dateutil.tz.tzutc())
+
def timestamp(dtObj):
- """ Returns POSIX timestamp of a datetime objects.
+ """Returns POSIX timestamp of a datetime objects.
If the dtObj object has a timestamp() method (python 3.3), this is
used. Otherwise (e.g. python 2.7) it is calculated here.
@@ -76,9 +77,22 @@ def timestamp(dtObj):
else:
# Back ported from Python 3.5
if dtObj.tzinfo is None:
- return time.mktime((dtObj.year, dtObj.month, dtObj.day,
- dtObj.hour, dtObj.minute, dtObj.second,
- -1, -1, -1)) + dtObj.microsecond / 1e6
+ return (
+ time.mktime(
+ (
+ dtObj.year,
+ dtObj.month,
+ dtObj.day,
+ dtObj.hour,
+ dtObj.minute,
+ dtObj.second,
+ -1,
+ -1,
+ -1,
+ )
+ )
+ + dtObj.microsecond / 1e6
+ )
else:
return (dtObj - _EPOCH).total_seconds()
@@ -95,7 +109,7 @@ class DtUnit(enum.Enum):
def getDateElement(dateTime, unit):
- """ Picks the date element with the unit from the dateTime
+ """Picks the date element with the unit from the dateTime
E.g. getDateElement(datetime(1970, 5, 6), DtUnit.Day) will return 6
@@ -121,7 +135,7 @@ def getDateElement(dateTime, unit):
def setDateElement(dateTime, value, unit):
- """ Returns a copy of dateTime with the tickStep unit set to value
+ """Returns a copy of dateTime with the tickStep unit set to value
:param datetime.datetime: date time object
:param int value: value to set
@@ -129,8 +143,9 @@ def setDateElement(dateTime, value, unit):
:return: datetime.datetime
"""
intValue = int(value)
- _logger.debug("setDateElement({}, {} (int={}), {})"
- .format(dateTime, value, intValue, unit))
+ _logger.debug(
+ "setDateElement({}, {} (int={}), {})".format(dateTime, value, intValue, unit)
+ )
year = dateTime.year
month = dateTime.month
@@ -157,16 +172,19 @@ def setDateElement(dateTime, value, unit):
else:
raise ValueError("Unexpected DtUnit: {}".format(unit))
- _logger.debug("creating date time {}"
- .format((year, month, day, hour, minute, second, microsecond)))
-
- return dt.datetime(year, month, day, hour, minute, second, microsecond,
- tzinfo=dateTime.tzinfo)
+ _logger.debug(
+ "creating date time {}".format(
+ (year, month, day, hour, minute, second, microsecond)
+ )
+ )
+ return dt.datetime(
+ year, month, day, hour, minute, second, microsecond, tzinfo=dateTime.tzinfo
+ )
def roundToElement(dateTime, unit):
- """ Returns a copy of dateTime rounded to given unit
+ """Returns a copy of dateTime rounded to given unit
:param datetime.datetime: date time object
:param DtUnit unit: unit
@@ -181,7 +199,7 @@ def roundToElement(dateTime, unit):
microsecond = dateTime.microsecond
if unit.value < DtUnit.YEARS.value:
- pass # Never round years
+ pass # Never round years
if unit.value < DtUnit.MONTHS.value:
month = 1
if unit.value < DtUnit.DAYS.value:
@@ -195,14 +213,15 @@ def roundToElement(dateTime, unit):
if unit.value < DtUnit.MICRO_SECONDS.value:
microsecond = 0
- result = dt.datetime(year, month, day, hour, minute, second, microsecond,
- tzinfo=dateTime.tzinfo)
+ result = dt.datetime(
+ year, month, day, hour, minute, second, microsecond, tzinfo=dateTime.tzinfo
+ )
return result
def addValueToDate(dateTime, value, unit):
- """ Adds a value with unit to a dateTime.
+ """Adds a value with unit to a dateTime.
Uses dateutil.relativedelta.relativedelta from the standard library to do
the actual math. This function doesn't allow for fractional month or years,
@@ -212,14 +231,15 @@ def addValueToDate(dateTime, value, unit):
:param float value: value to be added
:param DtUnit unit: of the value
:return:
+ :raises ValueError: unit is unsupported or result is out of datetime bounds
"""
- #logger.debug("addValueToDate({}, {}, {})".format(dateTime, value, unit))
+ # logger.debug("addValueToDate({}, {}, {})".format(dateTime, value, unit))
if unit == DtUnit.YEARS:
- intValue = int(value) # floats not implemented in relativeDelta(years)
+ intValue = int(value) # floats not implemented in relativeDelta(years)
return dateTime + relativedelta(years=intValue)
elif unit == DtUnit.MONTHS:
- intValue = int(value) # floats not implemented in relativeDelta(mohths)
+ intValue = int(value) # floats not implemented in relativeDelta(mohths)
return dateTime + relativedelta(months=intValue)
elif unit == DtUnit.DAYS:
return dateTime + relativedelta(days=value)
@@ -236,7 +256,7 @@ def addValueToDate(dateTime, value, unit):
def bestUnit(durationInSeconds):
- """ Gets the best tick spacing given a duration in seconds.
+ """Gets the best tick spacing given a duration in seconds.
:param durationInSeconds: time span duration in seconds
:return: DtUnit enumeration.
@@ -266,8 +286,7 @@ def bestUnit(durationInSeconds):
elif durationInSeconds > 1 * 2:
return (durationInSeconds, DtUnit.SECONDS)
else:
- return (durationInSeconds * MICROSECONDS_PER_SECOND,
- DtUnit.MICRO_SECONDS)
+ return (durationInSeconds * MICROSECONDS_PER_SECOND, DtUnit.MICRO_SECONDS)
NICE_DATE_VALUES = {
@@ -277,12 +296,12 @@ NICE_DATE_VALUES = {
DtUnit.HOURS: [1, 2, 3, 4, 6, 12],
DtUnit.MINUTES: [1, 2, 3, 5, 10, 15, 30],
DtUnit.SECONDS: [1, 2, 3, 5, 10, 15, 30],
- DtUnit.MICRO_SECONDS : [1.0, 2.0, 5.0, 10.0], # floats for microsec
+ DtUnit.MICRO_SECONDS: [1.0, 2.0, 3.0, 4.0, 5.0, 10.0], # floats for microsec
}
def bestFormatString(spacing, unit):
- """ Finds the best format string given the spacing and DtUnit.
+ """Finds the best format string given the spacing and DtUnit.
If the spacing is a fractional number < 1 the format string will take this
into account
@@ -312,8 +331,31 @@ def bestFormatString(spacing, unit):
raise ValueError("Unexpected DtUnit: {}".format(unit))
+def formatDatetimes(
+ datetimes: Sequence[dt.datetime], spacing: int | None, unit: DtUnit | None
+) -> dict[dt.datetime, str]:
+ """Returns formatted string for each datetime according to tick spacing and time unit"""
+ if spacing is None or unit is None:
+ # Locator has no spacing or units yet: Use elaborate fmtString
+ return {
+ datetime: datetime.strftime("Y-%m-%d %H:%M:%S") for datetime in datetimes
+ }
+
+ formatString = bestFormatString(spacing, unit)
+ if unit != DtUnit.MICRO_SECONDS:
+ return {datetime: datetime.strftime(formatString) for datetime in datetimes}
+
+ # For microseconds: Strip leading/trailing zeros
+ texts = tuple(datetime.strftime(formatString) for datetime in datetimes)
+ nzeros = min(len(text) - len(text.rstrip("0")) for text in texts)
+ return {
+ datetime: text[0 if text[0] != "0" else 1 : -min(nzeros, 5)]
+ for datetime, text in zip(datetimes, texts)
+ }
+
+
def niceDateTimeElement(value, unit, isRound=False):
- """ Uses the Nice Numbers algorithm to determine a nice value.
+ """Uses the Nice Numbers algorithm to determine a nice value.
The fractions are optimized for the unit of the date element.
"""
@@ -328,10 +370,8 @@ def niceDateTimeElement(value, unit, isRound=False):
def findStartDate(dMin, dMax, nTicks):
- """ Rounds a date down to the nearest nice number of ticks
- """
- assert dMax >= dMin, \
- "dMin ({}) should come before dMax ({})".format(dMin, dMax)
+ """Rounds a date down to the nearest nice number of ticks"""
+ assert dMax >= dMin, "dMin ({}) should come before dMax ({})".format(dMin, dMax)
if dMin == dMax:
# Fallback when range is smaller than microsecond resolution
@@ -339,31 +379,42 @@ def findStartDate(dMin, dMax, nTicks):
delta = dMax - dMin
lengthSec = delta.total_seconds()
- _logger.debug("findStartDate: {}, {} (duration = {} sec, {} days)"
- .format(dMin, dMax, lengthSec, lengthSec / SECONDS_PER_DAY))
+ _logger.debug(
+ "findStartDate: {}, {} (duration = {} sec, {} days)".format(
+ dMin, dMax, lengthSec, lengthSec / SECONDS_PER_DAY
+ )
+ )
length, unit = bestUnit(lengthSec)
niceLength = niceDateTimeElement(length, unit)
- _logger.debug("Length: {:8.3f} {} (nice = {})"
- .format(length, unit.name, niceLength))
+ _logger.debug(
+ "Length: {:8.3f} {} (nice = {})".format(length, unit.name, niceLength)
+ )
niceSpacing = niceDateTimeElement(niceLength / nTicks, unit, isRound=True)
- _logger.debug("Spacing: {:8.3f} {} (nice = {})"
- .format(niceLength / nTicks, unit.name, niceSpacing))
+ _logger.debug(
+ "Spacing: {:8.3f} {} (nice = {})".format(
+ niceLength / nTicks, unit.name, niceSpacing
+ )
+ )
dVal = getDateElement(dMin, unit)
- if unit == DtUnit.MONTHS: # TODO: better rounding?
- niceVal = math.floor((dVal-1) / niceSpacing) * niceSpacing + 1
+ if unit == DtUnit.MONTHS: # TODO: better rounding?
+ niceVal = math.floor((dVal - 1) / niceSpacing) * niceSpacing + 1
elif unit == DtUnit.DAYS:
- niceVal = math.floor((dVal-1) / niceSpacing) * niceSpacing + 1
+ niceVal = math.floor((dVal - 1) / niceSpacing) * niceSpacing + 1
else:
niceVal = math.floor(dVal / niceSpacing) * niceSpacing
- _logger.debug("StartValue: dVal = {}, niceVal: {} ({})"
- .format(dVal, niceVal, unit.name))
+ if unit == DtUnit.YEARS and niceVal <= dt.MINYEAR:
+ niceVal = max(1, niceSpacing)
+
+ _logger.debug(
+ "StartValue: dVal = {}, niceVal: {} ({})".format(dVal, niceVal, unit.name)
+ )
startDate = roundToElement(dMin, unit)
startDate = setDateElement(startDate, niceVal, unit)
@@ -371,8 +422,8 @@ def findStartDate(dMin, dMax, nTicks):
return startDate, niceSpacing, unit
-def dateRange(dMin, dMax, step, unit, includeFirstBeyond = False):
- """ Generates a range of dates
+def dateRange(dMin, dMax, step, unit, includeFirstBeyond=False):
+ """Generates a range of dates
:param datetime dMin: start date
:param datetime dMax: end date
@@ -383,8 +434,7 @@ def dateRange(dMin, dMax, step, unit, includeFirstBeyond = False):
datetime will always be smaller than dMax.
:return:
"""
- if (unit == DtUnit.YEARS or unit == DtUnit.MONTHS or
- unit == DtUnit.MICRO_SECONDS):
+ if unit == DtUnit.YEARS or unit == DtUnit.MONTHS or unit == DtUnit.MICRO_SECONDS:
# No support for fractional month or year and resolution is microsecond
# In those cases, make sure the step is at least 1
step = max(1, step)
@@ -394,13 +444,15 @@ def dateRange(dMin, dMax, step, unit, includeFirstBeyond = False):
dateTime = dMin
while dateTime < dMax:
yield dateTime
- dateTime = addValueToDate(dateTime, step, unit)
+ try:
+ dateTime = addValueToDate(dateTime, step, unit)
+ except ValueError:
+ return # current dateTime is out of datetime bounds
if includeFirstBeyond:
yield dateTime
-
def calcTicks(dMin, dMax, nTicks):
"""Returns tick positions.
@@ -410,33 +462,19 @@ def calcTicks(dMin, dMax, nTicks):
ticks may differ.
:returns: (list of datetimes, DtUnit) tuple
"""
- _logger.debug("Calc calcTicks({}, {}, nTicks={})"
- .format(dMin, dMax, nTicks))
+ _logger.debug("Calc calcTicks({}, {}, nTicks={})".format(dMin, dMax, nTicks))
startDate, niceSpacing, unit = findStartDate(dMin, dMax, nTicks)
result = []
- for d in dateRange(startDate, dMax, niceSpacing, unit,
- includeFirstBeyond=True):
+ for d in dateRange(startDate, dMax, niceSpacing, unit, includeFirstBeyond=True):
result.append(d)
- assert result[0] <= dMin, \
- "First nice date ({}) should be <= dMin {}".format(result[0], dMin)
-
- assert result[-1] >= dMax, \
- "Last nice date ({}) should be >= dMax {}".format(result[-1], dMax)
-
return result, niceSpacing, unit
def calcTicksAdaptive(dMin, dMax, axisLength, tickDensity):
- """ Calls calcTicks with a variable number of ticks, depending on axisLength
- """
+ """Calls calcTicks with a variable number of ticks, depending on axisLength"""
# At least 2 ticks
nticks = max(2, int(round(tickDensity * axisLength)))
- return calcTicks(dMin, dMax, nticks)
-
-
-
-
-
+ return calcTicks(dMin, dMax, nticks)
diff --git a/src/silx/gui/plot/_utils/panzoom.py b/src/silx/gui/plot/_utils/panzoom.py
index 77efd10..cac591d 100644
--- a/src/silx/gui/plot/_utils/panzoom.py
+++ b/src/silx/gui/plot/_utils/panzoom.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,6 +23,8 @@
# ###########################################################################*/
"""Functions to apply pan and zoom on a Plot"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent", "V. Valls"]
__license__ = "MIT"
__date__ = "08/08/2017"
@@ -31,6 +32,7 @@ __date__ = "08/08/2017"
import logging
import math
+from typing import NamedTuple
import numpy
@@ -47,11 +49,11 @@ FLOAT32_SAFE_MAX = 1e37
# TODO double support
-def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""):
+def checkAxisLimits(vmin: float, vmax: float, isLog: bool = False, name: str = ""):
"""Makes sure axis range is not empty and within supported range.
- :param float vmin: Min axis value
- :param float vmax: Max axis value
+ :param vmin: Min axis value
+ :param vmax: Max axis value
:return: (min, max) making sure min < max
:rtype: 2-tuple of float
"""
@@ -60,11 +62,11 @@ def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""):
vmin = numpy.clip(vmin, min_, FLOAT32_SAFE_MAX)
if vmax < vmin:
- _logger.debug('%s axis: max < min, inverting limits.', name)
+ _logger.debug("%s axis: max < min, inverting limits.", name)
vmin, vmax = vmax, vmin
elif vmax == vmin:
- _logger.debug('%s axis: max == min, expanding limits.', name)
- if vmin == 0.:
+ _logger.debug("%s axis: max == min, expanding limits.", name)
+ if vmin == 0.0:
vmin, vmax = -0.1, 0.1
elif vmin < 0:
vmax *= 0.9
@@ -76,26 +78,27 @@ def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""):
return vmin, vmax
-def scale1DRange(min_, max_, center, scale, isLog):
+def scale1DRange(
+ min_: float, max_: float, center: float, scale: float, isLog: bool
+) -> tuple[float, float]:
"""Scale a 1D range given a scale factor and an center point.
Keeps the values in a smaller range than float32.
- :param float min_: The current min value of the range.
- :param float max_: The current max value of the range.
- :param float center: The center of the zoom (i.e., invariant point).
- :param float scale: The scale to use for zoom
- :param bool isLog: Whether using log scale or not.
- :return: The zoomed range.
- :rtype: tuple of 2 floats: (min, max)
+ :param min_: The current min value of the range.
+ :param max_: The current max value of the range.
+ :param center: The center of the zoom (i.e., invariant point).
+ :param scale: The scale to use for zoom
+ :param isLog: Whether using log scale or not.
+ :return: The zoomed range (min, max)
"""
if isLog:
# Min and center can be < 0 when
# autoscale is off and switch to log scale
# max_ < 0 should not happen
- min_ = numpy.log10(min_) if min_ > 0. else FLOAT32_MINPOS
- center = numpy.log10(center) if center > 0. else FLOAT32_MINPOS
- max_ = numpy.log10(max_) if max_ > 0. else FLOAT32_MINPOS
+ min_ = numpy.log10(min_) if min_ > 0.0 else FLOAT32_MINPOS
+ center = numpy.log10(center) if center > 0.0 else FLOAT32_MINPOS
+ max_ = numpy.log10(max_) if max_ > 0.0 else FLOAT32_MINPOS
if min_ == max_:
return min_, max_
@@ -103,12 +106,12 @@ def scale1DRange(min_, max_, center, scale, isLog):
offset = (center - min_) / (max_ - min_)
range_ = (max_ - min_) / scale
newMin = center - offset * range_
- newMax = center + (1. - offset) * range_
+ newMax = center + (1.0 - offset) * range_
if isLog:
# No overflow as exponent is log10 of a float32
- newMin = pow(10., newMin)
- newMax = pow(10., newMax)
+ newMin = pow(10.0, newMin)
+ newMax = pow(10.0, newMax)
newMin = numpy.clip(newMin, FLOAT32_MINPOS, FLOAT32_SAFE_MAX)
newMax = numpy.clip(newMax, FLOAT32_MINPOS, FLOAT32_SAFE_MAX)
else:
@@ -117,16 +120,34 @@ def scale1DRange(min_, max_, center, scale, isLog):
return newMin, newMax
-def applyZoomToPlot(plot, scaleF, center=None):
+class EnabledAxes(NamedTuple):
+ """Toggle zoom for each axis"""
+
+ xaxis: bool = True
+ yaxis: bool = True
+ y2axis: bool = True
+
+ def isDisabled(self) -> bool:
+ """True only if all axes are disabled"""
+ return not (self.xaxis or self.yaxis or self.y2axis)
+
+
+def applyZoomToPlot(
+ plot,
+ scale: float,
+ center: tuple[float, float] = None,
+ enabled: EnabledAxes = EnabledAxes(),
+):
"""Zoom in/out plot given a scale and a center point.
:param plot: The plot on which to apply zoom.
- :param float scaleF: Scale factor of zoom.
+ :param scale: Scale factor of zoom.
:param center: (x, y) coords in pixel coordinates of the zoom center.
- :type center: 2-tuple of float
+ :param enabled: Toggle zoom for each axis independently
"""
xMin, xMax = plot.getXAxis().getLimits()
yMin, yMax = plot.getYAxis().getLimits()
+ y2Min, y2Max = plot.getYAxis(axis="right").getLimits()
if center is None:
left, top, width, height = plot.getPlotBoundsInPixels()
@@ -137,18 +158,23 @@ def applyZoomToPlot(plot, scaleF, center=None):
dataCenterPos = plot.pixelToData(cx, cy)
assert dataCenterPos is not None
- xMin, xMax = scale1DRange(xMin, xMax, dataCenterPos[0], scaleF,
- plot.getXAxis()._isLogarithmic())
+ if enabled.xaxis:
+ xMin, xMax = scale1DRange(
+ xMin, xMax, dataCenterPos[0], scale, plot.getXAxis()._isLogarithmic()
+ )
- yMin, yMax = scale1DRange(yMin, yMax, dataCenterPos[1], scaleF,
- plot.getYAxis()._isLogarithmic())
+ if enabled.yaxis:
+ yMin, yMax = scale1DRange(
+ yMin, yMax, dataCenterPos[1], scale, plot.getYAxis()._isLogarithmic()
+ )
- dataPos = plot.pixelToData(cx, cy, axis="right")
- assert dataPos is not None
- y2Center = dataPos[1]
- y2Min, y2Max = plot.getYAxis(axis="right").getLimits()
- y2Min, y2Max = scale1DRange(y2Min, y2Max, y2Center, scaleF,
- plot.getYAxis()._isLogarithmic())
+ if enabled.y2axis:
+ dataPos = plot.pixelToData(cx, cy, axis="right")
+ assert dataPos is not None
+ y2Center = dataPos[1]
+ y2Min, y2Max = scale1DRange(
+ y2Min, y2Max, y2Center, scale, plot.getYAxis()._isLogarithmic()
+ )
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
@@ -167,15 +193,15 @@ def applyPan(min_, max_, panFactor, isLog10):
:return: New min and max value with pan applied.
:rtype: 2-tuple of float.
"""
- if isLog10 and min_ > 0.:
+ if isLog10 and min_ > 0.0:
# Negative range and log scale can happen with matplotlib
logMin, logMax = math.log10(min_), math.log10(max_)
logOffset = panFactor * (logMax - logMin)
- newMin = pow(10., logMin + logOffset)
- newMax = pow(10., logMax + logOffset)
+ newMin = pow(10.0, logMin + logOffset)
+ newMax = pow(10.0, logMax + logOffset)
# Takes care of out-of-range values
- if newMin > 0. and newMax < float('inf'):
+ if newMin > 0.0 and newMax < float("inf"):
min_, max_ = newMin, newMax
else:
@@ -183,13 +209,14 @@ def applyPan(min_, max_, panFactor, isLog10):
newMin, newMax = min_ + offset, max_ + offset
# Takes care of out-of-range values
- if newMin > - float('inf') and newMax < float('inf'):
+ if newMin > -float("inf") and newMax < float("inf"):
min_, max_ = newMin, newMax
return min_, max_
class _Unset(object):
"""To be able to have distinction between None and unset"""
+
pass
@@ -204,10 +231,17 @@ class ViewConstraints(object):
self._minRange = [None, None]
self._maxRange = [None, None]
- def update(self, xMin=_Unset, xMax=_Unset,
- yMin=_Unset, yMax=_Unset,
- minXRange=_Unset, maxXRange=_Unset,
- minYRange=_Unset, maxYRange=_Unset):
+ def update(
+ self,
+ xMin=_Unset,
+ xMax=_Unset,
+ yMin=_Unset,
+ yMax=_Unset,
+ minXRange=_Unset,
+ maxXRange=_Unset,
+ minYRange=_Unset,
+ maxYRange=_Unset,
+ ):
"""
Update the constraints managed by the object
@@ -239,7 +273,6 @@ class ViewConstraints(object):
maxPos = [xMax, yMax]
for axis in range(2):
-
value = minPos[axis]
if value is not _Unset and value != self._min[axis]:
self._min[axis] = value
@@ -263,7 +296,11 @@ class ViewConstraints(object):
# Sanity checks
for axis in range(2):
- if self._maxRange[axis] is not None and self._min[axis] is not None and self._max[axis] is not None:
+ if (
+ self._maxRange[axis] is not None
+ and self._min[axis] is not None
+ and self._max[axis] is not None
+ ):
# max range cannot be larger than bounds
diff = self._max[axis] - self._min[axis]
self._maxRange[axis] = min(self._maxRange[axis], diff)
@@ -299,8 +336,12 @@ class ViewConstraints(object):
viewRange[axis][1] += delta * 0.5
# clamp min and max positions
- outMin = self._min[axis] is not None and viewRange[axis][0] < self._min[axis]
- outMax = self._max[axis] is not None and viewRange[axis][1] > self._max[axis]
+ outMin = (
+ self._min[axis] is not None and viewRange[axis][0] < self._min[axis]
+ )
+ outMax = (
+ self._max[axis] is not None and viewRange[axis][1] > self._max[axis]
+ )
if outMin and outMax:
if allow_scaling:
diff --git a/src/silx/gui/plot/_utils/test/__init__.py b/src/silx/gui/plot/_utils/test/__init__.py
index 3ad225d..78821ec 100644
--- a/src/silx/gui/plot/_utils/test/__init__.py
+++ b/src/silx/gui/plot/_utils/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py b/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py
index 8d35acf..adcb9c9 100644
--- a/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py
+++ b/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2022 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
@@ -23,57 +22,66 @@
#
# ###########################################################################*/
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["P. Kenter"]
__license__ = "MIT"
__date__ = "06/04/2018"
import datetime as dt
-import unittest
+import pytest
+
+from silx.gui.plot._utils.dtime_ticklayout import calcTicks, DtUnit, SECONDS_PER_YEAR
-from silx.gui.plot._utils.dtime_ticklayout import (
- calcTicks, DtUnit, SECONDS_PER_YEAR)
+def testSmallMonthlySpacing():
+ """Tests a range that did result in a spacing of less than 1 month.
+ It is impossible to add fractional month so the unit must be in days
+ """
+ from dateutil import parser
-class TestTickLayout(unittest.TestCase):
- """Test ticks layout algorithms"""
+ d1 = parser.parse("2017-01-03 13:15:06.000044")
+ d2 = parser.parse("2017-03-08 09:16:16.307584")
+ _ticks, _units, spacing = calcTicks(d1, d2, nTicks=4)
- def testSmallMonthlySpacing(self):
- """ Tests a range that did result in a spacing of less than 1 month.
- It is impossible to add fractional month so the unit must be in days
- """
- from dateutil import parser
- d1 = parser.parse("2017-01-03 13:15:06.000044")
- d2 = parser.parse("2017-03-08 09:16:16.307584")
- _ticks, _units, spacing = calcTicks(d1, d2, nTicks=4)
+ assert spacing == DtUnit.DAYS
- self.assertEqual(spacing, DtUnit.DAYS)
+def testNoCrash():
+ """Creates many combinations of and number-of-ticks and end-dates;
+ tests that it doesn't give an exception and returns a reasonable number
+ of ticks.
+ """
+ d1 = dt.datetime(2017, 1, 3, 13, 15, 6, 44)
- def testNoCrash(self):
- """ Creates many combinations of and number-of-ticks and end-dates;
- tests that it doesn't give an exception and returns a reasonable number
- of ticks.
- """
- d1 = dt.datetime(2017, 1, 3, 13, 15, 6, 44)
+ value = 100e-6 # Start at 100 micro sec range.
- value = 100e-6 # Start at 100 micro sec range.
+ while value <= 200 * SECONDS_PER_YEAR:
+ d2 = d1 + dt.timedelta(microseconds=value * 1e6) # end date range
- while value <= 200 * SECONDS_PER_YEAR:
+ for numTicks in range(2, 12):
+ ticks, _, _ = calcTicks(d1, d2, numTicks)
- d2 = d1 + dt.timedelta(microseconds=value*1e6) # end date range
+ margin = 2.5
+ assert (
+ numTicks / margin <= len(ticks) <= numTicks * margin
+ ), "Condition {} <= {} <= {} failed for # ticks={} and d2={}:".format(
+ numTicks / margin, len(ticks), numTicks * margin, numTicks, d2
+ )
- for numTicks in range(2, 12):
- ticks, _, _ = calcTicks(d1, d2, numTicks)
+ value = value * 1.5 # let date period grow exponentially
- margin = 2.5
- self.assertTrue(
- numTicks/margin <= len(ticks) <= numTicks*margin,
- "Condition {} <= {} <= {} failed for # ticks={} and d2={}:"
- .format(numTicks/margin, len(ticks), numTicks * margin,
- numTicks, d2))
- value = value * 1.5 # let date period grow exponentially
+@pytest.mark.parametrize(
+ "dMin, dMax",
+ [
+ (dt.datetime(1, 1, 1), dt.datetime(400, 1, 1)),
+ (dt.datetime(4000, 1, 1), dt.datetime(9999, 1, 1)),
+ (dt.datetime(1, 1, 1), dt.datetime(9999, 12, 23)),
+ ],
+)
+def testCalcTicksOutOfBoundTicks(dMin, dMax):
+ """Test tick generation with values leading to out-of-bound ticks"""
+ ticks, _, unit = calcTicks(dMin, dMax, nTicks=5)
+ assert len(ticks) != 0
+ assert unit == DtUnit.YEARS
diff --git a/src/silx/gui/plot/_utils/test/test_ticklayout.py b/src/silx/gui/plot/_utils/test/test_ticklayout.py
index 884b71b..1413563 100644
--- a/src/silx/gui/plot/_utils/test/test_ticklayout.py
+++ b/src/silx/gui/plot/_utils/test/test_ticklayout.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
@@ -23,14 +22,11 @@
#
# ###########################################################################*/
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "17/01/2018"
-import unittest
import numpy
from silx.utils.testutils import ParametricTestCase
@@ -44,10 +40,10 @@ class TestTickLayout(ParametricTestCase):
def testTicks(self):
"""Test of :func:`ticks`"""
tests = { # (vmin, vmax): ref_ticks
- (1., 1.): (1.,),
+ (1.0, 1.0): (1.0,),
(0.5, 10.5): (2.0, 4.0, 6.0, 8.0, 10.0),
- (0.001, 0.005): (0.001, 0.002, 0.003, 0.004, 0.005)
- }
+ (0.001, 0.005): (0.001, 0.002, 0.003, 0.004, 0.005),
+ }
for (vmin, vmax), ref_ticks in tests.items():
with self.subTest(vmin=vmin, vmax=vmax):
@@ -58,9 +54,9 @@ class TestTickLayout(ParametricTestCase):
"""Minimalistic tests of :func:`niceNumbers`"""
tests = { # (vmin, vmax): ref_ticks
(0.5, 10.5): (0.0, 12.0, 2.0, 0),
- (10000., 10000.5): (10000.0, 10000.5, 0.1, 1),
- (0.001, 0.005): (0.001, 0.005, 0.001, 3)
- }
+ (10000.0, 10000.5): (10000.0, 10000.5, 0.1, 1),
+ (0.001, 0.005): (0.001, 0.005, 0.001, 3),
+ }
for (vmin, vmax), ref_ticks in tests.items():
with self.subTest(vmin=vmin, vmax=vmax):
@@ -70,9 +66,9 @@ class TestTickLayout(ParametricTestCase):
def testNiceNumbersLog(self):
"""Minimalistic tests of :func:`niceNumbersForLog10`"""
tests = { # (log10(min), log10(max): ref_ticks
- (0., 3.): (0, 3, 1, 0),
- (-3., 3): (-3, 3, 1, 0),
- (-32., 0.): (-36, 0, 6, 0)
+ (0.0, 3.0): (0, 3, 1, 0),
+ (-3.0, 3): (-3, 3, 1, 0),
+ (-32.0, 0.0): (-36, 0, 6, 0),
}
for (vmin, vmax), ref_ticks in tests.items():
diff --git a/src/silx/gui/plot/_utils/ticklayout.py b/src/silx/gui/plot/_utils/ticklayout.py
index c9fd3e6..3678270 100644
--- a/src/silx/gui/plot/_utils/ticklayout.py
+++ b/src/silx/gui/plot/_utils/ticklayout.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module implements labels layout on graph axes."""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "18/10/2016"
@@ -36,6 +33,7 @@ import math
# utils #######################################################################
+
def numberOfDigits(tickSpacing):
"""Returns the number of digits to display for text label.
@@ -79,7 +77,7 @@ def numberOfDigits(tickSpacing):
def niceNumGeneric(value, niceFractions=None, isRound=False):
- """ A more generic implementation of the _niceNum function
+ """A more generic implementation of the _niceNum function
Allows the user to specify the fractions instead of using a hardcoded
list of [1, 2, 5, 10.0].
@@ -88,15 +86,15 @@ def niceNumGeneric(value, niceFractions=None, isRound=False):
return value
if niceFractions is None: # Use default values
- niceFractions = 1., 2., 5., 10.
- roundFractions = (1.5, 3., 7., 10.) if isRound else niceFractions
+ niceFractions = 1.0, 2.0, 5.0, 10.0
+ roundFractions = (1.5, 3.0, 7.0, 10.0) if isRound else niceFractions
else:
roundFractions = list(niceFractions)
if isRound:
# Take the average with the next element. The last remains the same.
for i in range(len(roundFractions) - 1):
- roundFractions[i] = (niceFractions[i] + niceFractions[i+1]) / 2
+ roundFractions[i] = (niceFractions[i] + niceFractions[i + 1]) / 2
highest = niceFractions[-1]
value = float(value)
@@ -136,7 +134,7 @@ def niceNumbers(vMin, vMax, nTicks=5):
def _frange(start, stop, step):
"""range for float (including stop)."""
- assert step >= 0.
+ assert step >= 0.0
while start <= stop:
yield start
start += step
@@ -169,7 +167,7 @@ def ticks(vMin, vMax, nbTicks=5):
nfrac = numberOfDigits(vMax - vMin)
# Generate labels
- format_ = '%g' if nfrac == 0 else '%.{}f'.format(nfrac)
+ format_ = "%g" if nfrac == 0 else "%.{}f".format(nfrac)
labels = [format_ % tick for tick in positions]
return positions, labels
@@ -197,6 +195,7 @@ def niceNumbersAdaptative(vMin, vMax, axisLength, tickDensity):
# Nice Numbers for log scale ##################################################
+
def niceNumbersForLog10(minLog, maxLog, nTicks=5):
"""Return tick positions for logarithmic scale
@@ -212,7 +211,7 @@ def niceNumbersForLog10(minLog, maxLog, nTicks=5):
rangelog = graphmaxlog - graphminlog
if rangelog <= nTicks:
- spacing = 1.
+ spacing = 1.0
else:
spacing = math.floor(rangelog / nTicks)
diff --git a/src/silx/gui/plot/actions/PlotAction.py b/src/silx/gui/plot/actions/PlotAction.py
index 2983775..9341bdd 100644
--- a/src/silx/gui/plot/actions/PlotAction.py
+++ b/src/silx/gui/plot/actions/PlotAction.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2017 European Synchrotron Radiation Facility
@@ -27,34 +26,41 @@ The class :class:`.PlotAction` help the creation of a qt.QAction associated
with a :class:`.PlotWidget`.
"""
-from __future__ import division
-
-
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "03/01/2018"
+from typing import Callable, Optional, Union
import weakref
from silx.gui import icons
from silx.gui import qt
+from silx.gui.plot import PlotWidget
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 icon: QIcon or name of icon to use
+ :param text: The name of this action to be used for menu label
+ :param 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)
+ signal. None for no callback (default)
+ :param 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):
+ def __init__(
+ self,
+ plot: PlotWidget,
+ icon: Union[str, qt.QIcon],
+ text: str,
+ tooltip: Optional[str] = None,
+ triggered: Optional[Callable] = None,
+ checkable: bool = False,
+ parent: Optional[qt.QObject] = None,
+ ):
assert plot is not None
self._plotRef = weakref.ref(plot)
diff --git a/src/silx/gui/plot/actions/PlotToolAction.py b/src/silx/gui/plot/actions/PlotToolAction.py
index fbb0b0f..479d7c2 100644
--- a/src/silx/gui/plot/actions/PlotToolAction.py
+++ b/src/silx/gui/plot/actions/PlotToolAction.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
@@ -27,9 +26,6 @@ The class :class:`.PlotToolAction` help the creation of a qt.QAction associating
a tool window with a :class:`.PlotWidget`.
"""
-from __future__ import division
-
-
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "10/10/2018"
@@ -45,16 +41,26 @@ class PlotToolAction(PlotAction):
"""Base class for QAction that maintain a tool window operating on a
PlotWidget."""
- def __init__(self, plot, icon, text, tooltip=None,
- triggered=None, checkable=False, parent=None):
- PlotAction.__init__(self,
- plot=plot,
- icon=icon,
- text=text,
- tooltip=tooltip,
- triggered=self._triggered,
- parent=parent,
- checkable=True)
+ def __init__(
+ self,
+ plot,
+ icon,
+ text,
+ tooltip=None,
+ triggered=None,
+ checkable=False,
+ parent=None,
+ ):
+ PlotAction.__init__(
+ self,
+ plot=plot,
+ icon=icon,
+ text=text,
+ tooltip=tooltip,
+ triggered=self._triggered,
+ parent=parent,
+ checkable=True,
+ )
self._previousGeometry = None
self._toolWindow = None
diff --git a/src/silx/gui/plot/actions/__init__.py b/src/silx/gui/plot/actions/__init__.py
index 930c728..3e606c6 100644
--- a/src/silx/gui/plot/actions/__init__.py
+++ b/src/silx/gui/plot/actions/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/actions/control.py b/src/silx/gui/plot/actions/control.py
index 439985e..c21d235 100755
--- a/src/silx/gui/plot/actions/control.py
+++ b/src/silx/gui/plot/actions/control.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -46,8 +45,6 @@ The following QAction are available:
- :class:`ZoomOutAction`
"""
-from __future__ import division
-
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "27/11/2020"
@@ -58,6 +55,7 @@ from silx.gui.plot import items
from silx.gui.plot._utils import applyZoomToPlot as _applyZoomToPlot
from silx.gui import qt
from silx.gui import icons
+from silx.utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -71,10 +69,14 @@ class ResetZoomAction(PlotAction):
def __init__(self, plot, parent=None):
super(ResetZoomAction, self).__init__(
- plot, icon='zoom-original', text='Reset Zoom',
- tooltip='Auto-scale the graph',
+ plot,
+ icon="zoom-original",
+ text="Reset Zoom",
+ tooltip="Auto-scale the graph",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self._autoscaleChanged(True)
plot.getXAxis().sigAutoScaleChanged.connect(self._autoscaleChanged)
plot.getYAxis().sigAutoScaleChanged.connect(self._autoscaleChanged)
@@ -85,13 +87,13 @@ class ResetZoomAction(PlotAction):
self.setEnabled(xAxis.isAutoScale() or yAxis.isAutoScale())
if xAxis.isAutoScale() and yAxis.isAutoScale():
- tooltip = 'Auto-scale the graph'
+ tooltip = "Auto-scale the graph"
elif xAxis.isAutoScale(): # And not Y axis
- tooltip = 'Auto-scale the x-axis of the graph only'
+ tooltip = "Auto-scale the x-axis of the graph only"
elif yAxis.isAutoScale(): # And not X axis
- tooltip = 'Auto-scale the y-axis of the graph only'
+ tooltip = "Auto-scale the y-axis of the graph only"
else: # no axis in autoscale
- tooltip = 'Auto-scale the graph'
+ tooltip = "Auto-scale the graph"
self.setToolTip(tooltip)
def _actionTriggered(self, checked=False):
@@ -107,10 +109,14 @@ class ZoomBackAction(PlotAction):
def __init__(self, plot, parent=None):
super(ZoomBackAction, self).__init__(
- plot, icon='zoom-back', text='Zoom Back',
- tooltip='Zoom back the plot',
+ plot,
+ icon="zoom-back",
+ text="Zoom Back",
+ tooltip="Zoom back the plot",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcutContext(qt.Qt.WidgetShortcut)
def _actionTriggered(self, checked=False):
@@ -126,10 +132,14 @@ class ZoomInAction(PlotAction):
def __init__(self, plot, parent=None):
super(ZoomInAction, self).__init__(
- plot, icon='zoom-in', text='Zoom In',
- tooltip='Zoom in the plot',
+ plot,
+ icon="zoom-in",
+ text="Zoom In",
+ tooltip="Zoom in the plot",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.ZoomIn)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -146,15 +156,19 @@ class ZoomOutAction(PlotAction):
def __init__(self, plot, parent=None):
super(ZoomOutAction, self).__init__(
- plot, icon='zoom-out', text='Zoom Out',
- tooltip='Zoom out the plot',
+ plot,
+ icon="zoom-out",
+ text="Zoom Out",
+ tooltip="Zoom out the plot",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ 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)
+ _applyZoomToPlot(self.plot, 1.0 / 1.1)
class XAxisAutoScaleAction(PlotAction):
@@ -166,11 +180,15 @@ class XAxisAutoScaleAction(PlotAction):
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.',
+ 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)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.getXAxis().isAutoScale())
plot.getXAxis().sigAutoScaleChanged.connect(self.setChecked)
@@ -189,11 +207,15 @@ class YAxisAutoScaleAction(PlotAction):
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.',
+ 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)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.getYAxis().isAutoScale())
plot.getYAxis().sigAutoScaleChanged.connect(self.setChecked)
@@ -212,10 +234,14 @@ class XAxisLogarithmicAction(PlotAction):
def __init__(self, plot, parent=None):
super(XAxisLogarithmicAction, self).__init__(
- plot, icon='plot-xlog', text='X Log. scale',
- tooltip='Logarithmic x-axis when checked',
+ plot,
+ icon="plot-xlog",
+ text="X Log. scale",
+ tooltip="Logarithmic x-axis when checked",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.axis = plot.getXAxis()
self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC)
self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale)
@@ -237,10 +263,14 @@ class YAxisLogarithmicAction(PlotAction):
def __init__(self, plot, parent=None):
super(YAxisLogarithmicAction, self).__init__(
- plot, icon='plot-ylog', text='Y Log. scale',
- tooltip='Logarithmic y-axis when checked',
+ plot,
+ icon="plot-ylog",
+ text="Y Log. scale",
+ tooltip="Logarithmic y-axis when checked",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.axis = plot.getYAxis()
self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC)
self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale)
@@ -262,21 +292,25 @@ class GridAction(PlotAction):
:param parent: See :class:`QAction`
"""
- def __init__(self, plot, gridMode='both', parent=None):
- assert gridMode in ('both', 'major')
+ 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)',
+ plot,
+ icon="plot-grid",
+ text="Grid",
+ tooltip="Toggle grid (on/off)",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ 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')
+ self.setChecked(which != "None")
def _actionTriggered(self, checked=False):
self.plot.setGraphGrid(self._gridMode if checked else None)
@@ -294,14 +328,17 @@ class CurveStyleAction(PlotAction):
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',
+ plot,
+ icon="plot-toggle-points",
+ text="Curve style",
+ tooltip="Change curve line and markers style",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
def _actionTriggered(self, checked=False):
- currentState = (self.plot.isDefaultPlotLines(),
- self.plot.isDefaultPlotPoints())
+ currentState = (self.plot.isDefaultPlotLines(), self.plot.isDefaultPlotPoints())
if currentState == (False, False):
newState = True, False
@@ -326,21 +363,39 @@ class ColormapAction(PlotAction):
def __init__(self, plot, parent=None):
self._dialog = None # To store an instance of ColormapDialog
super(ColormapAction, self).__init__(
- plot, icon='colormap', text='Colormap',
+ plot,
+ icon="colormap",
+ text="Colormap",
tooltip="Change colormap",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.plot.sigActiveImageChanged.connect(self._updateColormap)
self.plot.sigActiveScatterChanged.connect(self._updateColormap)
- def setColorDialog(self, colorDialog):
- """Set a specific color dialog instead of using the default dialog."""
- assert(colorDialog is not None)
- assert(self._dialog is None)
- self._dialog = colorDialog
- self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
+ def setColormapDialog(self, dialog):
+ """Set a specific colormap dialog instead of using the default one."""
+ assert dialog is not None
+ if self._dialog is not None:
+ self._dialog.visibleChanged.disconnect(self._dialogVisibleChanged)
+
+ self._dialog = dialog
+ self._dialog.visibleChanged.connect(
+ self._dialogVisibleChanged, qt.Qt.UniqueConnection
+ )
self.setChecked(self._dialog.isVisible())
+ @deprecated(replacement="setColormapDialog", since_version="2.0")
+ def setColorDialog(self, colorDialog):
+ self.setColormapDialog(colorDialog)
+
+ def getColormapDialog(self):
+ if self._dialog is None:
+ self._dialog = self._createDialog(self.plot)
+ self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
+ return self._dialog
+
@staticmethod
def _createDialog(parent):
"""Create the dialog if not already existing
@@ -349,22 +404,20 @@ class ColormapAction(PlotAction):
:rtype: ColormapDialog
"""
from silx.gui.dialog.ColormapDialog import ColormapDialog
+
dialog = ColormapDialog(parent=parent)
dialog.setModal(False)
return dialog
def _actionTriggered(self, checked=False):
"""Create a cmap dialog and update active image and default cmap."""
- if self._dialog is None:
- self._dialog = self._createDialog(self.plot)
- self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
-
+ dialog = self.getColormapDialog()
# Run the dialog listening to colormap change
if checked is True:
self._updateColormap()
- self._dialog.show()
+ dialog.show()
else:
- self._dialog.hide()
+ dialog.hide()
def _dialogVisibleChanged(self, isVisible):
self.setChecked(isVisible)
@@ -383,7 +436,7 @@ class ColormapAction(PlotAction):
else:
# No active image or active image is RGBA,
# Check for active scatter plot
- scatter = self.plot._getActiveItem(kind='scatter')
+ scatter = self.plot.getActiveScatter()
if scatter is not None:
colormap = scatter.getColormap()
self._dialog.setItem(scatter)
@@ -408,10 +461,14 @@ class ColorBarAction(PlotAction):
def __init__(self, plot, parent=None):
self._dialog = None # To store an instance of ColorBar
super(ColorBarAction, self).__init__(
- plot, icon='colorbar', text='Colorbar',
+ plot,
+ icon="colorbar",
+ text="Colorbar",
tooltip="Show/Hide the colorbar",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
colorBarWidget = self.plot.getColorBarWidget()
old = self.blockSignals(True)
self.setChecked(colorBarWidget.isVisibleTo(self.plot))
@@ -442,23 +499,24 @@ class KeepAspectRatioAction(PlotAction):
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")
+ 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',
+ text="Toggle keep aspect ratio",
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=False,
- parent=parent)
- plot.sigSetKeepDataAspectRatio.connect(
- self._keepDataAspectRatioChanged)
+ parent=parent,
+ )
+ plot.sigSetKeepDataAspectRatio.connect(self._keepDataAspectRatioChanged)
def _keepDataAspectRatioChanged(self, aspectRatio):
"""Handle Plot set keep aspect ratio signal"""
@@ -481,21 +539,20 @@ class YAxisInvertedAction(PlotAction):
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"),
+ False: (icons.getQIcon("plot-ydown"), "Orient Y axis downward"),
+ True: (icons.getQIcon("plot-yup"), "Orient Y axis upward"),
}
icon, tooltip = self._states[plot.getYAxis().isInverted()]
super(YAxisInvertedAction, self).__init__(
plot,
icon=icon,
- text='Invert Y Axis',
+ text="Invert Y Axis",
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=False,
- parent=parent)
+ parent=parent,
+ )
plot.getYAxis().sigInvertedChanged.connect(self._yAxisInvertedChanged)
def _yAxisInvertedChanged(self, inverted):
@@ -520,8 +577,7 @@ class CrosshairAction(PlotAction):
:param parent: See :class:`QAction`
"""
- def __init__(self, plot, color='black', linewidth=1, linestyle='-',
- parent=None):
+ def __init__(self, plot, color="black", linewidth=1, linestyle="-", parent=None):
self.color = color
"""Color used to draw the crosshair (str)."""
@@ -532,18 +588,24 @@ class CrosshairAction(PlotAction):
"""Style of line of the cursor (str)."""
super(CrosshairAction, self).__init__(
- plot, icon='crosshair', text='Crosshair Cursor',
- tooltip='Enable crosshair cursor when checked',
+ plot,
+ icon="crosshair",
+ text="Crosshair Cursor",
+ tooltip="Enable crosshair cursor when checked",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.getGraphCursor() is not None)
plot.sigSetGraphCursor.connect(self.setChecked)
def _actionTriggered(self, checked=False):
- self.plot.setGraphCursor(checked,
- color=self.color,
- linestyle=self.linestyle,
- linewidth=self.linewidth)
+ self.plot.setGraphCursor(
+ checked,
+ color=self.color,
+ linestyle=self.linestyle,
+ linewidth=self.linewidth,
+ )
class PanWithArrowKeysAction(PlotAction):
@@ -554,12 +616,15 @@ class PanWithArrowKeysAction(PlotAction):
"""
def __init__(self, plot, parent=None):
-
super(PanWithArrowKeysAction, self).__init__(
- plot, icon='arrow-keys', text='Pan with arrow keys',
- tooltip='Enable pan with arrow keys when checked',
+ plot,
+ icon="arrow-keys",
+ text="Pan with arrow keys",
+ tooltip="Enable pan with arrow keys when checked",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(plot.isPanWithArrowKeys())
plot.sigSetPanWithArrowKeys.connect(self.setChecked)
@@ -575,15 +640,17 @@ class ShowAxisAction(PlotAction):
"""
def __init__(self, plot, parent=None):
- tooltip = 'Show plot axis when checked, otherwise hide them'
- PlotAction.__init__(self,
- plot,
- icon='axis',
- text='show axis',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=True,
- parent=parent)
+ tooltip = "Show plot axis when checked, otherwise hide them"
+ PlotAction.__init__(
+ self,
+ plot,
+ icon="axis",
+ text="show axis",
+ tooltip=tooltip,
+ triggered=self._actionTriggered,
+ checkable=True,
+ parent=parent,
+ )
self.setChecked(self.plot.isAxesDisplayed())
plot._sigAxesVisibilityChanged.connect(self.setChecked)
@@ -600,15 +667,17 @@ class ClosePolygonInteractionAction(PlotAction):
"""
def __init__(self, plot, parent=None):
- tooltip = 'Close the current polygon drawn'
- PlotAction.__init__(self,
- plot,
- icon='add-shape-polygon',
- text='Close the polygon',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=True,
- parent=parent)
+ tooltip = "Close the current polygon drawn"
+ PlotAction.__init__(
+ self,
+ plot,
+ icon="add-shape-polygon",
+ text="Close the polygon",
+ tooltip=tooltip,
+ triggered=self._actionTriggered,
+ checkable=True,
+ parent=parent,
+ )
self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
self._modeChanged(None)
@@ -618,7 +687,7 @@ class ClosePolygonInteractionAction(PlotAction):
self.setEnabled(enabled)
def _actionTriggered(self, checked=False):
- self.plot._eventHandler.validate()
+ self.plot.interaction()._validate()
class OpenGLAction(PlotAction):
@@ -633,29 +702,32 @@ class OpenGLAction(PlotAction):
def __init__(self, plot, parent=None):
# Uses two images for checked/unchecked states
self._states = {
- "opengl": (icons.getQIcon('backend-opengl'),
- "OpenGL rendering (fast)\nClick to disable OpenGL"),
- "matplotlib": (icons.getQIcon('backend-opengl'),
- "Matplotlib rendering (safe)\nClick to enable OpenGL"),
- "unknown": (icons.getQIcon('backend-opengl'),
- "Custom rendering")
+ "opengl": (
+ icons.getQIcon("backend-opengl"),
+ "OpenGL rendering (fast)\nClick to disable OpenGL",
+ ),
+ "matplotlib": (
+ icons.getQIcon("backend-opengl"),
+ "Matplotlib rendering (safe)\nClick to enable OpenGL",
+ ),
+ "unknown": (icons.getQIcon("backend-opengl"), "Custom rendering"),
}
name = self._getBackendName(plot)
- self.__state = name
icon, tooltip = self._states[name]
super(OpenGLAction, self).__init__(
plot,
icon=icon,
- text='Enable/disable OpenGL rendering',
+ text="Enable/disable OpenGL rendering",
tooltip=tooltip,
triggered=self._actionTriggered,
checkable=True,
- parent=parent)
+ parent=parent,
+ )
+ plot.sigBackendChanged.connect(self._backendUpdated)
def _backendUpdated(self):
name = self._getBackendName(self.plot)
- self.__state = name
icon, tooltip = self._states[name]
self.setIcon(icon)
self.setToolTip(tooltip)
@@ -674,21 +746,15 @@ class OpenGLAction(PlotAction):
def _actionTriggered(self, checked=False):
plot = self.plot
name = self._getBackendName(self.plot)
- if self.__state != name:
- # THere is no event to know the backend was updated
- # So here we check if there is a mismatch between the displayed state
- # and the real state of the widget
- self._backendUpdated()
- return
if name != "opengl":
from silx.gui.utils import glutils
+
result = glutils.isOpenGLAvailable()
if not result:
- qt.QMessageBox.critical(plot, "OpenGL rendering not available", result.error)
- # Uncheck if needed
- self._backendUpdated()
+ qt.QMessageBox.critical(
+ plot, "OpenGL rendering is not available", result.error
+ )
return
plot.setBackend("opengl")
else:
plot.setBackend("matplotlib")
- self._backendUpdated()
diff --git a/src/silx/gui/plot/actions/fit.py b/src/silx/gui/plot/actions/fit.py
index e130b24..ae8835a 100644
--- a/src/silx/gui/plot/actions/fit.py
+++ b/src/silx/gui/plot/actions/fit.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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,8 +31,6 @@ The following QAction are available:
.. autoclass:`.FitAction`
"""
-from __future__ import division
-
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "10/10/2018"
@@ -45,7 +42,6 @@ import numpy
from .PlotToolAction import PlotToolAction
from .. import items
-from ....utils.deprecation import deprecated
from silx.gui import qt
from silx.gui.plot.ItemsSelectionDialog import ItemsSelectionDialog
@@ -66,10 +62,8 @@ def _getUniqueCurveOrHistogram(plot):
return curve
visibleItems = [item for item in plot.getItems() if item.isVisible()]
- histograms = [item for item in visibleItems
- if isinstance(item, items.Histogram)]
- curves = [item for item in visibleItems
- if isinstance(item, items.Curve)]
+ histograms = [item for item in visibleItems if isinstance(item, items.Histogram)]
+ curves = [item for item in visibleItems if isinstance(item, items.Curve)]
if len(histograms) == 1 and len(curves) == 0:
return histograms[0]
@@ -117,12 +111,11 @@ class _FitItemSelector(qt.QObject):
# disconnect from previous plot
previousPlotWidget = self.getPlotWidget()
if previousPlotWidget is not None:
- previousPlotWidget.sigItemAdded.disconnect(
- self.__plotWidgetUpdated)
- previousPlotWidget.sigItemRemoved.disconnect(
- self.__plotWidgetUpdated)
+ previousPlotWidget.sigItemAdded.disconnect(self.__plotWidgetUpdated)
+ previousPlotWidget.sigItemRemoved.disconnect(self.__plotWidgetUpdated)
previousPlotWidget.sigActiveCurveChanged.disconnect(
- self.__plotWidgetUpdated)
+ self.__plotWidgetUpdated
+ )
if plotWidget is None:
self.__plotWidgetRef = None
@@ -187,49 +180,15 @@ class FitAction(PlotToolAction):
self.__legend = None
super(FitAction, self).__init__(
- plot, icon='math-fit', text='Fit curve',
- tooltip='Open a fit dialog',
- parent=parent)
+ plot,
+ icon="math-fit",
+ text="Fit curve",
+ tooltip="Open a fit dialog",
+ parent=parent,
+ )
self.__fitItemSelector = _FitItemSelector()
- self.__fitItemSelector.sigCurrentItemChanged.connect(
- self._setFittedItem)
-
-
- @property
- @deprecated(replacement='getXRange()[0]', since_version='0.13.0')
- def xmin(self):
- return self.getXRange()[0]
-
- @property
- @deprecated(replacement='getXRange()[1]', since_version='0.13.0')
- def xmax(self):
- return self.getXRange()[1]
-
- @property
- @deprecated(replacement='getXData()', since_version='0.13.0')
- def x(self):
- return self.getXData()
-
- @property
- @deprecated(replacement='getYData()', since_version='0.13.0')
- def y(self):
- return self.getYData()
-
- @property
- @deprecated(since_version='0.13.0')
- def xlabel(self):
- return self.__curveParams.get('xlabel', None)
-
- @property
- @deprecated(since_version='0.13.0')
- def ylabel(self):
- return self.__curveParams.get('ylabel', None)
-
- @property
- @deprecated(since_version='0.13.0')
- def legend(self):
- return self.__legend
+ self.__fitItemSelector.sigCurrentItemChanged.connect(self._setFittedItem)
def _createToolWindow(self):
# import done here rather than at module level to avoid circular import
@@ -302,11 +261,10 @@ class FitAction(PlotToolAction):
else:
xmin, xmax = self.getXRange()
- fitWidget.setData(
- xdata, ydata, xmin=xmin, xmax=xmax)
+ fitWidget.setData(xdata, ydata, xmin=xmin, xmax=xmax)
fitWidget.setWindowTitle(
- "Fitting " + item.getName() +
- " on x range %f-%f" % (xmin, xmax))
+ "Fitting " + item.getName() + " on x range %f-%f" % (xmin, xmax)
+ )
# X Range management
@@ -400,12 +358,12 @@ class FitAction(PlotToolAction):
self.__updateFitWidget()
return
- axis = item.getYAxis() if isinstance(item, items.YAxisMixIn) else 'left'
+ axis = item.getYAxis() if isinstance(item, items.YAxisMixIn) else "left"
self.__curveParams = {
- 'yaxis': axis,
- 'xlabel': plot.getXAxis().getLabel(),
- 'ylabel': plot.getYAxis(axis).getLabel(),
- }
+ "yaxis": axis,
+ "xlabel": plot.getXAxis().getLabel(),
+ "ylabel": plot.getYAxis(axis).getLabel(),
+ }
self.__legend = item.getName()
if isinstance(item, items.Histogram):
@@ -418,7 +376,7 @@ class FitAction(PlotToolAction):
self.__x = item.getXData(copy=False)
self.__y = item.getYData(copy=False)
- self.__item = item
+ self.__item = item
self.__updateFitWidget()
def __setFittedItemAutoUpdateEnabled(self, enabled):
@@ -471,14 +429,13 @@ class FitAction(PlotToolAction):
return
y_fit = fit_widget.fitmanager.gendata()
if fit_curve is None:
- self.plot.addCurve(x_fit, y_fit,
- fit_legend,
- resetzoom=False,
- **self.__curveParams)
+ self.plot.addCurve(
+ x_fit, y_fit, fit_legend, resetzoom=False, **self.__curveParams
+ )
else:
fit_curve.setData(x_fit, y_fit)
fit_curve.setVisible(True)
- fit_curve.setYAxis(self.__curveParams.get('yaxis', 'left'))
+ fit_curve.setYAxis(self.__curveParams.get("yaxis", "left"))
if ddict["event"] in ["FitStarted", "FitFailed"]:
if fit_curve is not None:
diff --git a/src/silx/gui/plot/actions/histogram.py b/src/silx/gui/plot/actions/histogram.py
index be9f5a7..39c669b 100644
--- a/src/silx/gui/plot/actions/histogram.py
+++ b/src/silx/gui/plot/actions/histogram.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,10 +30,8 @@ The following QAction are available:
- :class:`PixelIntensitiesHistoAction`
"""
-from __future__ import division
-
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
-__date__ = "01/12/2020"
+__date__ = "07/11/2023"
__license__ = "MIT"
from typing import Optional, Tuple
@@ -50,7 +47,6 @@ from silx.gui import qt
from silx.gui.plot import items
from silx.gui.widgets.ElidedLabel import ElidedLabel
from silx.gui.widgets.RangeSlider import RangeSlider
-from silx.utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
@@ -64,8 +60,8 @@ class _ElidedLabel(ElidedLabel):
def sizeHint(self):
hint = super().sizeHint()
- nbchar = max(len(self.getText()), 12)
- width = self.fontMetrics().boundingRect('#' * nbchar).width()
+ nbchar = max(len(self.text()), 12)
+ width = self.fontMetrics().boundingRect("#" * nbchar).width()
return qt.QSize(max(hint.width(), width), hint.height())
@@ -76,7 +72,7 @@ class _StatWidget(qt.QWidget):
:param name:
"""
- def __init__(self, parent=None, name: str=''):
+ def __init__(self, parent=None, name: str = ""):
super().__init__(parent)
layout = qt.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
@@ -87,7 +83,8 @@ class _StatWidget(qt.QWidget):
self.__valueWidget = _ElidedLabel(parent=self)
self.__valueWidget.setText("-")
self.__valueWidget.setTextInteractionFlags(
- qt.Qt.TextSelectableByMouse | qt.Qt.TextSelectableByKeyboard)
+ qt.Qt.TextSelectableByMouse | qt.Qt.TextSelectableByKeyboard
+ )
layout.addWidget(self.__valueWidget)
def setValue(self, value: Optional[float]):
@@ -95,8 +92,7 @@ class _StatWidget(qt.QWidget):
:param value:
"""
- self.__valueWidget.setText(
- "-" if value is None else "{:.5g}".format(value))
+ self.__valueWidget.setText("-" if value is None else "{:.5g}".format(value))
class _IntEdit(qt.QLineEdit):
@@ -127,9 +123,7 @@ class _IntEdit(qt.QLineEdit):
font = self.font()
font.setStyle(qt.QFont.StyleItalic)
fontMetrics = qt.QFontMetrics(font)
- self.setMaximumWidth(
- fontMetrics.boundingRect('0' * (nbchar + 1)).width()
- )
+ self.setMaximumWidth(fontMetrics.boundingRect("0" * (nbchar + 1)).width())
self.setMaxLength(nbchar)
def __textEdited(self, _):
@@ -194,7 +188,7 @@ class _IntEdit(qt.QLineEdit):
self.setRange(min(value, bottom), max(value, top))
return numpy.clip(value, *self.getRange())
- def setDefaultValue(self, value: int, extend_range: bool=False):
+ def setDefaultValue(self, value: int, extend_range: bool = False):
"""Set default value when QLineEdit is empty
:param int value:
@@ -213,7 +207,7 @@ class _IntEdit(qt.QLineEdit):
except ValueError:
return None
- def setCurrentValue(self, value: int, extend_range: bool=False):
+ def setCurrentValue(self, value: int, extend_range: bool = False):
"""Set the currently displayed value
:param int value:
@@ -239,7 +233,7 @@ class HistogramWidget(qt.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.setWindowTitle('Histogram')
+ self.setWindowTitle("Histogram")
self.__itemRef = None # weakref on the item to track
@@ -250,6 +244,7 @@ class HistogramWidget(qt.QWidget):
# Plot
# Lazy import to avoid circular dependencies
from silx.gui.plot.PlotWindow import Plot1D
+
self.__plot = Plot1D(self)
layout.addWidget(self.__plot)
@@ -269,16 +264,18 @@ class HistogramWidget(qt.QWidget):
controlsLayout.addWidget(qt.QLabel("N. bins:"))
self.__nbinsLineEdit = _IntEdit(self)
self.__nbinsLineEdit.setRange(2, 9999)
- self.__nbinsLineEdit.sigValueChanged.connect(
- self.__updateHistogramFromControls)
+ self.__nbinsLineEdit.sigValueChanged.connect(self.__updateHistogramFromControls)
controlsLayout.addWidget(self.__nbinsLineEdit)
self.__rangeLabel = qt.QLabel("Range:")
controlsLayout.addWidget(self.__rangeLabel)
self.__rangeSlider = RangeSlider(parent=self)
- self.__rangeSlider.sigValueChanged.connect(
- self.__updateHistogramFromControls)
+ self.__rangeSlider.sigValueChanged.connect(self.__updateHistogramFromControls)
self.__rangeSlider.sigValueChanged.connect(self.__rangeChanged)
controlsLayout.addWidget(self.__rangeSlider)
+ self.__weightCheckBox = qt.QCheckBox(self)
+ self.__weightCheckBox.setText("Use weights")
+ self.__weightCheckBox.clicked.connect(self.__weightChanged)
+ controlsLayout.addWidget(self.__weightCheckBox)
controlsLayout.addStretch(1)
# Stats display
@@ -289,7 +286,8 @@ class HistogramWidget(qt.QWidget):
self.__statsWidgets = dict(
(name, _StatWidget(parent=statsWidget, name=name))
- for name in ("min", "max", "mean", "std", "sum"))
+ for name in ("min", "max", "mean", "std", "sum")
+ )
for widget in self.__statsWidgets.values():
statsLayout.addWidget(widget)
@@ -339,8 +337,10 @@ class HistogramWidget(qt.QWidget):
hist = self.getHistogram(copy=False)
if hist is not None:
count, edges = hist
- if (len(count) == self.__nbinsLineEdit.getValue() and
- (edges[0], edges[-1]) == self.__rangeSlider.getValues()):
+ if (
+ len(count) == self.__nbinsLineEdit.getValue()
+ and (edges[0], edges[-1]) == self.__rangeSlider.getValues()
+ ):
return # Nothing has changed
self._updateFromItem()
@@ -351,6 +351,9 @@ class HistogramWidget(qt.QWidget):
self.__rangeSlider.setToolTip(tooltip)
self.__rangeLabel.setToolTip(tooltip)
+ def __weightChanged(self, value):
+ self._updateFromItem()
+
def _updateFromItem(self):
"""Update histogram and stats from the item"""
item = self.getItem()
@@ -391,31 +394,39 @@ class HistogramWidget(qt.QWidget):
if xmin == 0:
range_ = -0.01, 0.01
else:
- range_ = sorted((xmin * .99, xmin * 1.01))
+ range_ = sorted((xmin * 0.99, xmin * 1.01))
else:
range_ = xmin, xmax
self.__rangeSlider.setRange(*range_)
self.__rangeSlider.setPositions(*previousPositions)
+ data = array.ravel().astype(numpy.float32)
histogram = Histogramnd(
- array.ravel().astype(numpy.float32),
+ data,
n_bins=max(2, self.__nbinsLineEdit.getValue()),
histo_range=self.__rangeSlider.getValues(),
+ weights=data,
)
if len(histogram.edges) != 1:
_logger.error("Error while computing the histogram")
self.reset()
return
- self.setHistogram(histogram.histo, histogram.edges[0])
+ if self.__weightCheckBox.isChecked():
+ self.setHistogram(histogram.weighted_histo, histogram.edges[0])
+ self.__plot.getYAxis().setLabel("Count * Value")
+ else:
+ self.setHistogram(histogram.histo, histogram.edges[0])
+ self.__plot.getYAxis().setLabel("Count")
self.resetZoom()
self.setStatistics(
min_=xmin,
max_=xmax,
mean=numpy.nanmean(array),
std=numpy.nanstd(array),
- sum_=numpy.nansum(array))
+ sum_=numpy.nansum(array),
+ )
def setHistogram(self, histogram, edges):
"""Set displayed histogram
@@ -425,20 +436,21 @@ class HistogramWidget(qt.QWidget):
"""
# Only useful if setHistogram is called directly
# TODO
- #nbins = len(histogram)
- #if nbins != self.__nbinsLineEdit.getDefaultValue():
+ # nbins = len(histogram)
+ # if nbins != self.__nbinsLineEdit.getDefaultValue():
# self.__nbinsLineEdit.setValue(nbins, extend_range=True)
- #self.__rangeSlider.setValues(edges[0], edges[-1])
+ # self.__rangeSlider.setValues(edges[0], edges[-1])
self.getPlotWidget().addHistogram(
histogram=histogram,
edges=edges,
- legend='histogram',
+ legend="histogram",
fill=True,
- color='#66aad7',
- resetzoom=False)
+ color="#66aad7",
+ resetzoom=False,
+ )
- def getHistogram(self, copy: bool=True):
+ def getHistogram(self, copy: bool = True):
"""Returns currently displayed histogram.
:param copy: True to get a copy,
@@ -446,24 +458,25 @@ class HistogramWidget(qt.QWidget):
:return: (histogram, edges) or None
"""
for item in self.getPlotWidget().getItems():
- if item.getName() == 'histogram':
- return (item.getValueData(copy=copy),
- item.getBinEdgesData(copy=copy))
+ if item.getName() == "histogram":
+ return (item.getValueData(copy=copy), item.getBinEdgesData(copy=copy))
else:
return None
- def setStatistics(self,
- min_: Optional[float] = None,
- max_: Optional[float] = None,
- mean: Optional[float] = None,
- std: Optional[float] = None,
- sum_: Optional[float] = None):
+ def setStatistics(
+ self,
+ min_: Optional[float] = None,
+ max_: Optional[float] = None,
+ mean: Optional[float] = None,
+ std: Optional[float] = None,
+ sum_: Optional[float] = None,
+ ):
"""Set displayed statistic indicators."""
- self.__statsWidgets['min'].setValue(min_)
- self.__statsWidgets['max'].setValue(max_)
- self.__statsWidgets['mean'].setValue(mean)
- self.__statsWidgets['std'].setValue(std)
- self.__statsWidgets['sum'].setValue(sum_)
+ self.__statsWidgets["min"].setValue(min_)
+ self.__statsWidgets["max"].setValue(max_)
+ self.__statsWidgets["mean"].setValue(mean)
+ self.__statsWidgets["std"].setValue(std)
+ self.__statsWidgets["sum"].setValue(sum_)
class PixelIntensitiesHistoAction(PlotToolAction):
@@ -474,12 +487,14 @@ class PixelIntensitiesHistoAction(PlotToolAction):
"""
def __init__(self, plot, parent=None):
- PlotToolAction.__init__(self,
- plot,
- icon='pixel-intensities',
- text='pixels intensity',
- tooltip='Compute image intensity distribution',
- parent=parent)
+ PlotToolAction.__init__(
+ self,
+ plot,
+ icon="pixel-intensities",
+ text="pixels intensity",
+ tooltip="Compute image intensity distribution",
+ parent=parent,
+ )
def _connectPlot(self, window):
plot = self.plot
@@ -517,19 +532,10 @@ class PixelIntensitiesHistoAction(PlotToolAction):
if self._isWindowInUse():
self._updateSelectedItem()
- @deprecated(since_version='0.15.0')
- def computeIntensityDistribution(self):
- self.getHistogramWidget()._updateFromItem()
-
def getHistogramWidget(self):
"""Returns the widget displaying the histogram"""
return self._getToolWindow()
- @deprecated(since_version='0.15.0',
- replacement='getHistogramWidget().getPlotWidget()')
- def getHistogramPlotWidget(self):
- return self._getToolWindow().getPlotWidget()
-
def _createToolWindow(self):
return HistogramWidget(self.plot, qt.Qt.Window)
diff --git a/src/silx/gui/plot/actions/io.py b/src/silx/gui/plot/actions/io.py
index 7f4edd3..1ff95f3 100644
--- a/src/silx/gui/plot/actions/io.py
+++ b/src/silx/gui/plot/actions/io.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,36 +32,31 @@ The following QAction are available:
- :class:`SaveAction`
"""
-from __future__ import division
-
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "25/09/2020"
-from . import PlotAction
-from silx.io.utils import save1D, savespec, NEXUS_HDF5_EXT
-from silx.io.nxdata import save_NXdata
+from io import BytesIO
import logging
import sys
import os.path
-from collections import OrderedDict
import traceback
import numpy
-from silx.utils.deprecation import deprecated
+from fabio.TiffIO import TiffIO
+from fabio.edfimage import EdfImage
+
from silx.gui import qt, printer
from silx.gui.dialog.GroupDialog import GroupDialog
-from silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
+from silx.io.utils import save1D, savespec, NEXUS_HDF5_EXT
+from silx.io.nxdata import save_NXdata
+
+from . import PlotAction
from ...utils.image import convertArrayToQImage
-if sys.version_info[0] == 3:
- from io import BytesIO
-else:
- import cStringIO as _StringIO
- BytesIO = _StringIO.StringIO
+
_logger = logging.getLogger(__name__)
-_NEXUS_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT])
+_NEXUS_HDF5_EXT_STR = " ".join(["*" + ext for ext in NEXUS_HDF5_EXT])
def selectOutputGroup(h5filename):
@@ -90,111 +84,142 @@ class SaveAction(PlotAction):
:param parent: See :class:`QAction`.
"""
- SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)'
- SNAPSHOT_FILTER_PNG = 'Plot Snapshot as PNG (*.png)'
+ SNAPSHOT_FILTER_SVG = "Plot Snapshot as SVG (*.svg)"
+ SNAPSHOT_FILTER_PNG = "Plot Snapshot as PNG (*.png)"
DEFAULT_ALL_FILTERS = (SNAPSHOT_FILTER_PNG, 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': '%.10g', 'delimiter': '', 'header': False})
- ))
-
- CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)'
-
- CURVE_FILTER_NXDATA = 'Curve as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
+ CURVE_FILTERS_TXT = dict(
+ (
+ (
+ "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": "%.10g", "delimiter": "", "header": False},
+ ),
+ )
+ )
+
+ CURVE_FILTER_NPY = "Curve as NumPy binary file (*.npy)"
+
+ CURVE_FILTER_NXDATA = "Curve as NXdata (%s)" % _NEXUS_HDF5_EXT_STR
DEFAULT_CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [
- CURVE_FILTER_NPY, CURVE_FILTER_NXDATA]
+ CURVE_FILTER_NPY,
+ CURVE_FILTER_NXDATA,
+ ]
DEFAULT_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_NXDATA = 'Image as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
-
- DEFAULT_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_NXDATA)
-
- SCATTER_FILTER_NXDATA = 'Scatter as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
+ 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_NXDATA = "Image as NXdata (%s)" % _NEXUS_HDF5_EXT_STR
+
+ DEFAULT_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_NXDATA,
+ )
+
+ SCATTER_FILTER_NXDATA = "Scatter as NXdata (%s)" % _NEXUS_HDF5_EXT_STR
DEFAULT_SCATTER_FILTERS = (SCATTER_FILTER_NXDATA,)
# filters for which we don't want an "overwrite existing file" warning
- DEFAULT_APPEND_FILTERS = (CURVE_FILTER_NXDATA, IMAGE_FILTER_NXDATA,
- SCATTER_FILTER_NXDATA)
+ DEFAULT_APPEND_FILTERS = (
+ CURVE_FILTER_NXDATA,
+ IMAGE_FILTER_NXDATA,
+ SCATTER_FILTER_NXDATA,
+ )
def __init__(self, plot, parent=None):
self._filters = {
- 'all': OrderedDict(),
- 'curve': OrderedDict(),
- 'curves': OrderedDict(),
- 'image': OrderedDict(),
- 'scatter': OrderedDict()}
+ "all": {},
+ "curve": {},
+ "curves": {},
+ "image": {},
+ "scatter": {},
+ }
self._appendFilters = list(self.DEFAULT_APPEND_FILTERS)
# Initialize filters
for nameFilter in self.DEFAULT_ALL_FILTERS:
self.setFileFilter(
- dataKind='all', nameFilter=nameFilter, func=self._saveSnapshot)
+ dataKind="all", nameFilter=nameFilter, func=self._saveSnapshot
+ )
for nameFilter in self.DEFAULT_CURVE_FILTERS:
self.setFileFilter(
- dataKind='curve', nameFilter=nameFilter, func=self._saveCurve)
+ dataKind="curve", nameFilter=nameFilter, func=self._saveCurve
+ )
for nameFilter in self.DEFAULT_ALL_CURVES_FILTERS:
self.setFileFilter(
- dataKind='curves', nameFilter=nameFilter, func=self._saveCurves)
+ dataKind="curves", nameFilter=nameFilter, func=self._saveCurves
+ )
for nameFilter in self.DEFAULT_IMAGE_FILTERS:
self.setFileFilter(
- dataKind='image', nameFilter=nameFilter, func=self._saveImage)
+ dataKind="image", nameFilter=nameFilter, func=self._saveImage
+ )
for nameFilter in self.DEFAULT_SCATTER_FILTERS:
self.setFileFilter(
- dataKind='scatter', nameFilter=nameFilter, func=self._saveScatter)
+ dataKind="scatter", nameFilter=nameFilter, func=self._saveScatter
+ )
super(SaveAction, self).__init__(
- plot, icon='document-save', text='Save as...',
- tooltip='Save curve/image/plot snapshot dialog',
+ plot,
+ icon="document-save",
+ text="Save as...",
+ tooltip="Save curve/image/plot snapshot dialog",
triggered=self._actionTriggered,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.Save)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@staticmethod
- def _errorMessage(informativeText='', parent=None):
+ def _errorMessage(informativeText="", parent=None):
"""Display an error message."""
# TODO issue with QMessageBox size fixed and too small
msg = qt.QMessageBox(parent)
msg.setIcon(qt.QMessageBox.Critical)
- msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
+ msg.setInformativeText(informativeText + " " + str(sys.exc_info()[1]))
msg.setDetailedText(traceback.format_exc())
msg.exec()
@@ -207,12 +232,11 @@ class SaveAction(PlotAction):
True otherwise.
"""
if nameFilter == self.SNAPSHOT_FILTER_PNG:
- fileFormat = 'png'
+ fileFormat = "png"
elif nameFilter == self.SNAPSHOT_FILTER_SVG:
- fileFormat = 'svg'
+ fileFormat = "svg"
else: # Format not supported
- _logger.error(
- 'Saving plot snapshot failed: format not supported')
+ _logger.error("Saving plot snapshot failed: format not supported")
return False
plot.saveGraph(filename, fileFormat=fileFormat)
@@ -263,8 +287,11 @@ class SaveAction(PlotAction):
@staticmethod
def _selectWriteableOutputGroup(filename, parent):
- if os.path.exists(filename) and os.path.isfile(filename) \
- and os.access(filename, os.W_OK):
+ if (
+ os.path.exists(filename)
+ and os.path.isfile(filename)
+ and os.access(filename, os.W_OK)
+ ):
entryPath = selectOutputGroup(filename)
if entryPath is None:
_logger.info("Save operation cancelled")
@@ -274,7 +301,7 @@ class SaveAction(PlotAction):
# create new entry in new file
return "/entry"
else:
- SaveAction._errorMessage('Save failed (file access issue)\n', parent=parent)
+ SaveAction._errorMessage("Save failed (file access issue)\n", parent=parent)
return None
def _saveCurveAsNXdata(self, curve, filename):
@@ -295,7 +322,8 @@ class SaveAction(PlotAction):
axes_long_names=[xlabel],
signal_errors=curve.getYErrorData(copy=False),
axes_errors=[curve.getXErrorData(copy=True)],
- title=self.plot.getGraphTitle())
+ title=self.plot.getGraphTitle(),
+ )
def _saveCurve(self, plot, filename, nameFilter):
"""Save a curve from the plot.
@@ -321,9 +349,9 @@ class SaveAction(PlotAction):
if nameFilter in self.CURVE_FILTERS_TXT:
filter_ = self.CURVE_FILTERS_TXT[nameFilter]
- fmt = filter_['fmt']
- csvdelim = filter_['delimiter']
- autoheader = filter_['header']
+ fmt = filter_["fmt"]
+ csvdelim = filter_["delimiter"]
+ autoheader = filter_["header"]
else:
# .npy or nxdata
fmt, csvdelim, autoheader = ("", "", False)
@@ -334,13 +362,18 @@ class SaveAction(PlotAction):
xdata, data, xlabel, labels = self._get1dData(curve)
try:
- save1D(filename,
- xdata, data,
- xlabel, labels,
- fmt=fmt, csvdelim=csvdelim,
- autoheader=autoheader)
+ save1D(
+ filename,
+ xdata,
+ data,
+ xlabel,
+ labels,
+ fmt=fmt,
+ csvdelim=csvdelim,
+ autoheader=autoheader,
+ )
except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
return True
@@ -366,28 +399,39 @@ class SaveAction(PlotAction):
try:
xdata, data, xlabel, labels = self._get1dData(curve)
- specfile = savespec(filename,
- xdata, data,
- xlabel, labels,
- fmt="%.7g", scan_number=1, mode="w",
- write_file_header=True,
- close_file=False)
+ specfile = savespec(
+ filename,
+ xdata,
+ data,
+ xlabel,
+ labels,
+ fmt="%.7g",
+ scan_number=1,
+ mode="w",
+ write_file_header=True,
+ close_file=False,
+ )
except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
for curve in curves[1:]:
try:
scanno += 1
xdata, data, xlabel, labels = self._get1dData(curve)
- specfile = savespec(specfile,
- xdata, data,
- xlabel, labels,
- fmt="%.7g", scan_number=scanno,
- write_file_header=False,
- close_file=False)
+ specfile = savespec(
+ specfile,
+ xdata,
+ data,
+ xlabel,
+ labels,
+ fmt="%.7g",
+ scan_number=scanno,
+ write_file_header=False,
+ close_file=False,
+ )
except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
specfile.close()
@@ -406,28 +450,26 @@ class SaveAction(PlotAction):
image = plot.getActiveImage()
if image is None:
- qt.QMessageBox.warning(
- plot, "No Data", "No image to be saved")
+ qt.QMessageBox.warning(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)
+ EdfImage(data=data, header={}).write(filename)
return True
elif nameFilter == self.IMAGE_FILTER_TIFF:
- tiffFile = TiffIO(filename, mode='w')
- tiffFile.writeImage(data, software='silx')
+ 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', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
return True
@@ -442,39 +484,47 @@ class SaveAction(PlotAction):
xlabel, ylabel = self._getAxesLabels(image)
interpretation = "image" if len(data.shape) == 2 else "rgba-image"
- return save_NXdata(filename,
- nxentry_name=entryPath,
- signal=data,
- axes=[yaxis, xaxis],
- signal_name="image",
- axes_names=["y", "x"],
- axes_long_names=[ylabel, xlabel],
- title=plot.getGraphTitle(),
- interpretation=interpretation)
-
- elif nameFilter in (self.IMAGE_FILTER_ASCII,
- self.IMAGE_FILTER_CSV_COMMA,
- self.IMAGE_FILTER_CSV_SEMICOLON,
- self.IMAGE_FILTER_CSV_TAB):
+ return save_NXdata(
+ filename,
+ nxentry_name=entryPath,
+ signal=data,
+ axes=[yaxis, xaxis],
+ signal_name="image",
+ axes_names=["y", "x"],
+ axes_long_names=[ylabel, xlabel],
+ title=plot.getGraphTitle(),
+ interpretation=interpretation,
+ )
+
+ 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]
+ 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)
+ 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', parent=self.plot)
+ self._errorMessage("Save failed\n", parent=self.plot)
return False
return True
@@ -484,14 +534,13 @@ class SaveAction(PlotAction):
# Convert RGB QImage
qimage = convertArrayToQImage(rgbaImage[:, :, :3])
- if qimage.save(filename, 'PNG'):
+ if qimage.save(filename, "PNG"):
return True
else:
- _logger.error('Failed to save image as %s', filename)
+ _logger.error("Failed to save image as %s", filename)
qt.QMessageBox.critical(
- self.parent(),
- 'Save image as',
- 'Failed to save image')
+ self.parent(), "Save image as", "Failed to save image"
+ )
return False
@@ -536,7 +585,8 @@ class SaveAction(PlotAction):
axes_names=["x", "y"],
axes_long_names=[xlabel, ylabel],
axes_errors=[xerror, yerror],
- title=plot.getGraphTitle())
+ title=plot.getGraphTitle(),
+ )
def setFileFilter(self, dataKind, nameFilter, func, index=None, appendToFile=False):
"""Set a name filter to add/replace a file format support
@@ -553,7 +603,7 @@ class SaveAction(PlotAction):
file.
:param integer index: Index of the filter in the final list (or None)
"""
- assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')
+ assert dataKind in ("all", "curve", "curves", "image", "scatter")
if appendToFile:
self._appendFilters.append(nameFilter)
@@ -575,7 +625,7 @@ class SaveAction(PlotAction):
if index >= len(keyList):
# nothing to be done, already at the end
- txt = 'Requested index %d impossible, already at the end' % index
+ txt = "Requested index %d impossible, already at the end" % index
_logger.info(txt)
return
@@ -585,7 +635,7 @@ class SaveAction(PlotAction):
keyList.insert(index, nameFilter)
# build the new filters
- newFilters = OrderedDict()
+ newFilters = {}
for key in keyList:
newFilters[key] = self._filters[dataKind][key]
@@ -600,52 +650,51 @@ class SaveAction(PlotAction):
The kind of data for which the provided filter is valid.
On of: 'all', 'curve', 'curves', 'image', 'scatter'
:return: {nameFilter: function} associations.
- :rtype: collections.OrderedDict
+ :rtype: dict
"""
- assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')
+ assert dataKind in ("all", "curve", "curves", "image", "scatter")
return self._filters[dataKind].copy()
def _actionTriggered(self, checked=False):
"""Handle save action."""
# Set-up filters
- filters = OrderedDict()
+ filters = {}
# Add image filters if there is an active image
if self.plot.getActiveImage() is not None:
- filters.update(self._filters['image'].items())
+ filters.update(self._filters["image"].items())
# Add curve filters if there is a curve to save
- if (self.plot.getActiveCurve() is not None or
- len(self.plot.getAllCurves()) == 1):
- filters.update(self._filters['curve'].items())
+ if self.plot.getActiveCurve() is not None or len(self.plot.getAllCurves()) == 1:
+ filters.update(self._filters["curve"].items())
if len(self.plot.getAllCurves()) >= 1:
- filters.update(self._filters['curves'].items())
+ filters.update(self._filters["curves"].items())
# Add scatter filters if there is a scatter
# todo: CSV
if self.plot.getScatter() is not None:
- filters.update(self._filters['scatter'].items())
+ filters.update(self._filters["scatter"].items())
- filters.update(self._filters['all'].items())
+ filters.update(self._filters["all"].items())
# Create and run File dialog
dialog = qt.QFileDialog(self.plot)
- dialog.setOption(dialog.DontUseNativeDialog)
+ dialog.setOption(qt.QFileDialog.DontUseNativeDialog)
dialog.setWindowTitle("Output File Selection")
dialog.setModal(1)
dialog.setNameFilters(list(filters.keys()))
- dialog.setFileMode(dialog.AnyFile)
- dialog.setAcceptMode(dialog.AcceptSave)
+ dialog.setFileMode(qt.QFileDialog.AnyFile)
+ dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
def onFilterSelection(filt_):
# disable overwrite confirmation for NXdata types,
# because we append the data to existing files
if filt_ in self._appendFilters:
- dialog.setOption(dialog.DontConfirmOverwrite)
+ dialog.setOption(qt.QFileDialog.DontConfirmOverwrite)
else:
- dialog.setOption(dialog.DontConfirmOverwrite, False)
+ dialog.setOption(qt.QFileDialog.DontConfirmOverwrite, False)
dialog.filterSelected.connect(onFilterSelection)
@@ -656,14 +705,18 @@ class SaveAction(PlotAction):
filename = dialog.selectedFiles()[0]
dialog.close()
- if '(' in nameFilter and ')' == nameFilter.strip()[-1]:
+ if "(" in nameFilter and ")" == nameFilter.strip()[-1]:
# Check for correct file extension
# Extract file extensions as .something
- extensions = [ext[ext.find('.'):] for ext in
- nameFilter[nameFilter.find('(') + 1:-1].split()]
+ extensions = [
+ ext[ext.find(".") :]
+ for ext in nameFilter[nameFilter.find("(") + 1 : -1].split()
+ ]
for ext in extensions:
- if (len(filename) > len(ext) and
- filename[-len(ext):].lower() == ext.lower()):
+ if (
+ len(filename) > len(ext)
+ and filename[-len(ext) :].lower() == ext.lower()
+ ):
break
else: # filename has no extension supported in nameFilter, add one
if len(extensions) >= 1:
@@ -674,7 +727,7 @@ class SaveAction(PlotAction):
if func is not None:
return func(self.plot, filename, nameFilter)
else:
- _logger.error('Unsupported file filter: %s', nameFilter)
+ _logger.error("Unsupported file filter: %s", nameFilter)
return False
@@ -684,7 +737,7 @@ def _plotAsPNG(plot):
:param plot: The :class:`Plot` to save
"""
pngFile = BytesIO()
- plot.saveGraph(pngFile, fileFormat='png')
+ plot.saveGraph(pngFile, fileFormat="png")
pngFile.flush()
pngFile.seek(0)
data = pngFile.read()
@@ -706,10 +759,14 @@ class PrintAction(PlotAction):
def __init__(self, plot, parent=None):
super(PrintAction, self).__init__(
- plot, icon='document-print', text='Print...',
- tooltip='Open print dialog',
+ plot,
+ icon="document-print",
+ text="Print...",
+ tooltip="Open print dialog",
triggered=self.printPlot,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.Print)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -720,11 +777,6 @@ class PrintAction(PlotAction):
"""
return printer.getDefaultPrinter()
- @property
- @deprecated(replacement="getPrinter()", since_version="0.8.0")
- def printer(self):
- return self.getPrinter()
-
def printPlotAsWidget(self):
"""Open the print dialog and print the plot.
@@ -733,7 +785,7 @@ class PrintAction(PlotAction):
:return: True if successful
"""
dialog = qt.QPrintDialog(self.getPrinter(), self.plot)
- dialog.setWindowTitle('Print Plot')
+ dialog.setWindowTitle("Print Plot")
if not dialog.exec():
return False
@@ -749,9 +801,9 @@ class PrintAction(PlotAction):
yScale = pageRect.height() / widget.height()
scale = min(xScale, yScale)
- painter.translate(pageRect.width() / 2., 0.)
+ painter.translate(pageRect.width() / 2.0, 0.0)
painter.scale(scale, scale)
- painter.translate(-widget.width() / 2., 0.)
+ painter.translate(-widget.width() / 2.0, 0.0)
widget.render(painter)
painter.end()
@@ -766,7 +818,7 @@ class PrintAction(PlotAction):
"""
# Init printer and start printer dialog
dialog = qt.QPrintDialog(self.getPrinter(), self.plot)
- dialog.setWindowTitle('Print Plot')
+ dialog.setWindowTitle("Print Plot")
if not dialog.exec():
return False
@@ -774,7 +826,7 @@ class PrintAction(PlotAction):
pngData = _plotAsPNG(self.plot)
pixmap = qt.QPixmap()
- pixmap.loadFromData(pngData, 'png')
+ pixmap.loadFromData(pngData, "png")
pageRect = self.getPrinter().pageRect(qt.QPrinter.DevicePixel)
xScale = pageRect.width() / pixmap.width()
@@ -786,10 +838,9 @@ class PrintAction(PlotAction):
if not painter.begin(self.getPrinter()):
return False
- painter.drawPixmap(0, 0,
- pixmap.width() * scale,
- pixmap.height() * scale,
- pixmap)
+ painter.drawPixmap(
+ 0, 0, pixmap.width() * scale, pixmap.height() * scale, pixmap
+ )
painter.end()
return True
@@ -804,10 +855,14 @@ class CopyAction(PlotAction):
def __init__(self, plot, parent=None):
super(CopyAction, self).__init__(
- plot, icon='edit-copy', text='Copy plot',
- tooltip='Copy a snapshot of the plot into the clipboard',
+ plot,
+ icon="edit-copy",
+ text="Copy plot",
+ tooltip="Copy a snapshot of the plot into the clipboard",
triggered=self.copyPlot,
- checkable=False, parent=parent)
+ checkable=False,
+ parent=parent,
+ )
self.setShortcut(qt.QKeySequence.Copy)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -815,5 +870,5 @@ class CopyAction(PlotAction):
"""Copy plot content to the clipboard as a bitmap."""
# Save Plot as PNG and make a QImage from it with default dpi
pngData = _plotAsPNG(self.plot)
- image = qt.QImage.fromData(pngData, 'png')
+ image = qt.QImage.fromData(pngData, "png")
qt.QApplication.clipboard().setImage(image)
diff --git a/src/silx/gui/plot/actions/medfilt.py b/src/silx/gui/plot/actions/medfilt.py
index f86a377..a335499 100644
--- a/src/silx/gui/plot/actions/medfilt.py
+++ b/src/silx/gui/plot/actions/medfilt.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
@@ -34,8 +33,6 @@ The following QAction are available:
"""
-from __future__ import division
-
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
__license__ = "MIT"
@@ -57,12 +54,14 @@ class MedianFilterAction(PlotToolAction):
"""
def __init__(self, plot, parent=None):
- PlotToolAction.__init__(self,
- plot,
- icon='median-filter',
- text='median filter',
- tooltip='Apply a median filter on the image',
- parent=parent)
+ PlotToolAction.__init__(
+ self,
+ plot,
+ icon="median-filter",
+ text="median filter",
+ tooltip="Apply a median filter on the image",
+ parent=parent,
+ )
self._originalImage = None
self._legend = None
self._filteredImage = None
@@ -88,7 +87,9 @@ class MedianFilterAction(PlotToolAction):
self._originalImage = None
self._legend = None
else:
- self._originalImage = self.plot.getImage(self._activeImageLegend).getData(copy=False)
+ self._originalImage = self.plot.getImage(self._activeImageLegend).getData(
+ copy=False
+ )
self._legend = self.plot.getImage(self._activeImageLegend).getName()
def _updateFilter(self, kernelWidth, conditional=False):
@@ -97,13 +98,11 @@ class MedianFilterAction(PlotToolAction):
self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage)
filteredImage = self._computeFilteredImage(kernelWidth, conditional)
- self.plot.addImage(data=filteredImage,
- legend=self._legend,
- replace=True)
+ self.plot.addImage(data=filteredImage, legend=self._legend, replace=True)
self.plot.sigActiveImageChanged.connect(self._updateActiveImage)
def _computeFilteredImage(self, kernelWidth, conditional):
- raise NotImplementedError('MedianFilterAction is a an abstract class')
+ raise NotImplementedError("MedianFilterAction is a an abstract class")
def getFilteredImage(self):
"""
@@ -117,16 +116,13 @@ class MedianFilter1DAction(MedianFilterAction):
:param plot: :class:`.PlotWidget` instance on which to operate
:param parent: See :class:`QAction`
"""
+
def __init__(self, plot, parent=None):
- MedianFilterAction.__init__(self,
- plot,
- parent=parent)
+ MedianFilterAction.__init__(self, plot, parent=parent)
def _computeFilteredImage(self, kernelWidth, conditional):
- assert(self.plot is not None)
- return medfilt2d(self._originalImage,
- (kernelWidth, 1),
- conditional)
+ assert self.plot is not None
+ return medfilt2d(self._originalImage, (kernelWidth, 1), conditional)
class MedianFilter2DAction(MedianFilterAction):
@@ -135,13 +131,10 @@ class MedianFilter2DAction(MedianFilterAction):
:param plot: :class:`.PlotWidget` instance on which to operate
:param parent: See :class:`QAction`
"""
+
def __init__(self, plot, parent=None):
- MedianFilterAction.__init__(self,
- plot,
- parent=parent)
+ MedianFilterAction.__init__(self, plot, parent=parent)
def _computeFilteredImage(self, kernelWidth, conditional):
- assert(self.plot is not None)
- return medfilt2d(self._originalImage,
- (kernelWidth, kernelWidth),
- conditional)
+ assert self.plot is not None
+ return medfilt2d(self._originalImage, (kernelWidth, kernelWidth), conditional)
diff --git a/src/silx/gui/plot/actions/mode.py b/src/silx/gui/plot/actions/mode.py
index ee05256..511a8df 100644
--- a/src/silx/gui/plot/actions/mode.py
+++ b/src/silx/gui/plot/actions/mode.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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,16 +31,15 @@ The following QAction are available:
- :class:`PanModeAction`
"""
-from __future__ import division
-
__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "16/08/2017"
-from . import PlotAction
-import logging
-_logger = logging.getLogger(__name__)
+from silx.gui import qt
+
+from ..tools.menus import ZoomEnabledAxesMenu
+from . import PlotAction
class ZoomModeAction(PlotAction):
@@ -53,25 +51,58 @@ class ZoomModeAction(PlotAction):
def __init__(self, plot, parent=None):
super(ZoomModeAction, self).__init__(
- plot, icon='zoom', text='Zoom mode',
- tooltip='Zoom in or out',
+ plot,
+ icon="zoom",
+ text="Zoom mode",
+ tooltip="Zoom-in on mouse selection",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
- # Listen to mode change
- self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
+ checkable=True,
+ parent=parent,
+ )
+
+ self.__menu = ZoomEnabledAxesMenu(self.plot, self.plot)
+
+ # Listen to interaction configuration change
+ self.plot.interaction().sigChanged.connect(self._interactionChanged)
# Init the state
- self._modeChanged(None)
+ self._interactionChanged()
+
+ def isAxesMenuEnabled(self) -> bool:
+ """Returns whether the axes selection menu is enabled or not (default: False)"""
+ return self.menu() is self.__menu
+
+ def setAxesMenuEnabled(self, enabled: bool):
+ """Toggle the availability of the axes selection menu (default: False)"""
+ if enabled == self.isAxesMenuEnabled():
+ return
+
+ self.setMenu(self.__menu if enabled else None)
+
+ # Update associated QToolButton's popupMode if any, this is not done at least with Qt5
+ parent = self.parent()
+ if not isinstance(parent, qt.QToolBar):
+ return
+ widget = parent.widgetForAction(self)
+ if not isinstance(widget, qt.QToolButton):
+ return
+ widget.setPopupMode(
+ qt.QToolButton.MenuButtonPopup if enabled else qt.QToolButton.DelayedPopup
+ )
+ widget.update()
+
+ def _interactionChanged(self):
+ plot = self.plot
+ if plot is None:
+ return
- def _modeChanged(self, source):
- modeDict = self.plot.getInteractiveMode()
- old = self.blockSignals(True)
- self.setChecked(modeDict["mode"] == "zoom")
- self.blockSignals(old)
+ self.setChecked(plot.getInteractiveMode()["mode"] == "zoom")
def _actionTriggered(self, checked=False):
plot = self.plot
- if plot is not None:
- plot.setInteractiveMode('zoom', source=self)
+ if plot is None:
+ return
+
+ plot.setInteractiveMode("zoom", source=self)
class PanModeAction(PlotAction):
@@ -83,10 +114,14 @@ class PanModeAction(PlotAction):
def __init__(self, plot, parent=None):
super(PanModeAction, self).__init__(
- plot, icon='pan', text='Pan mode',
- tooltip='Pan the view',
+ plot,
+ icon="pan",
+ text="Pan mode",
+ tooltip="Pan the view",
triggered=self._actionTriggered,
- checkable=True, parent=parent)
+ checkable=True,
+ parent=parent,
+ )
# Listen to mode change
self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
# Init the state
@@ -101,4 +136,4 @@ class PanModeAction(PlotAction):
def _actionTriggered(self, checked=False):
plot = self.plot
if plot is not None:
- plot.setInteractiveMode('pan', source=self)
+ plot.setInteractiveMode("pan", source=self)
diff --git a/src/silx/gui/plot/backends/BackendBase.py b/src/silx/gui/plot/backends/BackendBase.py
index 1e86807..8d70286 100755
--- a/src/silx/gui/plot/backends/BackendBase.py
+++ b/src/silx/gui/plot/backends/BackendBase.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,20 +28,26 @@ It documents the Plot backend API.
This API is a simplified version of PyMca PlotBackend API.
"""
+from __future__ import annotations
+
+
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "21/12/2018"
+from collections.abc import Callable
import weakref
+from silx.gui.colors import RGBAColorType
+
from ... import qt
# Names for setCursor
-CURSOR_DEFAULT = 'default'
-CURSOR_POINTING = 'pointing'
-CURSOR_SIZE_HOR = 'size horizontal'
-CURSOR_SIZE_VER = 'size vertical'
-CURSOR_SIZE_ALL = 'size all'
+CURSOR_DEFAULT = "default"
+CURSOR_POINTING = "pointing"
+CURSOR_SIZE_HOR = "size horizontal"
+CURSOR_SIZE_VER = "size vertical"
+CURSOR_SIZE_ALL = "size all"
class BackendBase(object):
@@ -54,8 +59,8 @@ class BackendBase(object):
:param Plot plot: The Plot this backend is attached to
:param parent: The parent widget of the plot widget.
"""
- self.__xLimits = 1., 100.
- self.__yLimits = {'left': (1., 100.), 'right': (1., 100.)}
+ self.__xLimits = 1.0, 100.0
+ self.__yLimits = {"left": (1.0, 100.0), "right": (1.0, 100.0)}
self.__yAxisInverted = False
self.__keepDataAspectRatio = False
self.__xAxisTimeSeries = False
@@ -67,11 +72,11 @@ class BackendBase(object):
def _plot(self):
"""The plot this backend is attached to."""
if self._plotRef is None:
- raise RuntimeError('This backend is not attached to a Plot')
+ raise RuntimeError("This backend is not attached to a Plot")
plot = self._plotRef()
if plot is None:
- raise RuntimeError('This backend is no more attached to a Plot')
+ raise RuntimeError("This backend is no more attached to a Plot")
return plot
def _setPlot(self, plot):
@@ -83,11 +88,23 @@ class BackendBase(object):
# Add methods
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
+ def addCurve(
+ self,
+ x,
+ y,
+ color,
+ gapcolor,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ xerror,
+ yerror,
+ fill,
+ alpha,
+ symbolsize,
+ baseline,
+ ):
"""Add a 1D curve given by x an y to the graph.
:param numpy.ndarray x: The data corresponding to the x axis
@@ -95,6 +112,8 @@ class BackendBase(object):
:param color: color(s) to be used
:type color: string ("#RRGGBB") or (npoints, 4) unsigned byte array or
one of the predefined color names defined in colors.py
+ :param Union[str, None] gapcolor:
+ color used to fill dashed line gaps.
:param str symbol: Symbol to be drawn at each (x, y) position::
- ' ' or '' no symbol
@@ -107,13 +126,14 @@ class BackendBase(object):
- 's' square
:param float linewidth: The width of the curve in pixels
- :param str linestyle: Type of line::
+ :param linestyle: Type of line::
- ' ' or '' no line
- '-' solid line
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
+ - (offset, (dash pattern))
:param str yaxis: The Y axis this curve belongs to in: 'left', 'right'
:param xerror: Values with the uncertainties on the x values
@@ -128,9 +148,7 @@ class BackendBase(object):
"""
return object()
- def addImage(self, data,
- origin, scale,
- colormap, alpha):
+ def addImage(self, data, origin, scale, colormap, alpha):
"""Add an image to the plot.
:param numpy.ndarray data: (nrows, ncolumns) data or
@@ -148,8 +166,7 @@ class BackendBase(object):
"""
return object()
- def addTriangles(self, x, y, triangles,
- color, alpha):
+ def addTriangles(self, x, y, triangles, color, alpha):
"""Add a set of triangles.
:param numpy.ndarray x: The data corresponding to the x axis
@@ -162,8 +179,9 @@ class BackendBase(object):
"""
return object()
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
+ def addShape(
+ self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
+ ):
"""Add an item (i.e. a shape) to the plot.
:param numpy.ndarray x: The X coords of the points of the shape
@@ -173,7 +191,7 @@ class BackendBase(object):
:param str color: Color of the item
:param bool fill: True to fill the shape
:param bool overlay: True if item is an overlay, False otherwise
- :param str linestyle: Style of the line.
+ :param linestyle: Style of the line.
Only relevant for line markers where X or Y is None.
Value in:
@@ -182,25 +200,39 @@ class BackendBase(object):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
+ - (offset, (dash pattern))
:param float linewidth: Width of the line.
Only relevant for line markers where X or Y is None.
- :param str linebgcolor: Background color of the line, e.g., 'blue', 'b',
+ :param str gapcolor: Background color of the line, e.g., 'blue', 'b',
'#FF0000'. It is used to draw dotted line using a second color.
:returns: The handle used by the backend to univocally access the item
"""
return object()
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
+ def addMarker(
+ self,
+ x: float | None,
+ y: float | None,
+ text: str | None,
+ color: str,
+ symbol: str | None,
+ linestyle: str | tuple[float, tuple[float, ...] | None],
+ linewidth: float,
+ constraint: Callable[[float, float], tuple[float, float]] | None,
+ yaxis: str,
+ font: qt.QFont,
+ bgcolor: RGBAColorType | None,
+ ) -> object:
"""Add a point, vertical line or horizontal line marker to the plot.
- :param float x: Horizontal position of the marker in graph coordinates.
- If None, the marker is a horizontal line.
- :param float y: Vertical position of the marker in graph coordinates.
- If None, the marker is a vertical line.
- :param str text: Text associated to the marker (or None for no text)
- :param str color: Color to be used for instance 'blue', 'b', '#FF0000'
- :param str symbol: Symbol representing the marker.
+ :param x: Horizontal position of the marker in graph coordinates.
+ If None, the marker is a horizontal line.
+ :param y: Vertical position of the marker in graph coordinates.
+ If None, the marker is a vertical line.
+ :param text: Text associated to the marker (or None for no text)
+ :param color: Color to be used for instance 'blue', 'b', '#FF0000'
+ :param bgcolor: Text background color to be used for instance 'blue', 'b', '#FF0000'
+ :param symbol: Symbol representing the marker.
Only relevant for point markers where X and Y are not None.
Value in:
@@ -211,7 +243,7 @@ class BackendBase(object):
- 'x' x-cross
- 'd' diamond
- 's' square
- :param str linestyle: Style of the line.
+ :param linestyle: Style of the line.
Only relevant for line markers where X or Y is None.
Value in:
@@ -220,16 +252,16 @@ class BackendBase(object):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
- :param float linewidth: Width of the line.
+ - (offset, (dash pattern))
+ :param linewidth: Width of the line.
Only relevant for line markers where X or Y is None.
: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.
- :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.
- :param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
+ dragging operations or None for no filter.
+ This function is called each time a marker is moved.
+ It takes the coordinates of the current cursor position in the plot
+ as input and that returns the filtered coordinates.
+ :param yaxis: The Y axis this marker belongs to in: 'left', 'right'
+ :param font: QFont to use to render text
:return: Handle used by the backend to univocally access the marker
"""
return object()
@@ -271,8 +303,9 @@ class BackendBase(object):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
+ - (offset, (dash pattern))
- :type linestyle: None or one of the predefined styles.
+ :type linestyle: None, one of the predefined styles or (offset, (dash pattern)).
"""
pass
@@ -296,8 +329,8 @@ class BackendBase(object):
content = [item for item in content if condition(item)]
return sorted(
- content,
- key=lambda i: ((1 if i.isOverlay() else 0), i.getZValue()))
+ content, key=lambda i: ((1 if i.isOverlay() else 0), i.getZValue())
+ )
def pickItem(self, x, y, item):
"""Return picked indices if any, or None.
@@ -385,9 +418,9 @@ class BackendBase(object):
:param float y2max: maximum right axis value
"""
self.__xLimits = xmin, xmax
- self.__yLimits['left'] = ymin, ymax
+ self.__yLimits["left"] = ymin, ymax
if y2min is not None and y2max is not None:
- self.__yLimits['right'] = y2min, y2max
+ self.__yLimits["right"] = y2min, y2max
def getGraphXLimits(self):
"""Get the graph X (bottom) limits.
@@ -423,7 +456,6 @@ class BackendBase(object):
# Graph axes
-
def getXAxisTimeZone(self):
"""Returns tzinfo that is used if the X-Axis plots date-times.
@@ -481,6 +513,10 @@ class BackendBase(object):
"""Return True if left Y axis is inverted, False otherwise."""
return self.__yAxisInverted
+ def isYRightAxisVisible(self) -> bool:
+ """Return True if the Y axis on the right side of the plot is visible"""
+ return False
+
def isKeepDataAspectRatio(self):
"""Returns whether the plot is keeping data aspect ratio or not."""
return self.__keepDataAspectRatio
@@ -507,8 +543,10 @@ class BackendBase(object):
"""Convert a position in data space to a position in pixels
in the widget.
- :param float x: The X coordinate in data space.
- :param float y: The Y coordinate in data space.
+ :param x: The X coordinate in data space.
+ :type x: float or sequence of float
+ :param y: The Y coordinate in data space.
+ :type y: float or sequence of float
:param str axis: The Y axis to use for the conversion
('left' or 'right').
:returns: The corresponding position in pixels or
@@ -552,7 +590,7 @@ class BackendBase(object):
def setForegroundColors(self, foregroundColor, gridColor):
"""Set foreground and grid colors used to display this widget.
-
+
:param List[float] foregroundColor: RGBA foreground color of the widget
:param List[float] gridColor: RGBA grid color of the data view
"""
diff --git a/src/silx/gui/plot/backends/BackendMatplotlib.py b/src/silx/gui/plot/backends/BackendMatplotlib.py
index 7fe4ec0..facb63c 100755
--- a/src/silx/gui/plot/backends/BackendMatplotlib.py
+++ b/src/silx/gui/plot/backends/BackendMatplotlib.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,7 +23,7 @@
# ###########################################################################*/
"""Matplotlib Plot backend."""
-from __future__ import division
+from __future__ import annotations
__authors__ = ["V.A. Sole", "T. Vincent, H. Payno"]
__license__ = "MIT"
@@ -33,10 +32,10 @@ __date__ = "21/12/2018"
import logging
import datetime as dt
-from typing import Tuple
+from typing import Tuple, Union
import numpy
-from pkg_resources import parse_version as _parse_version
+from packaging.version import Version
_logger = logging.getLogger(__name__)
@@ -45,7 +44,11 @@ _logger = logging.getLogger(__name__)
from ... import qt
# First of all init matplotlib and set its backend
-from ...utils.matplotlib import FigureCanvasQTAgg
+from ...utils.matplotlib import (
+ DefaultTickFormatter,
+ FigureCanvasQTAgg,
+ qFontToFontProperties,
+)
import matplotlib
from matplotlib.container import Container
from matplotlib.figure import Figure
@@ -55,7 +58,7 @@ from matplotlib.backend_bases import MouseEvent
from matplotlib.lines import Line2D
from matplotlib.text import Text
from matplotlib.collections import PathCollection, LineCollection
-from matplotlib.ticker import Formatter, ScalarFormatter, Locator
+from matplotlib.ticker import Formatter, Locator
from matplotlib.tri import Triangulation
from matplotlib.collections import TriMesh
from matplotlib import path as mpath
@@ -63,14 +66,21 @@ from matplotlib import path as mpath
from . import BackendBase
from .. import items
from .._utils import FLOAT32_MINPOS
-from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp
+from .._utils.dtime_ticklayout import (
+ calcTicks,
+ formatDatetimes,
+ timestamp,
+)
+from ...qt import inspect as qt_inspect
+from .... import config
+from silx.gui.colors import RGBAColorType
_PATCH_LINESTYLE = {
- "-": 'solid',
- "--": 'dashed',
- '-.': 'dashdot',
- ':': 'dotted',
- '': "solid",
+ "-": "solid",
+ "--": "dashed",
+ "-.": "dashdot",
+ ":": "dotted",
+ "": "solid",
None: "solid",
}
"""Patches do not uses the same matplotlib syntax"""
@@ -79,14 +89,14 @@ _MARKER_PATHS = {}
"""Store cached extra marker paths"""
_SPECIAL_MARKERS = {
- 'tickleft': 0,
- 'tickright': 1,
- 'tickup': 2,
- 'tickdown': 3,
- 'caretleft': 4,
- 'caretright': 5,
- 'caretup': 6,
- 'caretdown': 7,
+ "tickleft": 0,
+ "tickright": 1,
+ "tickup": 2,
+ "tickdown": 3,
+ "caretleft": 4,
+ "caretright": 5,
+ "caretup": 6,
+ "caretdown": 7,
}
@@ -94,6 +104,7 @@ def normalize_linestyle(linestyle):
"""Normalize known old-style linestyle, else return the provided value."""
return _PATCH_LINESTYLE.get(linestyle, linestyle)
+
def get_path_from_symbol(symbol):
"""Get the path representation of a symbol, else None if
it is not provided.
@@ -101,21 +112,40 @@ def get_path_from_symbol(symbol):
:param str symbol: Symbol description used by silx
:rtype: Union[None,matplotlib.path.Path]
"""
- if symbol == u'\u2665':
+ if symbol == "\u2665":
path = _MARKER_PATHS.get(symbol, None)
if path is not None:
return path
- vertices = numpy.array([
- [0,-99],
- [31,-73], [47,-55], [55,-46],
- [63,-37], [94,-2], [94,33],
- [94,69], [71,89], [47,89],
- [24,89], [8,74], [0,58],
- [-8,74], [-24,89], [-47,89],
- [-71,89], [-94,69], [-94,33],
- [-94,-2], [-63,-37], [-55,-46],
- [-47,-55], [-31,-73], [0,-99],
- [0,-99]])
+ vertices = numpy.array(
+ [
+ [0, -99],
+ [31, -73],
+ [47, -55],
+ [55, -46],
+ [63, -37],
+ [94, -2],
+ [94, 33],
+ [94, 69],
+ [71, 89],
+ [47, 89],
+ [24, 89],
+ [8, 74],
+ [0, 58],
+ [-8, 74],
+ [-24, 89],
+ [-47, 89],
+ [-71, 89],
+ [-94, 69],
+ [-94, 33],
+ [-94, -2],
+ [-63, -37],
+ [-55, -46],
+ [-47, -55],
+ [-31, -73],
+ [0, -99],
+ [0, -99],
+ ]
+ )
codes = [mpath.Path.CURVE4] * len(vertices)
codes[0] = mpath.Path.MOVETO
codes[-1] = mpath.Path.CLOSEPOLY
@@ -124,6 +154,7 @@ def get_path_from_symbol(symbol):
return path
return None
+
class NiceDateLocator(Locator):
"""
Matplotlib Locator that uses Nice Numbers algorithm (adapted to dates)
@@ -132,6 +163,7 @@ class NiceDateLocator(Locator):
Expects the data to be posix timestampes (i.e. seconds since 1970)
"""
+
def __init__(self, numTicks=5, tz=None):
"""
:param numTicks: target number of ticks
@@ -146,12 +178,12 @@ class NiceDateLocator(Locator):
@property
def spacing(self):
- """ The current spacing. Will be updated when new tick value are made"""
+ """The current spacing. Will be updated when new tick value are made"""
return self._spacing
@property
def unit(self):
- """ The current DtUnit. Will be updated when new tick value are made"""
+ """The current DtUnit. Will be updated when new tick value are made"""
return self._unit
def __call__(self):
@@ -160,16 +192,19 @@ class NiceDateLocator(Locator):
return self.tick_values(vmin, vmax)
def tick_values(self, vmin, vmax):
- """ Calculates tick values
- """
+ """Calculates tick values"""
if vmax < vmin:
vmin, vmax = vmax, vmin
# vmin and vmax should be timestamps (i.e. seconds since 1 Jan 1970)
- dtMin = dt.datetime.fromtimestamp(vmin, tz=self.tz)
- dtMax = dt.datetime.fromtimestamp(vmax, tz=self.tz)
- dtTicks, self._spacing, self._unit = \
- calcTicks(dtMin, dtMax, self.numTicks)
+ try:
+ dtMin = dt.datetime.fromtimestamp(vmin, tz=self.tz)
+ dtMax = dt.datetime.fromtimestamp(vmax, tz=self.tz)
+ except ValueError:
+ _logger.warning("Data range cannot be displayed with time axis")
+ return []
+
+ dtTicks, self._spacing, self._unit = calcTicks(dtMin, dtMax, self.numTicks)
# Convert datetime back to time stamps.
ticks = [timestamp(dtTick) for dtTick in dtTicks]
@@ -191,21 +226,25 @@ class NiceAutoDateFormatter(Formatter):
self.locator = locator
self.tz = tz
- @property
- def formatString(self):
- if self.locator.spacing is None or self.locator.unit is None:
- # Locator has no spacing or units yet. Return elaborate fmtString
- return "Y-%m-%d %H:%M:%S"
- else:
- return bestFormatString(self.locator.spacing, self.locator.unit)
-
def __call__(self, x, pos=None):
"""Return the format for tick val *x* at position *pos*
- Expects x to be a POSIX timestamp (seconds since 1 Jan 1970)
+ Expects x to be a POSIX timestamp (seconds since 1 Jan 1970)
"""
- dateTime = dt.datetime.fromtimestamp(x, tz=self.tz)
- tickStr = dateTime.strftime(self.formatString)
- return tickStr
+ datetime = dt.datetime.fromtimestamp(x, tz=self.tz)
+ return formatDatetimes(
+ [datetime],
+ self.locator.spacing,
+ self.locator.unit,
+ )[datetime]
+
+ def format_ticks(self, values):
+ return tuple(
+ formatDatetimes(
+ [dt.datetime.fromtimestamp(value, tz=self.tz) for value in values],
+ self.locator.spacing,
+ self.locator.unit,
+ ).values()
+ )
class _PickableContainer(Container):
@@ -219,7 +258,7 @@ class _PickableContainer(Container):
def axes(self):
"""Mimin Artist.axes"""
for child in self.get_children():
- if hasattr(child, 'axes'):
+ if hasattr(child, "axes"):
return child.axes
return None
@@ -351,18 +390,19 @@ class _MarkerContainer(_PickableContainer):
:param yinverted: True if the y axis is inverted
"""
if self.text is not None:
- visible = ((self.x is None or xmin <= self.x <= xmax) and
- (self.y is None or ymin <= self.y <= ymax))
+ visible = (self.x is None or xmin <= self.x <= xmax) and (
+ self.y is None or ymin <= self.y <= ymax
+ )
self.text.set_visible(visible)
if self.x is not None and self.y is not None:
if self.symbol is None:
- valign = 'baseline'
+ valign = "baseline"
else:
if yinverted:
- valign = 'bottom'
+ valign = "bottom"
else:
- valign = 'top'
+ valign = "top"
self.text.set_verticalalignment(valign)
elif self.y is None: # vertical line
@@ -390,42 +430,47 @@ class _MarkerContainer(_PickableContainer):
return self.line.contains(mouseevent)
-class _DoubleColoredLinePatch(matplotlib.patches.Patch):
- """Matplotlib patch to display any patch using double color."""
+class SecondEdgeColorPatchMixIn:
+ """Mix-in class to add a second color for patches with dashed lines"""
- def __init__(self, patch):
- super(_DoubleColoredLinePatch, self).__init__()
- self.__patch = patch
- self.linebgcolor = None
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._second_edgecolor = None
+
+ def set_second_edgecolor(self, color):
+ """Set the second color used to fill dashed edges"""
+ self._second_edgecolor = color
- def __getattr__(self, name):
- return getattr(self.__patch, name)
+ def get_second_edgecolor(self):
+ """Returns the second color used to fill dashed edges"""
+ return self._second_edgecolor
def draw(self, renderer):
- oldLineStype = self.__patch.get_linestyle()
- if self.linebgcolor is not None and oldLineStype != "solid":
- oldLineColor = self.__patch.get_edgecolor()
- oldHatch = self.__patch.get_hatch()
- self.__patch.set_linestyle("solid")
- self.__patch.set_edgecolor(self.linebgcolor)
- self.__patch.set_hatch(None)
- self.__patch.draw(renderer)
- self.__patch.set_linestyle(oldLineStype)
- self.__patch.set_edgecolor(oldLineColor)
- self.__patch.set_hatch(oldHatch)
- self.__patch.draw(renderer)
+ linestyle = self.get_linestyle()
+ if linestyle == "solid" or self.get_second_edgecolor() is None:
+ super().draw(renderer)
+ return
+
+ edgecolor = self.get_edgecolor()
+ hatch = self.get_hatch()
- def set_transform(self, transform):
- self.__patch.set_transform(transform)
+ self.set_linestyle("solid")
+ self.set_edgecolor(self.get_second_edgecolor())
+ self.set_hatch(None)
+ super().draw(renderer)
- def get_path(self):
- return self.__patch.get_path()
+ self.set_linestyle(linestyle)
+ self.set_edgecolor(edgecolor)
+ self.set_hatch(hatch)
+ super().draw(renderer)
- def contains(self, mouseevent, radius=None):
- return self.__patch.contains(mouseevent, radius)
- def contains_point(self, point, radius=None):
- return self.__patch.contains_point(point, radius)
+class Rectangle2EdgeColor(SecondEdgeColorPatchMixIn, Rectangle):
+ """Rectangle patch with a second edge color for dashed line"""
+
+
+class Polygon2EdgeColor(SecondEdgeColorPatchMixIn, Polygon):
+ """Polygon patch with a second edge color for dashed line"""
class Image(AxesImage):
@@ -435,10 +480,7 @@ class Image(AxesImage):
:param List[float] silx_scale: (sx, sy) Scale of the image.
"""
- def __init__(self, *args,
- silx_origin=(0., 0.),
- silx_scale=(1., 1.),
- **kwargs):
+ def __init__(self, *args, silx_origin=(0.0, 0.0), silx_scale=(1.0, 1.0), **kwargs):
super().__init__(*args, **kwargs)
self.__silx_origin = silx_origin
self.__silx_scale = silx_scale
@@ -453,7 +495,7 @@ class Image(AxesImage):
height, width = self.get_size()
column = numpy.clip(int((x - ox) / sx), 0, width - 1)
row = numpy.clip(int((y - oy) / sy), 0, height - 1)
- info['ind'] = (row,), (column,)
+ info["ind"] = (row,), (column,)
return inside, info
def set_data(self, A):
@@ -486,12 +528,17 @@ class BackendMatplotlib(BackendBase.BackendBase):
# when getting the limits at the expense of a replot
self._dirtyLimits = True
self._axesDisplayed = True
- self._matplotlibVersion = _parse_version(matplotlib.__version__)
+ self._matplotlibVersion = Version(matplotlib.__version__)
- self.fig = Figure()
+ self.fig = Figure(
+ tight_layout=config._MPL_TIGHT_LAYOUT,
+ )
self.fig.set_facecolor("w")
- self.ax = self.fig.add_axes([.15, .15, .75, .75], label="left")
+ if config._MPL_TIGHT_LAYOUT:
+ self.ax = self.fig.add_subplot(label="left")
+ else:
+ self.ax = self.fig.add_axes([0.15, 0.15, 0.75, 0.75], label="left")
self.ax2 = self.ax.twinx()
self.ax2.set_label("right")
# Make sure background of Axes is displayed
@@ -501,28 +548,17 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Set axis zorder=0.5 so grid is displayed at 0.5
self.ax.set_axisbelow(True)
- # disable the use of offsets
- try:
- axes = [
- self.ax.get_yaxis().get_major_formatter(),
- self.ax.get_xaxis().get_major_formatter(),
- self.ax2.get_yaxis().get_major_formatter(),
- self.ax2.get_xaxis().get_major_formatter(),
- ]
- for axis in axes:
- axis.set_useOffset(False)
- axis.set_scientific(False)
- except:
- _logger.warning('Cannot disabled axes offsets in %s '
- % matplotlib.__version__)
+ # Configure axes tick label formatter
+ for axis in (self.ax.yaxis, self.ax.xaxis, self.ax2.yaxis, self.ax2.xaxis):
+ axis.set_major_formatter(DefaultTickFormatter())
self.ax2.set_autoscaley_on(True)
# this works but the figure color is left
- if self._matplotlibVersion < _parse_version('2'):
- self.ax.set_axis_bgcolor('none')
+ if self._matplotlibVersion < Version("2"):
+ self.ax.set_axis_bgcolor("none")
else:
- self.ax.set_facecolor('none')
+ self.ax.set_facecolor("none")
self.fig.sca(self.ax)
self._background = None
@@ -531,30 +567,33 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._graphCursor = tuple()
- self._enableAxis('right', False)
+ self._enableAxis("right", False)
self._isXAxisTimeSeries = False
def getItemsFromBackToFront(self, condition=None):
"""Order as BackendBase + take into account matplotlib Axes structure"""
+
def axesOrder(item):
if item.isOverlay():
return 2
- elif isinstance(item, items.YAxisMixIn) and item.getYAxis() == 'right':
+ elif isinstance(item, items.YAxisMixIn) and item.getYAxis() == "right":
return 1
else:
return 0
return sorted(
- BackendBase.BackendBase.getItemsFromBackToFront(
- self, condition=condition),
- key=axesOrder)
+ BackendBase.BackendBase.getItemsFromBackToFront(self, condition=condition),
+ key=axesOrder,
+ )
def _overlayItems(self):
"""Generator of backend renderer for overlay items"""
for item in self._plot.getItems():
- if (item.isOverlay() and
- item.isVisible() and
- item._backendRenderer is not None):
+ if (
+ item.isOverlay()
+ and item.isVisible()
+ and item._backendRenderer is not None
+ ):
yield item._backendRenderer
def _hasOverlays(self):
@@ -588,19 +627,40 @@ class BackendMatplotlib(BackendBase.BackendBase):
# This symbol must be supported by matplotlib
return symbol
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
- for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, fill, alpha, symbolsize):
+ def addCurve(
+ self,
+ x,
+ y,
+ color,
+ gapcolor,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ xerror,
+ yerror,
+ fill,
+ alpha,
+ symbolsize,
+ baseline,
+ ):
+ for parameter in (
+ x,
+ y,
+ color,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ fill,
+ alpha,
+ symbolsize,
+ ):
assert parameter is not None
- assert yaxis in ('left', 'right')
+ assert yaxis in ("left", "right")
- if (len(color) == 4 and
- type(color[3]) in [type(1), numpy.uint8, numpy.int8]):
- color = numpy.array(color, dtype=numpy.float64) / 255.
+ if len(color) == 4 and type(color[3]) in [type(1), numpy.uint8, numpy.int8]:
+ color = numpy.array(color, dtype=numpy.float64) / 255.0
if yaxis == "right":
axes = self.ax2
@@ -614,50 +674,62 @@ class BackendMatplotlib(BackendBase.BackendBase):
# First add errorbars if any so they are behind the curve
if xerror is not None or yerror is not None:
- if hasattr(color, 'dtype') and len(color) == len(x):
- errorbarColor = 'k'
+ if hasattr(color, "dtype") and len(color) == len(x):
+ errorbarColor = "k"
else:
errorbarColor = color
# Nx1 error array deprecated in matplotlib >=3.1 (removed in 3.3)
- if (isinstance(xerror, numpy.ndarray) and xerror.ndim == 2 and
- xerror.shape[1] == 1):
+ if (
+ isinstance(xerror, numpy.ndarray)
+ and xerror.ndim == 2
+ and xerror.shape[1] == 1
+ ):
xerror = numpy.ravel(xerror)
- if (isinstance(yerror, numpy.ndarray) and yerror.ndim == 2 and
- yerror.shape[1] == 1):
+ if (
+ isinstance(yerror, numpy.ndarray)
+ and yerror.ndim == 2
+ and yerror.shape[1] == 1
+ ):
yerror = numpy.ravel(yerror)
- errorbars = axes.errorbar(x, y,
- xerr=xerror, yerr=yerror,
- linestyle=' ', color=errorbarColor)
+ errorbars = axes.errorbar(
+ x, y, xerr=xerror, yerr=yerror, linestyle=" ", color=errorbarColor
+ )
artists += list(errorbars.get_children())
- if hasattr(color, 'dtype') and len(color) == len(x):
+ if hasattr(color, "dtype") and len(color) == len(x):
# scatter plot
if color.dtype not in [numpy.float32, numpy.float64]:
- actualColor = color / 255.
+ actualColor = color / 255.0
else:
actualColor = color
if linestyle not in ["", " ", None]:
# scatter plot with an actual line ...
# we need to assign a color ...
- curveList = axes.plot(x, y,
- linestyle=linestyle,
- color=actualColor[0],
- linewidth=linewidth,
- picker=True,
- pickradius=pickradius,
- marker=None)
+ curveList = axes.plot(
+ x,
+ y,
+ linestyle=linestyle,
+ color=actualColor[0],
+ linewidth=linewidth,
+ picker=True,
+ pickradius=pickradius,
+ marker=None,
+ )
artists += list(curveList)
marker = self._getMarkerFromSymbol(symbol)
- scatter = axes.scatter(x, y,
- color=actualColor,
- marker=marker,
- picker=True,
- pickradius=pickradius,
- s=symbolsize**2)
+ scatter = axes.scatter(
+ x,
+ y,
+ color=actualColor,
+ marker=marker,
+ picker=True,
+ pickradius=pickradius,
+ s=symbolsize**2,
+ )
artists.append(scatter)
if fill:
@@ -665,18 +737,28 @@ class BackendMatplotlib(BackendBase.BackendBase):
_baseline = FLOAT32_MINPOS
else:
_baseline = baseline
- artists.append(axes.fill_between(
- x, _baseline, y, facecolor=actualColor[0], linestyle=''))
+ artists.append(
+ axes.fill_between(
+ x, _baseline, y, facecolor=actualColor[0], linestyle=""
+ )
+ )
else: # Curve
- curveList = axes.plot(x, y,
- linestyle=linestyle,
- color=color,
- linewidth=linewidth,
- marker=symbol,
- picker=True,
- pickradius=pickradius,
- markersize=symbolsize)
+ curveList = axes.plot(
+ x,
+ y,
+ linestyle=linestyle,
+ color=color,
+ linewidth=linewidth,
+ marker=symbol,
+ picker=True,
+ pickradius=pickradius,
+ markersize=symbolsize,
+ )
+
+ if gapcolor is not None and self._matplotlibVersion >= Version("3.6.0"):
+ for line2d in curveList:
+ line2d.set_gapcolor(gapcolor)
artists += list(curveList)
if fill:
@@ -684,8 +766,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
_baseline = FLOAT32_MINPOS
else:
_baseline = baseline
- artists.append(
- axes.fill_between(x, _baseline, y, facecolor=color))
+ artists.append(axes.fill_between(x, _baseline, y, facecolor=color))
for artist in artists:
if alpha < 1:
@@ -706,12 +787,14 @@ class BackendMatplotlib(BackendBase.BackendBase):
height, width = data.shape[0:2]
# All image are shown as RGBA image
- image = Image(self.ax,
- interpolation='nearest',
- picker=True,
- origin='lower',
- silx_origin=origin,
- silx_scale=scale)
+ image = Image(
+ self.ax,
+ interpolation="nearest",
+ picker=True,
+ origin="lower",
+ silx_origin=origin,
+ silx_scale=scale,
+ )
if alpha < 1:
image.set_alpha(alpha)
@@ -719,21 +802,21 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Set image extent
xmin = origin[0]
xmax = xmin + scale[0] * width
- if scale[0] < 0.:
+ if scale[0] < 0.0:
xmin, xmax = xmax, xmin
ymin = origin[1]
ymax = ymin + scale[1] * height
- if scale[1] < 0.:
+ if scale[1] < 0.0:
ymin, ymax = ymax, ymin
image.set_extent((xmin, xmax, ymin, ymax))
# Set image data
- if scale[0] < 0. or scale[1] < 0.:
+ if scale[0] < 0.0 or scale[1] < 0.0:
# For negative scale, step by -1
- xstep = 1 if scale[0] >= 0. else -1
- ystep = 1 if scale[1] >= 0. else -1
+ xstep = 1 if scale[0] >= 0.0 else -1
+ ystep = 1 if scale[1] >= 0.0 else -1
data = data[::ystep, ::xstep]
if data.ndim == 2: # Data image, convert to RGBA image
@@ -742,7 +825,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Normalize uint16 data to have a similar behavior as opengl backend
data = data.astype(numpy.float32)
data /= 65535
-
+
image.set_data(data)
self.ax.add_artist(image)
return image
@@ -755,87 +838,92 @@ class BackendMatplotlib(BackendBase.BackendBase):
assert color.ndim == 2 and len(color) == len(x)
if color.dtype not in [numpy.float32, numpy.float64]:
- color = color.astype(numpy.float32) / 255.
+ color = color.astype(numpy.float32) / 255.0
collection = TriMesh(
- Triangulation(x, y, triangles),
- alpha=alpha,
- pickradius=0) # 0 enables picking on filled triangle
+ Triangulation(x, y, triangles), alpha=alpha, pickradius=0
+ ) # 0 enables picking on filled triangle
collection.set_color(color)
self.ax.add_collection(collection)
return collection
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
- if (linebgcolor is not None and
- shape not in ('rectangle', 'polygon', 'polylines')):
+ def addShape(
+ self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
+ ):
+ if gapcolor is not None and shape not in (
+ "rectangle",
+ "polygon",
+ "polylines",
+ ):
_logger.warning(
- 'linebgcolor not implemented for %s with matplotlib backend',
- shape)
+ "gapcolor not implemented for %s with matplotlib backend", shape
+ )
xView = numpy.array(x, copy=False)
yView = numpy.array(y, copy=False)
linestyle = normalize_linestyle(linestyle)
if shape == "line":
- item = self.ax.plot(x, y, color=color,
- linestyle=linestyle, linewidth=linewidth,
- marker=None)[0]
+ item = self.ax.plot(
+ x, y, color=color, linestyle=linestyle, linewidth=linewidth, marker=None
+ )[0]
elif shape == "hline":
if hasattr(y, "__len__"):
y = y[-1]
- item = self.ax.axhline(y, color=color,
- linestyle=linestyle, linewidth=linewidth)
+ item = self.ax.axhline(
+ y, color=color, linestyle=linestyle, linewidth=linewidth
+ )
elif shape == "vline":
if hasattr(x, "__len__"):
x = x[-1]
- item = self.ax.axvline(x, color=color,
- linestyle=linestyle, linewidth=linewidth)
+ item = self.ax.axvline(
+ x, color=color, linestyle=linestyle, linewidth=linewidth
+ )
- elif shape == 'rectangle':
+ elif shape == "rectangle":
xMin = numpy.nanmin(xView)
xMax = numpy.nanmax(xView)
yMin = numpy.nanmin(yView)
yMax = numpy.nanmax(yView)
w = xMax - xMin
h = yMax - yMin
- item = Rectangle(xy=(xMin, yMin),
- width=w,
- height=h,
- fill=False,
- color=color,
- linestyle=linestyle,
- linewidth=linewidth)
- if fill:
- item.set_hatch('.')
+ item = Rectangle2EdgeColor(
+ xy=(xMin, yMin),
+ width=w,
+ height=h,
+ fill=False,
+ color=color,
+ linestyle=linestyle,
+ linewidth=linewidth,
+ )
+ item.set_second_edgecolor(gapcolor)
- if linestyle != "solid" and linebgcolor is not None:
- item = _DoubleColoredLinePatch(item)
- item.linebgcolor = linebgcolor
+ if fill:
+ item.set_hatch(".")
self.ax.add_patch(item)
- elif shape in ('polygon', 'polylines'):
+ elif shape in ("polygon", "polylines"):
points = numpy.array((xView, yView)).T
- if shape == 'polygon':
+ if shape == "polygon":
closed = True
else: # shape == 'polylines'
closed = numpy.all(numpy.equal(points[0], points[-1]))
- item = Polygon(points,
- closed=closed,
- fill=False,
- color=color,
- linestyle=linestyle,
- linewidth=linewidth)
- if fill and shape == 'polygon':
- item.set_hatch('/')
-
- if linestyle != "solid" and linebgcolor is not None:
- item = _DoubleColoredLinePatch(item)
- item.linebgcolor = linebgcolor
+ item = Polygon2EdgeColor(
+ points,
+ closed=closed,
+ fill=False,
+ color=color,
+ linestyle=linestyle,
+ linewidth=linewidth,
+ )
+ item.set_second_edgecolor(gapcolor)
+
+ if fill and shape == "polygon":
+ item.set_hatch("/")
self.ax.add_patch(item)
@@ -847,61 +935,87 @@ class BackendMatplotlib(BackendBase.BackendBase):
return item
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
+ def addMarker(
+ self,
+ x,
+ y,
+ text,
+ color,
+ symbol,
+ linestyle,
+ linewidth,
+ constraint,
+ yaxis,
+ font,
+ bgcolor: RGBAColorType | None,
+ ):
textArtist = None
+ fontProperties = None if font is None else qFontToFontProperties(font)
xmin, xmax = self.getGraphXLimits()
ymin, ymax = self.getGraphYLimits(axis=yaxis)
- if yaxis == 'left':
+ if yaxis == "left":
ax = self.ax
- elif yaxis == 'right':
+ elif yaxis == "right":
ax = self.ax2
else:
- assert(False)
+ assert False
+
+ if bgcolor is None:
+ bgcolor = "none"
marker = self._getMarkerFromSymbol(symbol)
if x is not None and y is not None:
- line = ax.plot(x, y,
- linestyle=" ",
- color=color,
- marker=marker,
- markersize=10.)[-1]
+ line = ax.plot(
+ x, y, linestyle=" ", color=color, marker=marker, markersize=10.0
+ )[-1]
if text is not None:
- textArtist = _TextWithOffset(x, y, text,
- color=color,
- horizontalalignment='left')
+ textArtist = _TextWithOffset(
+ x,
+ y,
+ text,
+ color=color,
+ backgroundcolor=bgcolor,
+ horizontalalignment="left",
+ fontproperties=fontProperties,
+ )
if symbol is not None:
textArtist.pixel_offset = 10, 3
elif x is not None:
- line = ax.axvline(x,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
+ line = ax.axvline(x, color=color, linewidth=linewidth, linestyle=linestyle)
if text is not None:
# Y position will be updated in updateMarkerText call
- textArtist = _TextWithOffset(x, 1., text,
- color=color,
- horizontalalignment='left',
- verticalalignment='top')
+ textArtist = _TextWithOffset(
+ x,
+ 1.0,
+ text,
+ color=color,
+ backgroundcolor=bgcolor,
+ horizontalalignment="left",
+ verticalalignment="top",
+ fontproperties=fontProperties,
+ )
textArtist.pixel_offset = 5, 3
elif y is not None:
- line = ax.axhline(y,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
+ line = ax.axhline(y, color=color, linewidth=linewidth, linestyle=linestyle)
if text is not None:
# X position will be updated in updateMarkerText call
- textArtist = _TextWithOffset(1., y, text,
- color=color,
- horizontalalignment='right',
- verticalalignment='top')
+ textArtist = _TextWithOffset(
+ 1.0,
+ y,
+ text,
+ color=color,
+ backgroundcolor=bgcolor,
+ horizontalalignment="right",
+ verticalalignment="top",
+ fontproperties=fontProperties,
+ )
textArtist.pixel_offset = 5, 3
else:
- raise RuntimeError('A marker must at least have one coordinate')
+ raise RuntimeError("A marker must at least have one coordinate")
line.set_picker(True)
line.set_pickradius(5)
@@ -925,7 +1039,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
yinverted = self.isYAxisInverted()
for item in self._overlayItems():
if isinstance(item, _MarkerContainer):
- if item.yAxis == 'left':
+ if item.yAxis == "left":
item.updateMarkerText(xmin, xmax, ymin1, ymax1, yinverted)
else:
item.updateMarkerText(xmin, xmax, ymin2, ymax2, yinverted)
@@ -943,13 +1057,21 @@ class BackendMatplotlib(BackendBase.BackendBase):
def setGraphCursor(self, flag, color, linewidth, linestyle):
if flag:
lineh = self.ax.axhline(
- self.ax.get_ybound()[0], visible=False, color=color,
- linewidth=linewidth, linestyle=linestyle)
+ self.ax.get_ybound()[0],
+ visible=False,
+ color=color,
+ linewidth=linewidth,
+ linestyle=linestyle,
+ )
lineh.set_animated(True)
linev = self.ax.axvline(
- self.ax.get_xbound()[0], visible=False, color=color,
- linewidth=linewidth, linestyle=linestyle)
+ self.ax.get_xbound()[0],
+ visible=False,
+ color=color,
+ linewidth=linewidth,
+ linestyle=linestyle,
+ )
linev.set_animated(True)
self._graphCursor = lineh, linev
@@ -971,8 +1093,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
artist.set_facecolors(color)
artist.set_edgecolors(color)
else:
- _logger.warning(
- 'setActiveCurve ignoring artist %s', str(artist))
+ _logger.warning("setActiveCurve ignoring artist %s", str(artist))
# Misc.
@@ -985,8 +1106,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
:param str axis: Axis name: 'left' or 'right'
:param bool flag: Default, True
"""
- assert axis in ('right', 'left')
- axes = self.ax2 if axis == 'right' else self.ax
+ assert axis in ("right", "left")
+ axes = self.ax2 if axis == "right" else self.ax
axes.get_yaxis().set_visible(flag)
def replot(self):
@@ -1004,18 +1125,20 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Hide right Y axis if no line is present
self._dirtyLimits = False
if not self.ax2.lines:
- self._enableAxis('right', False)
+ self._enableAxis("right", False)
def _drawOverlays(self):
"""Draw overlays if any."""
+
def condition(item):
- return (item.isVisible() and
- item._backendRenderer is not None and
- item.isOverlay())
+ return (
+ item.isVisible()
+ and item._backendRenderer is not None
+ and item.isOverlay()
+ )
for item in self.getItemsFromBackToFront(condition=condition):
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
+ if isinstance(item, items.YAxisMixIn) and item.getYAxis() == "right":
axes = self.ax2
else:
axes = self.ax
@@ -1027,14 +1150,15 @@ class BackendMatplotlib(BackendBase.BackendBase):
def updateZOrder(self):
"""Reorder all items with z order from 0 to 1"""
items = self.getItemsFromBackToFront(
- lambda item: item.isVisible() and item._backendRenderer is not None)
+ lambda item: item.isVisible() and item._backendRenderer is not None
+ )
count = len(items)
for index, item in enumerate(items):
if item.getZValue() < 0.5:
# Make sure matplotlib z order is below the grid (with z=0.5)
zorder = 0.5 * index / count
else: # Make sure matplotlib z order is above the grid (> 0.5)
- zorder = 1. + index / count
+ zorder = 1.0 + index / count
if zorder != item._backendRenderer.get_zorder():
item._backendRenderer.set_zorder(zorder)
@@ -1057,7 +1181,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
self.ax.set_xlabel(label)
def setGraphYLabel(self, label, axis):
- axes = self.ax if axis == 'left' else self.ax2
+ axes = self.ax if axis == "left" else self.ax2
axes.set_ylabel(label)
# Graph limits
@@ -1093,8 +1217,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._updateMarkers()
def getGraphYLimits(self, axis):
- assert axis in ('left', 'right')
- ax = self.ax2 if axis == 'right' else self.ax
+ assert axis in ("left", "right")
+ ax = self.ax2 if axis == "right" else self.ax
if not ax.get_visible():
return None
@@ -1107,7 +1231,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
return ax.get_ybound()
def setGraphYLimits(self, ymin, ymax, axis):
- ax = self.ax2 if axis == 'right' else self.ax
+ ax = self.ax2 if axis == "right" else self.ax
if ymax < ymin:
ymin, ymax = ymax, ymin
self._dirtyLimits = True
@@ -1134,6 +1258,23 @@ class BackendMatplotlib(BackendBase.BackendBase):
# Graph axes
+ def __initXAxisFormatterAndLocator(self):
+ if self.ax.xaxis.get_scale() != "linear":
+ return # Do not override formatter and locator
+
+ if not self.isXAxisTimeSeries():
+ self.ax.xaxis.set_major_formatter(DefaultTickFormatter())
+ return
+
+ # We can't use a matplotlib.dates.DateFormatter because it expects
+ # the data to be in datetimes. Silx works internally with
+ # timestamps (floats).
+ locator = NiceDateLocator(tz=self.getXAxisTimeZone())
+ self.ax.xaxis.set_major_locator(locator)
+ self.ax.xaxis.set_major_formatter(
+ NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone())
+ )
+
def setXAxisTimeZone(self, tz):
super(BackendMatplotlib, self).setXAxisTimeZone(tz)
@@ -1145,40 +1286,27 @@ class BackendMatplotlib(BackendBase.BackendBase):
def setXAxisTimeSeries(self, isTimeSeries):
self._isXAxisTimeSeries = isTimeSeries
- if self._isXAxisTimeSeries:
- # We can't use a matplotlib.dates.DateFormatter because it expects
- # the data to be in datetimes. Silx works internally with
- # timestamps (floats).
- locator = NiceDateLocator(tz=self.getXAxisTimeZone())
- self.ax.xaxis.set_major_locator(locator)
- self.ax.xaxis.set_major_formatter(
- NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone()))
- else:
- try:
- scalarFormatter = ScalarFormatter(useOffset=False)
- except:
- _logger.warning('Cannot disabled axes offsets in %s ' %
- matplotlib.__version__)
- scalarFormatter = ScalarFormatter()
- self.ax.xaxis.set_major_formatter(scalarFormatter)
+ self.__initXAxisFormatterAndLocator()
def setXAxisLogarithmic(self, flag):
# Workaround for matplotlib 2.1.0 when one tries to set an axis
# to log scale with both limits <= 0
# In this case a draw with positive limits is needed first
- if flag and self._matplotlibVersion >= _parse_version('2.1.0'):
+ if flag and self._matplotlibVersion >= Version("2.1.0"):
xlim = self.ax.get_xlim()
if xlim[0] <= 0 and xlim[1] <= 0:
self.ax.set_xlim(1, 10)
self.draw()
- self.ax2.set_xscale('log' if flag else 'linear')
- self.ax.set_xscale('log' if flag else 'linear')
+ xscale = "log" if flag else "linear"
+ self.ax2.set_xscale(xscale)
+ self.ax.set_xscale(xscale)
+ self.__initXAxisFormatterAndLocator()
def setYAxisLogarithmic(self, flag):
# Workaround for matplotlib 2.0 issue with negative bounds
# before switching to log scale
- if flag and self._matplotlibVersion >= _parse_version('2.0.0'):
+ if flag and self._matplotlibVersion >= Version("2.0.0"):
redraw = False
for axis, dataRangeIndex in ((self.ax, 1), (self.ax2, 2)):
ylim = axis.get_ylim()
@@ -1191,8 +1319,15 @@ class BackendMatplotlib(BackendBase.BackendBase):
if redraw:
self.draw()
- self.ax2.set_yscale('log' if flag else 'linear')
- self.ax.set_yscale('log' if flag else 'linear')
+ if flag:
+ self.ax2.set_yscale("log")
+ self.ax.set_yscale("log")
+ return
+
+ self.ax2.set_yscale("linear")
+ self.ax2.yaxis.set_major_formatter(DefaultTickFormatter())
+ self.ax.set_yscale("linear")
+ self.ax.yaxis.set_major_formatter(DefaultTickFormatter())
def setYAxisInverted(self, flag):
if self.ax.yaxis_inverted() != bool(flag):
@@ -1202,15 +1337,18 @@ class BackendMatplotlib(BackendBase.BackendBase):
def isYAxisInverted(self):
return self.ax.yaxis_inverted()
+ def isYRightAxisVisible(self):
+ return self.ax2.yaxis.get_visible()
+
def isKeepDataAspectRatio(self):
- return self.ax.get_aspect() in (1.0, 'equal')
+ return self.ax.get_aspect() in (1.0, "equal")
def setKeepDataAspectRatio(self, flag):
- self.ax.set_aspect(1.0 if flag else 'auto')
- self.ax2.set_aspect(1.0 if flag else 'auto')
+ self.ax.set_aspect(1.0 if flag else "auto")
+ self.ax2.set_aspect(1.0 if flag else "auto")
def setGraphGrid(self, which):
- self.ax.grid(False, which='both') # Disable all grid first
+ self.ax.grid(False, which="both") # Disable all grid first
if which is not None:
self.ax.grid(True, which=which)
@@ -1218,19 +1356,19 @@ class BackendMatplotlib(BackendBase.BackendBase):
def _getDevicePixelRatio(self) -> float:
"""Compatibility wrapper for devicePixelRatioF"""
- return 1.
+ return 1.0
- def _mplToQtPosition(self, x: float, y: float) -> Tuple[float, float]:
- """Convert matplotlib "display" space coord to Qt widget logical pixel
- """
+ def _mplToQtPosition(
+ self, x: Union[float, numpy.ndarray], y: Union[float, numpy.ndarray]
+ ) -> Tuple[Union[float, numpy.ndarray], Union[float, numpy.ndarray]]:
+ """Convert matplotlib "display" space coord to Qt widget logical pixel"""
ratio = self._getDevicePixelRatio()
# Convert from matplotlib origin (bottom) to Qt origin (top)
# and apply device pixel ratio
return x / ratio, (self.fig.get_window_extent().height - y) / ratio
def _qtToMplPosition(self, x: float, y: float) -> Tuple[float, float]:
- """Convert Qt widget logical pixel to matplotlib "display" space coord
- """
+ """Convert Qt widget logical pixel to matplotlib "display" space coord"""
ratio = self._getDevicePixelRatio()
# Apply device pixel ration and
# convert from Qt origin (top) to matplotlib origin (bottom)
@@ -1238,7 +1376,8 @@ class BackendMatplotlib(BackendBase.BackendBase):
def dataToPixel(self, x, y, axis):
ax = self.ax2 if axis == "right" else self.ax
- displayPos = ax.transData.transform_point((x, y)).transpose()
+ points = numpy.transpose((x, y))
+ displayPos = ax.transData.transform(points).transpose()
return self._mplToQtPosition(*displayPos)
def pixelToData(self, x, y, axis):
@@ -1250,18 +1389,33 @@ class BackendMatplotlib(BackendBase.BackendBase):
bbox = self.ax.get_window_extent()
# Warning this is not returning int...
ratio = self._getDevicePixelRatio()
- return tuple(int(value / ratio) for value in (
- bbox.xmin,
- self.fig.get_window_extent().height - bbox.ymax,
- bbox.width,
- bbox.height))
+ return tuple(
+ int(value / ratio)
+ for value in (
+ bbox.xmin,
+ self.fig.get_window_extent().height - bbox.ymax,
+ bbox.width,
+ bbox.height,
+ )
+ )
def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
- width, height = 1. - left - right, 1. - top - bottom
+ width, height = 1.0 - left - right, 1.0 - top - bottom
position = left, bottom, width, height
+ istight = config._MPL_TIGHT_LAYOUT and (left, top, right, bottom) != (
+ 0,
+ 0,
+ 0,
+ 0,
+ )
+ if self._matplotlibVersion >= Version("3.6"):
+ self.fig.set_layout_engine("tight" if istight else None)
+ else:
+ self.fig.set_tight_layout(True if istight else None)
+
# Toggle display of axes and viewbox rect
- isFrameOn = position != (0., 0., 1., 1.)
+ isFrameOn = position != (0.0, 0.0, 1.0, 1.0)
self.ax.set_frame_on(isFrameOn)
self.ax2.set_frame_on(isFrameOn)
@@ -1283,7 +1437,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
if self.ax.get_frame_on():
self.fig.patch.set_facecolor(backgroundColor)
- if self._matplotlibVersion < _parse_version('2'):
+ if self._matplotlibVersion < Version("2"):
self.ax.set_axis_bgcolor(dataBackgroundColor)
else:
self.ax.set_facecolor(dataBackgroundColor)
@@ -1301,12 +1455,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
for axes in (self.ax, self.ax2):
if axes.get_frame_on():
- axes.spines['bottom'].set_color(foregroundColor)
- axes.spines['top'].set_color(foregroundColor)
- axes.spines['right'].set_color(foregroundColor)
- axes.spines['left'].set_color(foregroundColor)
- axes.tick_params(axis='x', colors=foregroundColor)
- axes.tick_params(axis='y', colors=foregroundColor)
+ axes.spines["bottom"].set_color(foregroundColor)
+ axes.spines["top"].set_color(foregroundColor)
+ axes.spines["right"].set_color(foregroundColor)
+ axes.spines["left"].set_color(foregroundColor)
+ axes.tick_params(axis="x", colors=foregroundColor)
+ axes.tick_params(axis="y", colors=foregroundColor)
axes.yaxis.label.set_color(foregroundColor)
axes.xaxis.label.set_color(foregroundColor)
axes.title.set_color(foregroundColor)
@@ -1325,7 +1479,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
self._synchronizeForegroundColors()
-class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
+class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg):
"""QWidget matplotlib backend using a QtAgg canvas.
It adds fast overlay drawing and mouse event management.
@@ -1342,19 +1496,19 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self._limitsBeforeResize = None
FigureCanvasQTAgg.setSizePolicy(
- self, qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ self, qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding
+ )
FigureCanvasQTAgg.updateGeometry(self)
# Make postRedisplay asynchronous using Qt signal
- self._sigPostRedisplay.connect(
- self.__deferredReplot, qt.Qt.QueuedConnection)
+ self._sigPostRedisplay.connect(self.__deferredReplot, qt.Qt.QueuedConnection)
self._picked = None
- self.mpl_connect('button_press_event', self._onMousePress)
- self.mpl_connect('button_release_event', self._onMouseRelease)
- self.mpl_connect('motion_notify_event', self._onMouseMove)
- self.mpl_connect('scroll_event', self._onMouseWheel)
+ self.mpl_connect("button_press_event", self._onMousePress)
+ self.mpl_connect("button_release_event", self._onMouseRelease)
+ self.mpl_connect("motion_notify_event", self._onMouseMove)
+ self.mpl_connect("scroll_event", self._onMouseWheel)
def postRedisplay(self):
self._sigPostRedisplay.emit()
@@ -1362,23 +1516,21 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def __deferredReplot(self):
# Since this is deferred, makes sure it is still needed
plot = self._plotRef()
- if (plot is not None and
- plot._getDirtyPlot() and
- plot.getBackend() is self):
+ if plot is not None and plot._getDirtyPlot() and plot.getBackend() is self:
self.replot()
def _getDevicePixelRatio(self) -> float:
"""Compatibility wrapper for devicePixelRatioF"""
- if hasattr(self, 'devicePixelRatioF'):
+ if hasattr(self, "devicePixelRatioF"):
ratio = self.devicePixelRatioF()
else: # Qt < 5.6 compatibility
ratio = float(self.devicePixelRatio())
# Safety net: avoid returning 0
- return ratio if ratio != 0. else 1.
+ return ratio if ratio != 0.0 else 1.0
# Mouse event forwarding
- _MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'}
+ _MPL_TO_PLOT_BUTTONS = {1: "left", 2: "middle", 3: "right"}
def _onMousePress(self, event):
button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
@@ -1389,8 +1541,7 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def _onMouseMove(self, event):
x, y = self._mplToQtPosition(event.x, event.y)
if self._graphCursor:
- position = self._plot.pixelToData(
- x, y, axis='left', check=True)
+ position = self._plot.pixelToData(x, y, axis="left", check=True)
lineh, linev = self._graphCursor
if position is not None:
linev.set_visible(True)
@@ -1399,9 +1550,9 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
lineh.set_ydata((position[1], position[1]))
self._plot._setDirtyPlot(overlayOnly=True)
elif lineh.get_visible():
- lineh.set_visible(False)
- linev.set_visible(False)
- self._plot._setDirtyPlot(overlayOnly=True)
+ lineh.set_visible(False)
+ linev.set_visible(False)
+ self._plot._setDirtyPlot(overlayOnly=True)
# onMouseMove must trigger replot if dirty flag is raised
self._plot.onMouseMove(int(x), int(y))
@@ -1430,11 +1581,13 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
def pickItem(self, x, y, item):
xDisplay, yDisplay = self._qtToMplPosition(x, y)
mouseEvent = MouseEvent(
- 'button_press_event', self, int(xDisplay), int(yDisplay))
+ "button_press_event", self, int(xDisplay), int(yDisplay)
+ )
# Override axes and data position with the axes
mouseEvent.inaxes = item.axes
mouseEvent.xdata, mouseEvent.ydata = self.pixelToData(
- x, y, axis='left' if item.axes is self.ax else 'right')
+ x, y, axis="left" if item.axes is self.ax else "right"
+ )
picked, info = item.contains(mouseEvent)
if not picked:
@@ -1443,26 +1596,30 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
elif isinstance(item, TriMesh):
# Convert selected triangle to data point indices
triangulation = item._triangulation
- indices = triangulation.get_masked_triangles()[info['ind'][0]]
+ indices = triangulation.get_masked_triangles()[info["ind"][0]]
# Sort picked triangle points by distance to mouse
# from furthest to closest to put closest point last
# This is to be somewhat consistent with last scatter point
# being the top one.
- xdata, ydata = self.pixelToData(x, y, axis='left')
- dists = ((triangulation.x[indices] - xdata) ** 2 +
- (triangulation.y[indices] - ydata) ** 2)
+ xdata, ydata = self.pixelToData(x, y, axis="left")
+ dists = (triangulation.x[indices] - xdata) ** 2 + (
+ triangulation.y[indices] - ydata
+ ) ** 2
return indices[numpy.flip(numpy.argsort(dists), axis=0)]
else: # Returns indices if any
- return info.get('ind', ())
+ return info.get("ind", ())
# replot control
def resizeEvent(self, event):
# Store current limits
self._limitsBeforeResize = (
- self.ax.get_xbound(), self.ax.get_ybound(), self.ax2.get_ybound())
+ self.ax.get_xbound(),
+ self.ax.get_ybound(),
+ self.ax2.get_ybound(),
+ )
FigureCanvasQTAgg.resizeEvent(self, event)
if self.isKeepDataAspectRatio() or self._hasOverlays():
@@ -1477,17 +1634,25 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
This is directly called by matplotlib for widget resize.
"""
+ if self.size().isEmpty():
+ return # Skip rendering of 0-sized canvas
+
self.updateZOrder()
+ if not qt_inspect.isValid(self):
+ _logger.info("draw requested but widget no longer exists")
+ return
+
# Starting with mpl 2.1.0, toggling autoscale raises a ValueError
# in some situations. See #1081, #1136, #1163,
- if self._matplotlibVersion >= _parse_version("2.0.0"):
+ if self._matplotlibVersion >= Version("2.0.0"):
try:
FigureCanvasQTAgg.draw(self)
except ValueError as err:
_logger.debug(
- "ValueError caught while calling FigureCanvasQTAgg.draw: "
- "'%s'", err)
+ "ValueError caught while calling FigureCanvasQTAgg.draw: " "'%s'",
+ err,
+ )
else:
FigureCanvasQTAgg.draw(self)
@@ -1502,26 +1667,29 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
xLimits, yLimits, yRightLimits = self._limitsBeforeResize
self._limitsBeforeResize = None
- if (xLimits != self.ax.get_xbound() or
- yLimits != self.ax.get_ybound()):
+ if xLimits != self.ax.get_xbound() or yLimits != self.ax.get_ybound():
self._updateMarkers()
if xLimits != self.ax.get_xbound():
self._plot.getXAxis()._emitLimitsChanged()
if yLimits != self.ax.get_ybound():
- self._plot.getYAxis(axis='left')._emitLimitsChanged()
+ self._plot.getYAxis(axis="left")._emitLimitsChanged()
if yRightLimits != self.ax2.get_ybound():
- self._plot.getYAxis(axis='right')._emitLimitsChanged()
+ self._plot.getYAxis(axis="right")._emitLimitsChanged()
self._drawOverlays()
def replot(self):
+ if not qt_inspect.isValid(self):
+ _logger.info("replot requested but widget no longer exists")
+ return
+
with self._plot._paintContext():
BackendMatplotlib._replot(self)
dirtyFlag = self._plot._getDirtyPlot()
- if dirtyFlag == 'overlay':
+ if dirtyFlag == "overlay":
# Only redraw overlays using fast rendering path
if self._background is None:
self._background = self.copy_from_bbox(self.fig.bbox)
@@ -1533,8 +1701,9 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
self.draw()
# Workaround issue of rendering overlays with some matplotlib versions
- if (_parse_version('1.5') <= self._matplotlibVersion < _parse_version('2.1') and
- not hasattr(self, '_firstReplot')):
+ if Version("1.5") <= self._matplotlibVersion < Version(
+ "2.1"
+ ) and not hasattr(self, "_firstReplot"):
self._firstReplot = False
if self._hasOverlays():
qt.QTimer.singleShot(0, self.draw) # Request async draw
diff --git a/src/silx/gui/plot/backends/BackendOpenGL.py b/src/silx/gui/plot/backends/BackendOpenGL.py
index f1a12af..370f14b 100755
--- a/src/silx/gui/plot/backends/BackendOpenGL.py
+++ b/src/silx/gui/plot/backends/BackendOpenGL.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,7 +23,7 @@
# ############################################################################*/
"""OpenGL Plot backend."""
-from __future__ import division
+from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
@@ -45,6 +44,7 @@ from ..._glutils import gl
from ... import _glutils as glu
from . import glutils
from .glutils.PlotImageFile import saveImageToFile
+from silx.gui.colors import RGBAColorType
_logger = logging.getLogger(__name__)
@@ -55,64 +55,95 @@ _logger = logging.getLogger(__name__)
# Content #####################################################################
+
class _ShapeItem(dict):
- def __init__(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
+ def __init__(
+ self,
+ x,
+ y,
+ shape,
+ color,
+ fill,
+ overlay,
+ linewidth,
+ dashoffset,
+ dashpattern,
+ gapcolor,
+ ):
super(_ShapeItem, self).__init__()
- if shape not in ('polygon', 'rectangle', 'line',
- 'vline', 'hline', 'polylines'):
+ if shape not in ("polygon", "rectangle", "line", "vline", "hline", "polylines"):
raise NotImplementedError("Unsupported shape {0}".format(shape))
x = numpy.array(x, copy=False)
y = numpy.array(y, copy=False)
- if shape == 'rectangle':
+ if shape == "rectangle":
xMin, xMax = x
x = numpy.array((xMin, xMin, xMax, xMax))
yMin, yMax = y
y = numpy.array((yMin, yMax, yMax, yMin))
# Ignore fill for polylines to mimic matplotlib
- fill = fill if shape != 'polylines' else False
-
- self.update({
- 'shape': shape,
- 'color': colors.rgba(color),
- 'fill': 'hatch' if fill else None,
- 'x': x,
- 'y': y,
- 'linestyle': linestyle,
- 'linewidth': linewidth,
- 'linebgcolor': linebgcolor,
- })
+ fill = fill if shape != "polylines" else False
+
+ self.update(
+ {
+ "shape": shape,
+ "color": colors.rgba(color),
+ "fill": "hatch" if fill else None,
+ "x": x,
+ "y": y,
+ "linewidth": linewidth,
+ "dashoffset": dashoffset,
+ "dashpattern": dashpattern,
+ "gapcolor": gapcolor,
+ }
+ )
class _MarkerItem(dict):
- def __init__(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
+ def __init__(
+ self,
+ x,
+ y,
+ text,
+ color,
+ symbol,
+ linewidth,
+ dashoffset,
+ dashpattern,
+ constraint,
+ yaxis,
+ font,
+ bgcolor,
+ ):
super(_MarkerItem, self).__init__()
if symbol is None:
- symbol = '+'
+ symbol = "+"
# Apply constraint to provided position
- isConstraint = (constraint is not None and
- x is not None and y is not None)
+ isConstraint = constraint is not None and x is not None and y is not None
if isConstraint:
x, y = constraint(x, y)
- self.update({
- 'x': x,
- 'y': y,
- 'text': text,
- 'color': colors.rgba(color),
- 'constraint': constraint if isConstraint else None,
- 'symbol': symbol,
- 'linestyle': linestyle,
- 'linewidth': linewidth,
- 'yaxis': yaxis,
- })
+ self.update(
+ {
+ "x": x,
+ "y": y,
+ "text": text,
+ "color": colors.rgba(color),
+ "constraint": constraint if isConstraint else None,
+ "symbol": symbol,
+ "linewidth": linewidth,
+ "dashoffset": dashoffset,
+ "dashpattern": dashpattern,
+ "yaxis": yaxis,
+ "font": font,
+ "bgcolor": bgcolor,
+ }
+ )
# shaders #####################################################################
@@ -196,24 +227,30 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
So, the caller should not modify these arrays afterwards.
"""
- def __init__(self, plot, parent=None, f=qt.Qt.WindowFlags()):
- glu.OpenGLWidget.__init__(self, parent,
- alphaBufferSize=8,
- depthBufferSize=0,
- stencilBufferSize=0,
- version=(2, 1),
- f=f)
+ _TEXT_MARKER_PADDING = 4
+
+ def __init__(self, plot, parent=None, f=qt.Qt.Widget):
+ glu.OpenGLWidget.__init__(
+ self,
+ parent,
+ alphaBufferSize=8,
+ depthBufferSize=0,
+ stencilBufferSize=0,
+ version=(2, 1),
+ f=f,
+ )
BackendBase.BackendBase.__init__(self, plot, parent)
- self._backgroundColor = 1., 1., 1., 1.
- self._dataBackgroundColor = 1., 1., 1., 1.
+ self._defaultFont: qt.QFont = None
+ self.__isOpenGLValid = False
+
+ self._backgroundColor = 1.0, 1.0, 1.0, 1.0
+ self._dataBackgroundColor = 1.0, 1.0, 1.0, 1.0
self.matScreenProj = glutils.mat4Identity()
- self._progBase = glu.Program(
- _baseVertShd, _baseFragShd, attrib0='position')
- self._progTex = glu.Program(
- _texVertShd, _texFragShd, attrib0='position')
+ self._progBase = glu.Program(_baseVertShd, _baseFragShd, attrib0="position")
+ self._progTex = glu.Program(_texVertShd, _texFragShd, attrib0="position")
self._plotFBOs = weakref.WeakKeyDictionary()
self._keepDataAspectRatio = False
@@ -224,19 +261,26 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._glGarbageCollector = []
self._plotFrame = glutils.GLPlotFrame2D(
- foregroundColor=(0., 0., 0., 1.),
- gridColor=(.7, .7, .7, 1.),
- marginRatios=(.15, .1, .1, .15))
+ foregroundColor=(0.0, 0.0, 0.0, 1.0),
+ gridColor=(0.7, 0.7, 0.7, 1.0),
+ marginRatios=(0.15, 0.1, 0.1, 0.15),
+ font=self.getDefaultFont(),
+ )
self._plotFrame.size = ( # Init size with size int
int(self.getDevicePixelRatio() * 640),
- int(self.getDevicePixelRatio() * 480))
+ int(self.getDevicePixelRatio() * 480),
+ )
self.setAutoFillBackground(False)
self.setMouseTracking(True)
# QWidget
- _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'}
+ _MOUSE_BTNS = {
+ qt.Qt.LeftButton: "left",
+ qt.Qt.RightButton: "right",
+ qt.Qt.MiddleButton: "middle",
+ }
def sizeHint(self):
return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend
@@ -244,12 +288,12 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def mousePressEvent(self, event):
if event.button() not in self._MOUSE_BTNS:
return super(BackendOpenGL, self).mousePressEvent(event)
- self._plot.onMousePress(
- event.x(), event.y(), self._MOUSE_BTNS[event.button()])
+ x, y = qt.getMouseEventPosition(event)
+ self._plot.onMousePress(x, y, self._MOUSE_BTNS[event.button()])
event.accept()
def mouseMoveEvent(self, event):
- qtPos = event.x(), event.y()
+ qtPos = qt.getMouseEventPosition(event)
previousMousePosInPixels = self._mousePosInPixels
if qtPos == self._mouseInPlotArea(*qtPos):
@@ -259,8 +303,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
else:
self._mousePosInPixels = None # Mouse outside plot area
- if (self._crosshairCursor is not None and
- previousMousePosInPixels != self._mousePosInPixels):
+ if (
+ self._crosshairCursor is not None
+ and previousMousePosInPixels != self._mousePosInPixels
+ ):
# Avoid replot when cursor remains outside plot area
self._plot._setDirtyPlot(overlayOnly=True)
@@ -270,17 +316,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def mouseReleaseEvent(self, event):
if event.button() not in self._MOUSE_BTNS:
return super(BackendOpenGL, self).mouseReleaseEvent(event)
- self._plot.onMouseRelease(
- event.x(), event.y(), self._MOUSE_BTNS[event.button()])
+ x, y = qt.getMouseEventPosition(event)
+ self._plot.onMouseRelease(x, y, self._MOUSE_BTNS[event.button()])
event.accept()
def wheelEvent(self, event):
delta = event.angleDelta().y()
- angleInDegrees = delta / 8.
- if qt.BINDING == "PySide6":
- x, y = event.position().x(), event.position().y()
- else:
- x, y = event.x(), event.y()
+ angleInDegrees = delta / 8.0
+ x, y = qt.getMouseEventPosition(event)
self._plot.onMouseWheel(x, y, angleInDegrees)
event.accept()
@@ -290,16 +333,17 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# OpenGLWidget API
def initializeGL(self):
- gl.testGL()
+ self.__isOpenGLValid = gl.testGL()
+ if not self.__isOpenGLValid:
+ return
gl.glClearStencil(0)
gl.glEnable(gl.GL_BLEND)
# gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
- gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA,
- gl.GL_ONE_MINUS_SRC_ALPHA,
- gl.GL_ONE,
- gl.GL_ONE)
+ gl.glBlendFuncSeparate(
+ gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA, gl.GL_ONE, gl.GL_ONE
+ )
# For lines
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
@@ -317,28 +361,33 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def _paintFBOGL(self):
context = glu.Context.getCurrent()
plotFBOTex = self._plotFBOs.get(context)
- if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or
- plotFBOTex is None):
+ if self._plot._getDirtyPlot() or self._plotFrame.isDirty or plotFBOTex is None:
self._plotVertices = (
# Vertex coordinates
- numpy.array(((-1., -1.), (1., -1.), (-1., 1.), (1., 1.)),
- dtype=numpy.float32),
- # Texture coordinates
- numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32))
- if plotFBOTex is None or \
- plotFBOTex.shape[1] != self._plotFrame.size[0] or \
- plotFBOTex.shape[0] != self._plotFrame.size[1]:
+ numpy.array(
+ ((-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)),
+ dtype=numpy.float32,
+ ),
+ # Texture coordinates
+ numpy.array(
+ ((0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)),
+ dtype=numpy.float32,
+ ),
+ )
+ if (
+ plotFBOTex is None
+ or plotFBOTex.shape[1] != self._plotFrame.size[0]
+ or plotFBOTex.shape[0] != self._plotFrame.size[1]
+ ):
if plotFBOTex is not None:
plotFBOTex.discard()
plotFBOTex = glu.FramebufferTexture(
gl.GL_RGBA,
- shape=(self._plotFrame.size[1],
- self._plotFrame.size[0]),
+ shape=(self._plotFrame.size[1], self._plotFrame.size[0]),
minFilter=gl.GL_NEAREST,
magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
+ wrap=(gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE),
+ )
self._plotFBOs[context] = plotFBOTex
with plotFBOTex:
@@ -353,25 +402,33 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._progTex.use()
texUnit = 0
- gl.glUniform1i(self._progTex.uniforms['tex'], texUnit)
- gl.glUniformMatrix4fv(self._progTex.uniforms['matrix'], 1, gl.GL_TRUE,
- glutils.mat4Identity().astype(numpy.float32))
-
- gl.glEnableVertexAttribArray(self._progTex.attributes['position'])
- gl.glVertexAttribPointer(self._progTex.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._plotVertices[0])
-
- gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords'])
- gl.glVertexAttribPointer(self._progTex.attributes['texCoords'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._plotVertices[1])
+ gl.glUniform1i(self._progTex.uniforms["tex"], texUnit)
+ gl.glUniformMatrix4fv(
+ self._progTex.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ glutils.mat4Identity().astype(numpy.float32),
+ )
+
+ gl.glEnableVertexAttribArray(self._progTex.attributes["position"])
+ gl.glVertexAttribPointer(
+ self._progTex.attributes["position"],
+ 2,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ self._plotVertices[0],
+ )
+
+ gl.glEnableVertexAttribArray(self._progTex.attributes["texCoords"])
+ gl.glVertexAttribPointer(
+ self._progTex.attributes["texCoords"],
+ 2,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ self._plotVertices[1],
+ )
with plotFBOTex.texture:
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0]))
@@ -379,6 +436,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._renderOverlayGL()
def paintGL(self):
+ if not self.__isOpenGLValid:
+ return
+
plot = self._plotRef()
if plot is None:
return
@@ -399,6 +459,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Sync plot frame with window
self._plotFrame.devicePixelRatio = self.getDevicePixelRatio()
+ self._plotFrame.dotsPerInch = self.getDotsPerInch()
# self._paintDirectGL()
self._paintFBOGL()
@@ -422,21 +483,29 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
pixelOffset = 3
context = glutils.RenderContext(
- isXLog=isXLog, isYLog=isYLog, dpi=self.getDotsPerInch())
+ isXLog=isXLog,
+ isYLog=isYLog,
+ dpi=self.getDotsPerInch(),
+ plotFrame=self._plotFrame,
+ )
for plotItem in self.getItemsFromBackToFront(
- condition=lambda i: i.isVisible() and i.isOverlay() == overlay):
+ condition=lambda i: i.isVisible() and i.isOverlay() == overlay
+ ):
if plotItem._backendRenderer is None:
continue
item = plotItem._backendRenderer
if isinstance(item, glutils.GLPlotItem): # Render data items
- gl.glViewport(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
+ gl.glViewport(
+ self._plotFrame.margins.left,
+ self._plotFrame.margins.bottom,
+ plotWidth,
+ plotHeight,
+ )
# Set matrix
- if item.yaxis == 'right':
+ if item.yaxis == "right":
context.matrix = self._plotFrame.transformedDataY2ProjMat
else:
context.matrix = self._plotFrame.transformedDataProjMat
@@ -445,140 +514,187 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
elif isinstance(item, _ShapeItem): # Render shape items
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- if ((isXLog and numpy.min(item['x']) < FLOAT32_MINPOS) or
- (isYLog and numpy.min(item['y']) < FLOAT32_MINPOS)):
+ if (isXLog and numpy.min(item["x"]) < FLOAT32_MINPOS) or (
+ isYLog and numpy.min(item["y"]) < FLOAT32_MINPOS
+ ):
# Ignore items <= 0. on log axes
continue
- if item['shape'] == 'hline':
+ if item["shape"] == "hline":
width = self._plotFrame.size[0]
_, yPixel = self._plotFrame.dataToPixel(
- 0.5 * sum(self._plotFrame.dataRanges[0]),
- item['y'],
- axis='left')
- subShapes = [numpy.array(((0., yPixel), (width, yPixel)),
- dtype=numpy.float32)]
-
- elif item['shape'] == 'vline':
+ 0.5 * sum(self._plotFrame.dataRanges[0]), item["y"], axis="left"
+ )
+ subShapes = [
+ numpy.array(
+ ((0.0, yPixel), (width, yPixel)), dtype=numpy.float32
+ )
+ ]
+
+ elif item["shape"] == "vline":
xPixel, _ = self._plotFrame.dataToPixel(
- item['x'],
- 0.5 * sum(self._plotFrame.dataRanges[1]),
- axis='left')
+ item["x"], 0.5 * sum(self._plotFrame.dataRanges[1]), axis="left"
+ )
height = self._plotFrame.size[1]
- subShapes = [numpy.array(((xPixel, 0), (xPixel, height)),
- dtype=numpy.float32)]
+ subShapes = [
+ numpy.array(
+ ((xPixel, 0), (xPixel, height)), dtype=numpy.float32
+ )
+ ]
else:
# Split sub-shapes at not finite values
- splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
- numpy.isfinite(item['x']), numpy.isfinite(item['y']))))[0]
- splits = numpy.concatenate(([-1], splits, [len(item['x'])]))
+ splits = numpy.nonzero(
+ numpy.logical_not(
+ numpy.logical_and(
+ numpy.isfinite(item["x"]), numpy.isfinite(item["y"])
+ )
+ )
+ )[0]
+ splits = numpy.concatenate(([-1], splits, [len(item["x"])]))
subShapes = []
for begin, end in zip(splits[:-1] + 1, splits[1:]):
if end > begin:
- subShapes.append(numpy.array([
- self._plotFrame.dataToPixel(x, y, axis='left')
- for (x, y) in zip(item['x'][begin:end], item['y'][begin:end])]))
+ subShapes.append(
+ numpy.array(
+ [
+ self._plotFrame.dataToPixel(x, y, axis="left")
+ for (x, y) in zip(
+ item["x"][begin:end], item["y"][begin:end]
+ )
+ ]
+ )
+ )
for points in subShapes: # Draw each sub-shape
# Draw the fill
- if (item['fill'] is not None and
- item['shape'] not in ('hline', 'vline')):
+ if item["fill"] is not None and item["shape"] not in (
+ "hline",
+ "vline",
+ ):
self._progBase.use()
gl.glUniformMatrix4fv(
- self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
- self.matScreenProj.astype(numpy.float32))
- gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
+ self._progBase.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ self.matScreenProj.astype(numpy.float32),
+ )
+ gl.glUniform2i(self._progBase.uniforms["isLog"], False, False)
+ gl.glUniform1f(self._progBase.uniforms["tickLen"], 0.0)
shape2D = glutils.FilledShape2D(
- points, style=item['fill'], color=item['color'])
+ points, style=item["fill"], color=item["color"]
+ )
shape2D.render(
- posAttrib=self._progBase.attributes['position'],
- colorUnif=self._progBase.uniforms['color'],
- hatchStepUnif=self._progBase.uniforms['hatchStep'])
+ posAttrib=self._progBase.attributes["position"],
+ colorUnif=self._progBase.uniforms["color"],
+ hatchStepUnif=self._progBase.uniforms["hatchStep"],
+ )
# Draw the stroke
- if item['linestyle'] not in ('', ' ', None):
- if item['shape'] != 'polylines':
+ if item["dashpattern"] is not None:
+ if item["shape"] != "polylines":
# close the polyline
- points = numpy.append(points,
- numpy.atleast_2d(points[0]), axis=0)
+ points = numpy.append(
+ points, numpy.atleast_2d(points[0]), axis=0
+ )
lines = glutils.GLLines2D(
- points[:, 0], points[:, 1],
- style=item['linestyle'],
- color=item['color'],
- dash2ndColor=item['linebgcolor'],
- width=item['linewidth'])
+ points[:, 0],
+ points[:, 1],
+ color=item["color"],
+ gapColor=item["gapcolor"],
+ width=item["linewidth"],
+ dashOffset=item["dashoffset"],
+ dashPattern=item["dashpattern"],
+ )
context.matrix = self.matScreenProj
lines.render(context)
elif isinstance(item, _MarkerItem):
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- xCoord, yCoord, yAxis = item['x'], item['y'], item['yaxis']
+ xCoord, yCoord, yAxis = item["x"], item["y"], item["yaxis"]
- if ((isXLog and xCoord is not None and xCoord <= 0) or
- (isYLog and yCoord is not None and yCoord <= 0)):
+ if (isXLog and xCoord is not None and xCoord <= 0) or (
+ isYLog and yCoord is not None and yCoord <= 0
+ ):
# Do not render markers with negative coords on log axis
continue
- color = item['color']
- intensity = color[0] * 0.299 + color[1] * 0.587 + color[2] * 0.114
- bgColor = (1., 1., 1., 0.5) if intensity <= 0.5 else (0., 0., 0., 0.5)
+ color = item["color"]
+ bgColor = item["bgcolor"]
if xCoord is None or yCoord is None:
if xCoord is None: # Horizontal line in data space
pixelPos = self._plotFrame.dataToPixel(
- 0.5 * sum(self._plotFrame.dataRanges[0]),
- yCoord,
- axis=yAxis)
-
- if item['text'] is not None:
- x = self._plotFrame.size[0] - \
- self._plotFrame.margins.right - pixelOffset
+ 0.5 * sum(self._plotFrame.dataRanges[0]), yCoord, axis=yAxis
+ )
+
+ if item["text"] is not None:
+ x = (
+ self._plotFrame.size[0]
+ - self._plotFrame.margins.right
+ - pixelOffset
+ )
y = pixelPos[1] - pixelOffset
label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
+ item["text"],
+ item["font"],
+ x,
+ y,
+ color=color,
bgColor=bgColor,
align=glutils.RIGHT,
valign=glutils.BOTTOM,
- devicePixelRatio=self.getDevicePixelRatio())
+ devicePixelRatio=self.getDevicePixelRatio(),
+ padding=self._TEXT_MARKER_PADDING,
+ )
labels.append(label)
width = self._plotFrame.size[0]
lines = glutils.GLLines2D(
- (0, width), (pixelPos[1], pixelPos[1]),
- style=item['linestyle'],
- color=item['color'],
- width=item['linewidth'])
+ (0, width),
+ (pixelPos[1], pixelPos[1]),
+ color=color,
+ width=item["linewidth"],
+ dashOffset=item["dashoffset"],
+ dashPattern=item["dashpattern"],
+ )
context.matrix = self.matScreenProj
lines.render(context)
else: # yCoord is None: vertical line in data space
- yRange = self._plotFrame.dataRanges[1 if yAxis == 'left' else 2]
+ yRange = self._plotFrame.dataRanges[1 if yAxis == "left" else 2]
pixelPos = self._plotFrame.dataToPixel(
- xCoord, 0.5 * sum(yRange), axis=yAxis)
+ xCoord, 0.5 * sum(yRange), axis=yAxis
+ )
- if item['text'] is not None:
+ if item["text"] is not None:
x = pixelPos[0] + pixelOffset
y = self._plotFrame.margins.top + pixelOffset
label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
+ item["text"],
+ item["font"],
+ x,
+ y,
+ color=color,
bgColor=bgColor,
align=glutils.LEFT,
valign=glutils.TOP,
- devicePixelRatio=self.getDevicePixelRatio())
+ devicePixelRatio=self.getDevicePixelRatio(),
+ padding=self._TEXT_MARKER_PADDING,
+ )
labels.append(label)
height = self._plotFrame.size[1]
lines = glutils.GLLines2D(
- (pixelPos[0], pixelPos[0]), (0, height),
- style=item['linestyle'],
- color=item['color'],
- width=item['linewidth'])
+ (pixelPos[0], pixelPos[0]),
+ (0, height),
+ color=color,
+ width=item["linewidth"],
+ dashOffset=item["dashoffset"],
+ dashPattern=item["dashpattern"],
+ )
context.matrix = self.matScreenProj
lines.render(context)
@@ -588,8 +704,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if not xmin < xCoord < xmax or not ymin < yCoord < ymax:
# Do not render markers outside visible plot area
continue
- pixelPos = self._plotFrame.dataToPixel(
- xCoord, yCoord, axis=yAxis)
+ pixelPos = self._plotFrame.dataToPixel(xCoord, yCoord, axis=yAxis)
if isYInverted:
valign = glutils.BOTTOM
@@ -598,52 +713,55 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
valign = glutils.TOP
vPixelOffset = pixelOffset
- if item['text'] is not None:
+ if item["text"] is not None:
x = pixelPos[0] + pixelOffset
y = pixelPos[1] + vPixelOffset
label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
+ item["text"],
+ item["font"],
+ x,
+ y,
+ color=color,
bgColor=bgColor,
align=glutils.LEFT,
valign=valign,
- devicePixelRatio=self.getDevicePixelRatio())
+ devicePixelRatio=self.getDevicePixelRatio(),
+ padding=self._TEXT_MARKER_PADDING,
+ )
labels.append(label)
# For now simple implementation: using a curve for each marker
# Should pack all markers to a single set of points
- markerCurve = glutils.GLPlotCurve2D(
- numpy.array((pixelPos[0],), dtype=numpy.float64),
- numpy.array((pixelPos[1],), dtype=numpy.float64),
- marker=item['symbol'],
- markerColor=item['color'],
- markerSize=11)
-
- context = glutils.RenderContext(
- matrix=self.matScreenProj,
- isXLog=False,
- isYLog=False,
- dpi=self.getDotsPerInch())
- markerCurve.render(context)
- markerCurve.discard()
+ marker = glutils.Points2D(
+ (pixelPos[0],),
+ (pixelPos[1],),
+ marker=item["symbol"],
+ color=color,
+ size=11,
+ )
+ context.matrix = self.matScreenProj
+ marker.render(context)
else:
- _logger.error('Unsupported item: %s', str(item))
+ _logger.error("Unsupported item: %s", str(item))
continue
# Render marker labels
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
for label in labels:
- label.render(self.matScreenProj)
+ label.render(self.matScreenProj, self._plotFrame.dotsPerInch)
def _renderOverlayGL(self):
"""Render overlay layer: overlay items and crosshair."""
plotWidth, plotHeight = self._plotFrame.plotSize
# Scissor to plot area
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
+ gl.glScissor(
+ self._plotFrame.margins.left,
+ self._plotFrame.margins.bottom,
+ plotWidth,
+ plotHeight,
+ )
gl.glEnable(gl.GL_SCISSOR_TEST)
self._renderItems(overlay=True)
@@ -651,17 +769,18 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Render crosshair cursor
if self._crosshairCursor is not None and self._mousePosInPixels is not None:
self._progBase.use()
- gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
- posAttrib = self._progBase.attributes['position']
- matrixUnif = self._progBase.uniforms['matrix']
- colorUnif = self._progBase.uniforms['color']
- hatchStepUnif = self._progBase.uniforms['hatchStep']
+ gl.glUniform2i(self._progBase.uniforms["isLog"], False, False)
+ gl.glUniform1f(self._progBase.uniforms["tickLen"], 0.0)
+ posAttrib = self._progBase.attributes["position"]
+ matrixUnif = self._progBase.uniforms["matrix"]
+ colorUnif = self._progBase.uniforms["color"]
+ hatchStepUnif = self._progBase.uniforms["hatchStep"]
gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- gl.glUniformMatrix4fv(matrixUnif, 1, gl.GL_TRUE,
- self.matScreenProj.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ matrixUnif, 1, gl.GL_TRUE, self.matScreenProj.astype(numpy.float32)
+ )
color, lineWidth = self._crosshairCursor
gl.glUniform4f(colorUnif, *color)
@@ -669,18 +788,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
xPixel, yPixel = self._mousePosInPixels
xPixel, yPixel = xPixel + 0.5, yPixel + 0.5
- vertices = numpy.array(((0., yPixel),
- (self._plotFrame.size[0], yPixel),
- (xPixel, 0.),
- (xPixel, self._plotFrame.size[1])),
- dtype=numpy.float32)
+ vertices = numpy.array(
+ (
+ (0.0, yPixel),
+ (self._plotFrame.size[0], yPixel),
+ (xPixel, 0.0),
+ (xPixel, self._plotFrame.size[1]),
+ ),
+ dtype=numpy.float32,
+ )
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, vertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, vertices
+ )
gl.glLineWidth(lineWidth)
gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))
@@ -693,9 +814,12 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
"""
plotWidth, plotHeight = self._plotFrame.plotSize
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
+ gl.glScissor(
+ self._plotFrame.margins.left,
+ self._plotFrame.margins.bottom,
+ plotWidth,
+ plotHeight,
+ )
gl.glEnable(gl.GL_SCISSOR_TEST)
if self._dataBackgroundColor != self._backgroundColor:
@@ -718,29 +842,28 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._plotFrame.size = (
int(self.getDevicePixelRatio() * width),
- int(self.getDevicePixelRatio() * height))
+ int(self.getDevicePixelRatio() * height),
+ )
self.matScreenProj = glutils.mat4Ortho(
- 0, self._plotFrame.size[0],
- self._plotFrame.size[1], 0,
- 1, -1)
+ 0, self._plotFrame.size[0], self._plotFrame.size[1], 0, 1, -1
+ )
# Store current ranges
previousXRange = self.getGraphXLimits()
- previousYRange = self.getGraphYLimits(axis='left')
- previousYRightRange = self.getGraphYLimits(axis='right')
+ previousYRange = self.getGraphYLimits(axis="left")
+ previousYRightRange = self.getGraphYLimits(axis="right")
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \
- self._plotFrame.dataRanges
+ (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self._plotFrame.dataRanges
self.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
# If plot range has changed, then emit signal
if previousXRange != self.getGraphXLimits():
self._plot.getXAxis()._emitLimitsChanged()
- if previousYRange != self.getGraphYLimits(axis='left'):
- self._plot.getYAxis(axis='left')._emitLimitsChanged()
- if previousYRightRange != self.getGraphYLimits(axis='right'):
- self._plot.getYAxis(axis='right')._emitLimitsChanged()
+ if previousYRange != self.getGraphYLimits(axis="left"):
+ self._plot.getYAxis(axis="left")._emitLimitsChanged()
+ if previousYRightRange != self.getGraphYLimits(axis="right"):
+ self._plot.getYAxis(axis="right")._emitLimitsChanged()
# Add methods
@@ -757,39 +880,92 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
elif numpy.issubdtype(v.dtype, numpy.integer):
return numpy.float32 if v.itemsize <= 2 else numpy.float64
else:
- raise ValueError('Unsupported data type')
-
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
- for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, fill, symbolsize):
+ raise ValueError("Unsupported data type")
+
+ _DASH_PATTERNS = {
+ "": (0.0, None),
+ " ": (0.0, None),
+ "-": (0.0, ()),
+ "--": (0.0, (3.7, 1.6, 3.7, 1.6)),
+ "-.": (0.0, (6.4, 1.6, 1, 1.6)),
+ ":": (0.0, (1, 1.65, 1, 1.65)),
+ None: (0.0, None),
+ }
+ """Convert from linestyle to (offset, (dash pattern))
+
+ Note: dash pattern internal convention differs from matplotlib:
+ - None: no line at all
+ - (): "solid" line
+ """
+
+ def _lineStyleToDashOffsetPattern(
+ self, style
+ ) -> tuple[float, tuple[float, float, float, float] | tuple[()] | None]:
+ """Convert a linestyle to its corresponding offset and dash pattern"""
+ if style is None or isinstance(style, str):
+ return self._DASH_PATTERNS[style]
+
+ # (offset, (dash pattern)) case
+ offset, pattern = style
+ if pattern is None:
+ # Convert from matplotlib to internal representation of solid
+ pattern = ()
+ if len(pattern) == 2:
+ pattern = pattern * 2
+ return float(offset), tuple(float(v) for v in pattern)
+
+ def addCurve(
+ self,
+ x,
+ y,
+ color,
+ gapcolor,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ xerror,
+ yerror,
+ fill,
+ alpha,
+ symbolsize,
+ baseline,
+ ):
+ for parameter in (
+ x,
+ y,
+ color,
+ symbol,
+ linewidth,
+ linestyle,
+ yaxis,
+ fill,
+ symbolsize,
+ ):
assert parameter is not None
- assert yaxis in ('left', 'right')
+ assert yaxis in ("left", "right")
# Convert input data
x = numpy.array(x, copy=False)
y = numpy.array(y, copy=False)
# Check if float32 is enough
- if (self._castArrayTo(x) is numpy.float32 and
- self._castArrayTo(y) is numpy.float32):
+ if (
+ self._castArrayTo(x) is numpy.float32
+ and self._castArrayTo(y) is numpy.float32
+ ):
dtype = numpy.float32
else:
dtype = numpy.float64
- x = numpy.array(x, dtype=dtype, copy=False, order='C')
- y = numpy.array(y, dtype=dtype, copy=False, order='C')
+ x = numpy.array(x, dtype=dtype, copy=False, order="C")
+ y = numpy.array(y, dtype=dtype, copy=False, order="C")
# Convert errors to float32
if xerror is not None:
- xerror = numpy.array(
- xerror, dtype=numpy.float32, copy=False, order='C')
+ xerror = numpy.array(xerror, dtype=numpy.float32, copy=False, order="C")
if yerror is not None:
- yerror = numpy.array(
- yerror, dtype=numpy.float32, copy=False, order='C')
+ yerror = numpy.array(yerror, dtype=numpy.float32, copy=False, order="C")
# Handle axes log scale: convert data
@@ -799,21 +975,21 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if xerror is not None:
# Transform xerror so that
# log10(x) +/- xerror' = log10(x +/- xerror)
- if hasattr(xerror, 'shape') and len(xerror.shape) == 2:
+ if hasattr(xerror, "shape") and len(xerror.shape) == 2:
xErrorMinus, xErrorPlus = xerror[0], xerror[1]
else:
xErrorMinus, xErrorPlus = xerror, xerror
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
# Ignore divide by zero, invalid value encountered in log10
xErrorMinus = logX - numpy.log10(x - xErrorMinus)
xErrorPlus = numpy.log10(x + xErrorPlus) - logX
- xerror = numpy.array((xErrorMinus, xErrorPlus),
- dtype=numpy.float32)
+ xerror = numpy.array((xErrorMinus, xErrorPlus), dtype=numpy.float32)
x = logX
- isYLog = (yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
- yaxis == 'right' and self._plotFrame.y2Axis.isLog)
+ isYLog = (yaxis == "left" and self._plotFrame.yAxis.isLog) or (
+ yaxis == "right" and self._plotFrame.y2Axis.isLog
+ )
if isYLog:
logY = numpy.log10(y)
@@ -821,25 +997,23 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if yerror is not None:
# Transform yerror so that
# log10(y) +/- yerror' = log10(y +/- yerror)
- if hasattr(yerror, 'shape') and len(yerror.shape) == 2:
+ if hasattr(yerror, "shape") and len(yerror.shape) == 2:
yErrorMinus, yErrorPlus = yerror[0], yerror[1]
else:
yErrorMinus, yErrorPlus = yerror, yerror
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
# Ignore divide by zero, invalid value encountered in log10
yErrorMinus = logY - numpy.log10(y - yErrorMinus)
yErrorPlus = numpy.log10(y + yErrorPlus) - logY
- yerror = numpy.array((yErrorMinus, yErrorPlus),
- dtype=numpy.float32)
+ yerror = numpy.array((yErrorMinus, yErrorPlus), dtype=numpy.float32)
y = logY
# TODO check if need more filtering of error (e.g., clip to positive)
# TODO check and improve this
- if (len(color) == 4 and
- type(color[3]) in [type(1), numpy.uint8, numpy.int8]):
- color = numpy.array(color, dtype=numpy.float32) / 255.
+ if len(color) == 4 and type(color[3]) in [type(1), numpy.uint8, numpy.int8]:
+ color = numpy.array(color, dtype=numpy.float32) / 255.0
if isinstance(color, numpy.ndarray) and color.ndim == 2:
colorArray = color
@@ -848,7 +1022,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
colorArray = None
color = colors.rgba(color)
- if alpha < 1.: # Apply image transparency
+ if alpha < 1.0: # Apply image transparency
if colorArray is not None and colorArray.shape[1] == 4:
# multiply alpha channel
colorArray[:, 3] = colorArray[:, 3] * alpha
@@ -858,43 +1032,49 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
fillColor = None
if fill is True:
fillColor = color
+
+ dashoffset, dashpattern = self._lineStyleToDashOffsetPattern(linestyle)
curve = glutils.GLPlotCurve2D(
- x, y, colorArray,
+ x,
+ y,
+ colorArray,
xError=xerror,
yError=yerror,
- lineStyle=linestyle,
lineColor=color,
+ lineGapColor=gapcolor,
lineWidth=linewidth,
+ lineDashOffset=dashoffset,
+ lineDashPattern=dashpattern,
marker=symbol,
markerColor=color,
markerSize=symbolsize,
fillColor=fillColor,
baseline=baseline,
- isYLog=isYLog)
- curve.yaxis = 'left' if yaxis is None else yaxis
+ isYLog=isYLog,
+ )
+ curve.yaxis = "left" if yaxis is None else yaxis
if yaxis == "right":
self._plotFrame.isY2Axis = True
return curve
- def addImage(self, data,
- origin, scale,
- colormap, alpha):
+ def addImage(self, data, origin, scale, colormap, alpha):
for parameter in (data, origin, scale):
assert parameter is not None
if data.ndim == 2:
# Ensure array is contiguous and eventually convert its type
- dtypes = [dtype for dtype in (
- numpy.float32, numpy.float16, numpy.uint8, numpy.uint16)
- if glu.isSupportedGLType(dtype)]
+ dtypes = [
+ dtype
+ for dtype in (numpy.float32, numpy.float16, numpy.uint8, numpy.uint16)
+ if glu.isSupportedGLType(dtype)
+ ]
if data.dtype in dtypes:
- data = numpy.array(data, copy=False, order='C')
+ data = numpy.array(data, copy=False, order="C")
else:
- _logger.info(
- 'addImage: Convert %s data to float32', str(data.dtype))
- data = numpy.array(data, dtype=numpy.float32, order='C')
+ _logger.info("addImage: Convert %s data to float32", str(data.dtype))
+ data = numpy.array(data, dtype=numpy.float32, order="C")
normalization = colormap.getNormalization()
if normalization in glutils.GLPlotColormap.SUPPORTED_NORMALIZATIONS:
@@ -913,7 +1093,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
gamma,
cmapRange,
alpha,
- nanColor)
+ nanColor,
+ )
else: # Fallback applying colormap on CPU
rgba = colormap.applyToData(data)
@@ -930,7 +1111,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
elif numpy.issubdtype(data.dtype, numpy.integer):
data = numpy.array(data, dtype=numpy.uint8, copy=False)
else:
- raise ValueError('Unsupported data type')
+ raise ValueError("Unsupported data type")
image = glutils.GLPlotRGBAImage(data, origin, scale, alpha)
@@ -938,17 +1119,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
raise RuntimeError("Unsupported data shape {0}".format(data.shape))
# TODO is this needed?
- if self._plotFrame.xAxis.isLog and image.xMin <= 0.:
- raise RuntimeError(
- 'Cannot add image with X <= 0 with X axis log scale')
- if self._plotFrame.yAxis.isLog and image.yMin <= 0.:
- raise RuntimeError(
- 'Cannot add image with Y <= 0 with Y axis log scale')
+ if self._plotFrame.xAxis.isLog and image.xMin <= 0.0:
+ raise RuntimeError("Cannot add image with X <= 0 with X axis log scale")
+ if self._plotFrame.yAxis.isLog and image.yMin <= 0.0:
+ raise RuntimeError("Cannot add image with Y <= 0 with Y axis log scale")
return image
- def addTriangles(self, x, y, triangles,
- color, alpha):
+ def addTriangles(self, x, y, triangles, color, alpha):
# Handle axes log scale: convert data
if self._plotFrame.xAxis.isLog:
x = numpy.log10(x)
@@ -959,36 +1137,90 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return triangles
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
+ def addShape(
+ self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
+ ):
x = numpy.array(x, copy=False)
y = numpy.array(y, copy=False)
# TODO is this needed?
- if self._plotFrame.xAxis.isLog and x.min() <= 0.:
- raise RuntimeError(
- 'Cannot add item with X <= 0 with X axis log scale')
- if self._plotFrame.yAxis.isLog and y.min() <= 0.:
- raise RuntimeError(
- 'Cannot add item with Y <= 0 with Y axis log scale')
-
- return _ShapeItem(x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor)
-
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
- return _MarkerItem(x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis)
+ if self._plotFrame.xAxis.isLog and x.min() <= 0.0:
+ raise RuntimeError("Cannot add item with X <= 0 with X axis log scale")
+ if self._plotFrame.yAxis.isLog and y.min() <= 0.0:
+ raise RuntimeError("Cannot add item with Y <= 0 with Y axis log scale")
+
+ dashoffset, dashpattern = self._lineStyleToDashOffsetPattern(linestyle)
+ return _ShapeItem(
+ x,
+ y,
+ shape,
+ color,
+ fill,
+ overlay,
+ linewidth,
+ dashoffset,
+ dashpattern,
+ gapcolor,
+ )
+
+ def getDefaultFont(self):
+ """Returns the default font, used by raw markers and axes labels"""
+ if self._defaultFont is None:
+ from matplotlib.font_manager import findfont, FontProperties
+
+ font_filename = findfont(FontProperties(family=["sans-serif"]))
+ _logger.debug("Load font from mpl: %s", font_filename)
+ id = qt.QFontDatabase.addApplicationFont(font_filename)
+ family = qt.QFontDatabase.applicationFontFamilies(id)[0]
+ font = qt.QFont(family, 10, qt.QFont.Normal, False)
+ font.setStyleStrategy(qt.QFont.PreferAntialias)
+ self._defaultFont = font
+ return self._defaultFont
+
+ def addMarker(
+ self,
+ x,
+ y,
+ text,
+ color,
+ symbol,
+ linestyle,
+ linewidth,
+ constraint,
+ yaxis,
+ font,
+ bgcolor: RGBAColorType | None,
+ ):
+ if font is None:
+ font = self.getDefaultFont()
+
+ dashoffset, dashpattern = self._lineStyleToDashOffsetPattern(linestyle)
+ return _MarkerItem(
+ x,
+ y,
+ text,
+ color,
+ symbol,
+ linewidth,
+ dashoffset,
+ dashpattern,
+ constraint,
+ yaxis,
+ font,
+ bgcolor,
+ )
# Remove methods
def remove(self, item):
if isinstance(item, glutils.GLPlotItem):
- if item.yaxis == 'right':
+ if item.yaxis == "right":
# Check if some curves remains on the right Y axis
- y2AxisItems = (item for item in self._plot.getItems()
- if isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right')
+ y2AxisItems = (
+ item
+ for item in self._plot.getItems()
+ if isinstance(item, items.YAxisMixIn) and item.getYAxis() == "right"
+ )
self._plotFrame.isY2Axis = next(y2AxisItems, None) is not None
if item.isInitialized():
@@ -998,7 +1230,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
pass # No-op
else:
- _logger.error('Unsupported item: %s', str(item))
+ _logger.error("Unsupported item: %s", str(item))
# Interaction methods
@@ -1018,9 +1250,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
super(BackendOpenGL, self).setCursor(qt.QCursor(cursor))
def setGraphCursor(self, flag, color, linewidth, linestyle):
- if linestyle != '-':
- _logger.warning(
- "BackendOpenGL.setGraphCursor linestyle parameter ignored")
+ if linestyle != "-":
+ _logger.warning("BackendOpenGL.setGraphCursor linestyle parameter ignored")
if flag:
color = colors.rgba(color)
@@ -1044,8 +1275,10 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
:rtype: List[float]
"""
left, top, width, height = self.getPlotBoundsInPixels()
- return (numpy.clip(x, left, left + width - 1), # TODO -1?
- numpy.clip(y, top, top + height - 1))
+ return (
+ numpy.clip(x, left, left + width - 1), # TODO -1?
+ numpy.clip(y, top, top + height - 1),
+ )
def __pickCurves(self, item, x, y):
"""Perform picking on a curve item.
@@ -1060,24 +1293,26 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if item.marker is not None:
# Convert markerSize from points to qt pixels
qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
- size = item.markerSize / 72. * qtDpi
- offset = max(size / 2., offset)
- if item.lineStyle is not None:
+ size = item.markerSize / 72.0 * qtDpi
+ offset = max(size / 2.0, offset)
+ if item.lineDashPattern is not None:
# Convert line width from points to qt pixels
qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
- lineWidth = item.lineWidth / 72. * qtDpi
- offset = max(lineWidth / 2., offset)
+ lineWidth = item.lineWidth / 72.0 * qtDpi
+ offset = max(lineWidth / 2.0, offset)
inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
- dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=item.yaxis, check=True)
+ dataPos = self._plot.pixelToData(
+ inAreaPos[0], inAreaPos[1], axis=item.yaxis, check=True
+ )
if dataPos is None:
return None
xPick0, yPick0 = dataPos
inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
- dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=item.yaxis, check=True)
+ dataPos = self._plot.pixelToData(
+ inAreaPos[0], inAreaPos[1], axis=item.yaxis, check=True
+ )
if dataPos is None:
return None
xPick1, yPick1 = dataPos
@@ -1097,17 +1332,17 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
xPickMin = numpy.log10(xPickMin)
xPickMax = numpy.log10(xPickMax)
- if (item.yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
- item.yaxis == 'right' and self._plotFrame.y2Axis.isLog):
+ if (item.yaxis == "left" and self._plotFrame.yAxis.isLog) or (
+ item.yaxis == "right" and self._plotFrame.y2Axis.isLog
+ ):
yPickMin = numpy.log10(yPickMin)
yPickMax = numpy.log10(yPickMax)
- return item.pick(xPickMin, yPickMin,
- xPickMax, yPickMax)
+ return item.pick(xPickMin, yPickMin, xPickMax, yPickMax)
def pickItem(self, x, y, item):
# Picking is performed in Qt widget pixels not device pixels
- dataPos = self._plot.pixelToData(x, y, axis='left', check=True)
+ dataPos = self._plot.pixelToData(x, y, axis="left", check=True)
if dataPos is None:
return None # Outside plot area
@@ -1117,32 +1352,36 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
# Pick markers
if isinstance(item, _MarkerItem):
- yaxis = item['yaxis']
+ yaxis = item["yaxis"]
pixelPos = self._plot.dataToPixel(
- item['x'], item['y'], axis=yaxis, check=False)
+ item["x"], item["y"], axis=yaxis, check=False
+ )
if pixelPos is None:
return None # negative coord on a log axis
- if item['x'] is None: # Horizontal line
+ if item["x"] is None: # Horizontal line
pt1 = self._plot.pixelToData(
- x, y - self._PICK_OFFSET, axis=yaxis, check=False)
+ x, y - self._PICK_OFFSET, axis=yaxis, check=False
+ )
pt2 = self._plot.pixelToData(
- x, y + self._PICK_OFFSET, axis=yaxis, check=False)
- isPicked = (min(pt1[1], pt2[1]) <= item['y'] <=
- max(pt1[1], pt2[1]))
+ x, y + self._PICK_OFFSET, axis=yaxis, check=False
+ )
+ isPicked = min(pt1[1], pt2[1]) <= item["y"] <= max(pt1[1], pt2[1])
- elif item['y'] is None: # Vertical line
+ elif item["y"] is None: # Vertical line
pt1 = self._plot.pixelToData(
- x - self._PICK_OFFSET, y, axis=yaxis, check=False)
+ x - self._PICK_OFFSET, y, axis=yaxis, check=False
+ )
pt2 = self._plot.pixelToData(
- x + self._PICK_OFFSET, y, axis=yaxis, check=False)
- isPicked = (min(pt1[0], pt2[0]) <= item['x'] <=
- max(pt1[0], pt2[0]))
+ x + self._PICK_OFFSET, y, axis=yaxis, check=False
+ )
+ isPicked = min(pt1[0], pt2[0]) <= item["x"] <= max(pt1[0], pt2[0])
else:
isPicked = (
- numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET and
- numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET)
+ numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET
+ and numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET
+ )
return (0,) if isPicked else None
@@ -1173,11 +1412,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if dpi is not None:
_logger.warning("saveGraph ignores dpi parameter")
- if fileFormat not in ['png', 'ppm', 'svg', 'tiff']:
- raise NotImplementedError('Unsupported format: %s' % fileFormat)
+ if fileFormat not in ["png", "ppm", "svg", "tif", "tiff"]:
+ raise NotImplementedError("Unsupported format: %s" % fileFormat)
if not self.isValid():
- _logger.error('OpenGL 2.1 not available, cannot save OpenGL image')
+ _logger.error("OpenGL 2.1 not available, cannot save OpenGL image")
width, height = self._plotFrame.size
data = numpy.zeros((height, width, 3), dtype=numpy.uint8)
else:
@@ -1185,7 +1424,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
data = numpy.empty(
(self._plotFrame.size[1], self._plotFrame.size[0], 3),
- dtype=numpy.uint8, order='C')
+ dtype=numpy.uint8,
+ order="C",
+ )
context = self.context()
framebufferTexture = self._plotFBOs.get(context)
@@ -1201,8 +1442,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fboName)
gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
- gl.glReadPixels(0, 0, width, height,
- gl.GL_RGB, gl.GL_UNSIGNED_BYTE, data)
+ gl.glReadPixels(0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, data)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer)
# glReadPixels gives bottom to top,
@@ -1221,7 +1461,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
self._plotFrame.xAxis.title = label
def setGraphYLabel(self, label, axis):
- if axis == 'left':
+ if axis == "left":
self._plotFrame.yAxis.title = label
else: # right axis
self._plotFrame.y2Axis.title = label
@@ -1254,24 +1494,27 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
if keepDim is None:
ranges = self._plot.getDataRange()
- if (ranges.y is not None and
- ranges.x is not None and
- (ranges.y[1] - ranges.y[0]) != 0.):
- dataRatio = (ranges.x[1] - ranges.x[0]) / float(ranges.y[1] - ranges.y[0])
+ if (
+ ranges.y is not None
+ and ranges.x is not None
+ and (ranges.y[1] - ranges.y[0]) != 0.0
+ ):
+ dataRatio = (ranges.x[1] - ranges.x[0]) / float(
+ ranges.y[1] - ranges.y[0]
+ )
plotRatio = plotWidth / float(plotHeight) # Test != 0 before
- keepDim = 'x' if dataRatio > plotRatio else 'y'
+ keepDim = "x" if dataRatio > plotRatio else "y"
else: # Limit case
- keepDim = 'x'
+ keepDim = "x"
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \
- self._plotFrame.dataRanges
- if keepDim == 'y':
+ (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self._plotFrame.dataRanges
+ if keepDim == "y":
dataW = (yMax - yMin) * plotWidth / float(plotHeight)
xCenter = 0.5 * (xMin + xMax)
xMin = xCenter - 0.5 * dataW
xMax = xCenter + 0.5 * dataW
- elif keepDim == 'x':
+ elif keepDim == "x":
dataH = (xMax - xMin) * plotHeight / float(plotWidth)
yCenter = 0.5 * (yMin + yMax)
yMin = yCenter - 0.5 * dataH
@@ -1280,19 +1523,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
y2Min = y2Center - 0.5 * dataH
y2Max = y2Center + 0.5 * dataH
else:
- raise RuntimeError('Unsupported dimension to keep: %s' % keepDim)
+ raise RuntimeError("Unsupported dimension to keep: %s" % keepDim)
# Update plot frame bounds
- self._setDataRanges(xlim=(xMin, xMax),
- ylim=(yMin, yMax),
- y2lim=(y2Min, y2Max))
+ self._setDataRanges(xlim=(xMin, xMax), ylim=(yMin, yMax), y2lim=(y2Min, y2Max))
- def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None,
- keepDim=None):
+ def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None, keepDim=None):
# Update axes range with a clipped range if too wide
- self._setDataRanges(xlim=xRange,
- ylim=yRange,
- y2lim=y2Range)
+ self._setDataRanges(xlim=xRange, ylim=yRange, y2lim=y2Range)
# Keep data aspect ratio
if self.isKeepDataAspectRatio():
@@ -1314,7 +1552,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def setGraphXLimits(self, xmin, xmax):
assert xmin < xmax
- self._setPlotBounds(xRange=(xmin, xmax), keepDim='x')
+ self._setPlotBounds(xRange=(xmin, xmax), keepDim="x")
def getGraphYLimits(self, axis):
assert axis in ("left", "right")
@@ -1328,9 +1566,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
assert axis in ("left", "right")
if axis == "left":
- self._setPlotBounds(yRange=(ymin, ymax), keepDim='y')
+ self._setPlotBounds(yRange=(ymin, ymax), keepDim="y")
else:
- self._setPlotBounds(y2Range=(ymin, ymax), keepDim='y')
+ self._setPlotBounds(y2Range=(ymin, ymax), keepDim="y")
# Graph axes
@@ -1349,17 +1587,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def setXAxisLogarithmic(self, flag):
if flag != self._plotFrame.xAxis.isLog:
if flag and self._keepDataAspectRatio:
- _logger.warning(
- "KeepDataAspectRatio is ignored with log axes")
+ _logger.warning("KeepDataAspectRatio is ignored with log axes")
self._plotFrame.xAxis.isLog = flag
def setYAxisLogarithmic(self, flag):
- if (flag != self._plotFrame.yAxis.isLog or
- flag != self._plotFrame.y2Axis.isLog):
+ if flag != self._plotFrame.yAxis.isLog or flag != self._plotFrame.y2Axis.isLog:
if flag and self._keepDataAspectRatio:
- _logger.warning(
- "KeepDataAspectRatio is ignored with log axes")
+ _logger.warning("KeepDataAspectRatio is ignored with log axes")
self._plotFrame.yAxis.isLog = flag
self._plotFrame.y2Axis.isLog = flag
@@ -1371,6 +1606,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
def isYAxisInverted(self):
return self._plotFrame.isYAxisInverted
+ def isYRightAxisVisible(self):
+ return self._plotFrame.isY2Axis
+
def isKeepDataAspectRatio(self):
if self._plotFrame.xAxis.isLog or self._plotFrame.yAxis.isLog:
return False
@@ -1378,14 +1616,13 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return self._keepDataAspectRatio
def setKeepDataAspectRatio(self, flag):
- if flag and (self._plotFrame.xAxis.isLog or
- self._plotFrame.yAxis.isLog):
+ if flag and (self._plotFrame.xAxis.isLog or self._plotFrame.yAxis.isLog):
_logger.warning("KeepDataAspectRatio is ignored with log axes")
self._keepDataAspectRatio = flag
def setGraphGrid(self, which):
- assert which in (None, 'major', 'both')
+ assert which in (None, "major", "both")
self._plotFrame.grid = which is not None # TODO True grid support
# Data <-> Pixel coordinates conversion
@@ -1396,17 +1633,20 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
return None
else:
devicePixelRatio = self.getDevicePixelRatio()
- return tuple(value/devicePixelRatio for value in result)
+ return tuple(value / devicePixelRatio for value in result)
def pixelToData(self, x, y, axis):
devicePixelRatio = self.getDevicePixelRatio()
return self._plotFrame.pixelToData(
- x * devicePixelRatio, y * devicePixelRatio, axis)
+ x * devicePixelRatio, y * devicePixelRatio, axis
+ )
def getPlotBoundsInPixels(self):
devicePixelRatio = self.getDevicePixelRatio()
- return tuple(int(value / devicePixelRatio)
- for value in self._plotFrame.plotOrigin + self._plotFrame.plotSize)
+ return tuple(
+ int(value / devicePixelRatio)
+ for value in self._plotFrame.plotOrigin + self._plotFrame.plotSize
+ )
def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
self._plotFrame.marginRatios = left, top, right, bottom
diff --git a/src/silx/gui/plot/backends/__init__.py b/src/silx/gui/plot/backends/__init__.py
index 966d9df..d75a943 100644
--- a/src/silx/gui/plot/backends/__init__.py
+++ b/src/silx/gui/plot/backends/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotCurve.py b/src/silx/gui/plot/backends/glutils/GLPlotCurve.py
index e4667b4..26442d7 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotCurve.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,8 +25,6 @@
This module provides classes to render 2D lines and scatter plots
"""
-from __future__ import division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "03/04/2017"
@@ -49,7 +46,7 @@ from .GLPlotImage import GLPlotItem
_logger = logging.getLogger(__name__)
-_MPL_NONES = None, 'None', '', ' '
+_MPL_NONES = None, "None", "", " "
"""Possible values for None"""
@@ -78,6 +75,7 @@ def _notNaNSlices(array, length=1):
# fill ########################################################################
+
class _Fill2D(object):
"""Object rendering curve filling as polygons
@@ -110,12 +108,17 @@ class _Fill2D(object):
gl_FragColor = color;
}
""",
- attrib0='xPos')
-
- def __init__(self, xData=None, yData=None,
- baseline=0,
- color=(0., 0., 0., 1.),
- offset=(0., 0.)):
+ attrib0="xPos",
+ )
+
+ def __init__(
+ self,
+ xData=None,
+ yData=None,
+ baseline=0,
+ color=(0.0, 0.0, 0.0, 1.0),
+ offset=(0.0, 0.0),
+ ):
self.xData = xData
self.yData = yData
self._xFillVboData = None
@@ -128,9 +131,11 @@ class _Fill2D(object):
def prepare(self):
"""Rendering preparation: build indices and bounding box vertices"""
- if (self._xFillVboData is None and
- self.xData is not None and self.yData is not None):
-
+ if (
+ self._xFillVboData is None
+ and self.xData is not None
+ and self.yData is not None
+ ):
# Get slices of not NaN values longer than 1 element
isnan = numpy.logical_or(numpy.isnan(self.xData), numpy.isnan(self.yData))
notnan = numpy.logical_not(isnan)
@@ -154,20 +159,28 @@ class _Fill2D(object):
new_y_data = numpy.append(self.yData, self.baseline)
for start, end in slices:
# Duplicate first point for connecting degenerated triangle
- points[offset:offset+2] = self.xData[start], new_y_data[start]
+ points[offset : offset + 2] = self.xData[start], new_y_data[start]
# 2nd point of the polygon is last point
- points[offset+2] = self.xData[start], self.baseline[start]
-
- indices = numpy.append(numpy.arange(start, end),
- numpy.arange(len(self.xData) + end-1, len(self.xData) + start-1, -1))
+ points[offset + 2] = self.xData[start], self.baseline[start]
+
+ indices = numpy.append(
+ numpy.arange(start, end),
+ numpy.arange(
+ len(self.xData) + end - 1, len(self.xData) + start - 1, -1
+ ),
+ )
indices = indices[buildFillMaskIndices(len(indices))]
- points[offset+3:offset+3+len(indices), 0] = self.xData[indices % len(self.xData)]
- points[offset+3:offset+3+len(indices), 1] = new_y_data[indices]
+ points[offset + 3 : offset + 3 + len(indices), 0] = self.xData[
+ indices % len(self.xData)
+ ]
+ points[offset + 3 : offset + 3 + len(indices), 1] = new_y_data[indices]
# Duplicate last point for connecting degenerated triangle
- points[offset+3+len(indices)] = points[offset+3+len(indices)-1]
+ points[offset + 3 + len(indices)] = points[
+ offset + 3 + len(indices) - 1
+ ]
offset += len(indices) + 4
@@ -186,14 +199,18 @@ class _Fill2D(object):
self._PROGRAM.use()
gl.glUniformMatrix4fv(
- self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
- numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32))
+ self._PROGRAM.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ numpy.dot(context.matrix, mat4Translate(*self.offset)).astype(
+ numpy.float32
+ ),
+ )
- gl.glUniform4f(self._PROGRAM.uniforms['color'], *self.color)
+ gl.glUniform4f(self._PROGRAM.uniforms["color"], *self.color)
- xPosAttrib = self._PROGRAM.attributes['xPos']
- yPosAttrib = self._PROGRAM.attributes['yPos']
+ xPosAttrib = self._PROGRAM.attributes["xPos"]
+ yPosAttrib = self._PROGRAM.attributes["yPos"]
gl.glEnableVertexAttribArray(xPosAttrib)
self._xFillVboData.setVertexAttrib(xPosAttrib)
@@ -218,16 +235,30 @@ class _Fill2D(object):
gl.glDepthMask(gl.GL_TRUE)
# Draw directly in NDC
- gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
- mat4Identity().astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ self._PROGRAM.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ mat4Identity().astype(numpy.float32),
+ )
# NDC vertices
gl.glVertexAttribPointer(
- xPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
- numpy.array((-1., -1., 1., 1.), dtype=numpy.float32))
+ xPosAttrib,
+ 1,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ numpy.array((-1.0, -1.0, 1.0, 1.0), dtype=numpy.float32),
+ )
gl.glVertexAttribPointer(
- yPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
- numpy.array((-1., 1., -1., 1.), dtype=numpy.float32))
+ yPosAttrib,
+ 1,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ numpy.array((-1.0, 1.0, -1.0, 1.0), dtype=numpy.float32),
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
@@ -247,8 +278,6 @@ class _Fill2D(object):
# line ########################################################################
-SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':'
-
class GLLines2D(object):
"""Object rendering curve as a polyline
@@ -257,17 +286,18 @@ class GLLines2D(object):
:param yVboData: Y coordinates VBO
:param colorVboData: VBO of colors
:param distVboData: VBO of distance along the polyline
- :param str style: Line style in: '-', '--', '-.', ':'
:param List[float] color: RGBA color as 4 float in [0, 1]
:param float width: Line width
- :param float dashPeriod: Period of dashes
+ :param List[float] dashPattern:
+ "unscaled" dash pattern as 4 lengths in points (dash1, gap1, dash2, gap2).
+ This pattern is scaled with the line width.
+ Set to () to draw solid lines (default), and to None to disable rendering.
+ :param float dashOffset: The offset in points the patterns starts at.
+ The offset is scaled with the line width.
:param drawMode: OpenGL drawing mode
:param List[float] offset: Translation of coordinates (ox, oy)
"""
- STYLES = SOLID, DASHED, DASHDOT, DOTTED
- """Supported line styles"""
-
_SOLID_PROGRAM = Program(
vertexShader="""
#version 120
@@ -293,7 +323,8 @@ class GLLines2D(object):
gl_FragColor = vColor;
}
""",
- attrib0='xPos')
+ attrib0="xPos",
+ )
# Limitation: Dash using an estimate of distance in screen coord
# to avoid computing distance when viewport is resized
@@ -303,7 +334,7 @@ class GLLines2D(object):
#version 120
uniform mat4 matrix;
- uniform vec2 halfViewportSize;
+ uniform float distanceScale;
attribute float xPos;
attribute float yPos;
attribute vec4 color;
@@ -314,11 +345,7 @@ class GLLines2D(object):
void main(void) {
gl_Position = matrix * vec4(xPos, yPos, 0., 1.);
- //Estimate distance in pixels
- vec2 probe = vec2(matrix * vec4(1., 1., 0., 0.)) *
- halfViewportSize;
- float pixelPerDataEstimate = length(probe)/sqrt(2.);
- vDist = distance * pixelPerDataEstimate;
+ vDist = distance * distanceScale;
vColor = color;
}
""",
@@ -328,51 +355,60 @@ class GLLines2D(object):
/* Dashes: [0, x], [y, z]
Dash period: w */
uniform vec4 dash;
- uniform vec4 dash2ndColor;
+ uniform float dashOffset;
+ uniform vec4 gapColor;
varying float vDist;
varying vec4 vColor;
void main(void) {
- float dist = mod(vDist, dash.w);
+ float dist = mod(vDist + dashOffset, dash.w);
if ((dist > dash.x && dist < dash.y) || dist > dash.z) {
- if (dash2ndColor.a == 0.) {
+ if (gapColor.a == 0.) {
discard; // Discard full transparent bg color
} else {
- gl_FragColor = dash2ndColor;
+ gl_FragColor = gapColor;
}
} else {
gl_FragColor = vColor;
}
}
""",
- attrib0='xPos')
-
- def __init__(self, xVboData=None, yVboData=None,
- colorVboData=None, distVboData=None,
- style=SOLID, color=(0., 0., 0., 1.), dash2ndColor=None,
- width=1, dashPeriod=10., drawMode=None,
- offset=(0., 0.)):
- if (xVboData is not None and
- not isinstance(xVboData, VertexBufferAttrib)):
+ attrib0="xPos",
+ )
+
+ def __init__(
+ self,
+ xVboData=None,
+ yVboData=None,
+ colorVboData=None,
+ distVboData=None,
+ color=(0.0, 0.0, 0.0, 1.0),
+ gapColor=None,
+ width=1,
+ dashOffset=0.0,
+ dashPattern=(),
+ drawMode=None,
+ offset=(0.0, 0.0),
+ ):
+ if xVboData is not None and not isinstance(xVboData, VertexBufferAttrib):
xVboData = numpy.array(xVboData, copy=False, dtype=numpy.float32)
self.xVboData = xVboData
- if (yVboData is not None and
- not isinstance(yVboData, VertexBufferAttrib)):
+ if yVboData is not None and not isinstance(yVboData, VertexBufferAttrib):
yVboData = numpy.array(yVboData, copy=False, dtype=numpy.float32)
self.yVboData = yVboData
# Compute distances if not given while providing numpy array coordinates
- if (isinstance(self.xVboData, numpy.ndarray) and
- isinstance(self.yVboData, numpy.ndarray) and
- distVboData is None):
+ if (
+ isinstance(self.xVboData, numpy.ndarray)
+ and isinstance(self.yVboData, numpy.ndarray)
+ and distVboData is None
+ ):
distVboData = distancesFromArrays(self.xVboData, self.yVboData)
- if (distVboData is not None and
- not isinstance(distVboData, VertexBufferAttrib)):
- distVboData = numpy.array(
- distVboData, copy=False, dtype=numpy.float32)
+ if distVboData is not None and not isinstance(distVboData, VertexBufferAttrib):
+ distVboData = numpy.array(distVboData, copy=False, dtype=numpy.float32)
self.distVboData = distVboData
if colorVboData is not None:
@@ -381,28 +417,14 @@ class GLLines2D(object):
self.useColorVboData = colorVboData is not None
self.color = color
- self.dash2ndColor = dash2ndColor
+ self.gapColor = gapColor
self.width = width
- self._style = None
- self.style = style
- self.dashPeriod = dashPeriod
+ self.dashPattern = dashPattern
+ self.dashOffset = dashOffset
self.offset = offset
self._drawMode = drawMode if drawMode is not None else gl.GL_LINE_STRIP
- @property
- def style(self):
- """Line style (Union[str,None])"""
- return self._style
-
- @style.setter
- def style(self, style):
- if style in _MPL_NONES:
- self._style = None
- else:
- assert style in self.STYLES
- self._style = style
-
@classmethod
def init(cls):
"""OpenGL context initialization"""
@@ -413,71 +435,57 @@ class GLLines2D(object):
:param RenderContext context:
"""
- width = self.width / 72. * context.dpi
-
- style = self.style
- if style is None:
+ if self.dashPattern is None: # Nothing to display
return
- elif style == SOLID:
+ if self.dashPattern == (): # No dash: solid line
program = self._SOLID_PROGRAM
program.use()
- else: # DASHED, DASHDOT, DOTTED
+ else: # Dashed line defined by 4 control points
program = self._DASH_PROGRAM
program.use()
- x, y, viewWidth, viewHeight = gl.glGetFloatv(gl.GL_VIEWPORT)
- gl.glUniform2f(program.uniforms['halfViewportSize'],
- 0.5 * viewWidth, 0.5 * viewHeight)
-
- dashPeriod = self.dashPeriod * width
- if self.style == DOTTED:
- dash = (0.2 * dashPeriod,
- 0.5 * dashPeriod,
- 0.7 * dashPeriod,
- dashPeriod)
- elif self.style == DASHDOT:
- dash = (0.3 * dashPeriod,
- 0.5 * dashPeriod,
- 0.6 * dashPeriod,
- dashPeriod)
- else:
- dash = (0.5 * dashPeriod,
- dashPeriod,
- dashPeriod,
- dashPeriod)
-
- gl.glUniform4f(program.uniforms['dash'], *dash)
+ # Scale pattern by width, convert from lengths in points to offsets in pixels
+ scale = self.width / 72.0 * context.dpi
+ dashOffsets = tuple(
+ offset * scale for offset in numpy.cumsum(self.dashPattern)
+ )
+ gl.glUniform4f(program.uniforms["dash"], *dashOffsets)
+ gl.glUniform1f(program.uniforms["dashOffset"], self.dashOffset * scale)
- if self.dash2ndColor is None:
+ if self.gapColor is None:
# Use fully transparent color which gets discarded in shader
- dash2ndColor = (0., 0., 0., 0.)
+ gapColor = (0.0, 0.0, 0.0, 0.0)
else:
- dash2ndColor = self.dash2ndColor
- gl.glUniform4f(program.uniforms['dash2ndColor'], *dash2ndColor)
-
- distAttrib = program.attributes['distance']
+ gapColor = self.gapColor
+ gl.glUniform4f(program.uniforms["gapColor"], *gapColor)
+
+ viewWidth = gl.glGetFloatv(gl.GL_VIEWPORT)[2]
+ xNDCPerData = (
+ numpy.dot(context.matrix, [1.0, 0.0, 0.0, 1.0])[0]
+ - numpy.dot(context.matrix, [0.0, 0.0, 0.0, 1.0])[0]
+ )
+ xPixelPerData = 0.5 * viewWidth * xNDCPerData
+ gl.glUniform1f(program.uniforms["distanceScale"], xPixelPerData)
+
+ distAttrib = program.attributes["distance"]
gl.glEnableVertexAttribArray(distAttrib)
if isinstance(self.distVboData, VertexBufferAttrib):
self.distVboData.setVertexAttrib(distAttrib)
else:
- gl.glVertexAttribPointer(distAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.distVboData)
-
- if width != 1:
- gl.glEnable(gl.GL_LINE_SMOOTH)
-
- matrix = numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32)
- gl.glUniformMatrix4fv(program.uniforms['matrix'],
- 1, gl.GL_TRUE, matrix)
-
- colorAttrib = program.attributes['color']
+ gl.glVertexAttribPointer(
+ distAttrib, 1, gl.GL_FLOAT, False, 0, self.distVboData
+ )
+
+ gl.glEnable(gl.GL_LINE_SMOOTH)
+
+ matrix = numpy.dot(context.matrix, mat4Translate(*self.offset)).astype(
+ numpy.float32
+ )
+ gl.glUniformMatrix4fv(program.uniforms["matrix"], 1, gl.GL_TRUE, matrix)
+
+ colorAttrib = program.attributes["color"]
if self.useColorVboData and self.colorVboData is not None:
gl.glEnableVertexAttribArray(colorAttrib)
self.colorVboData.setVertexAttrib(colorAttrib)
@@ -485,46 +493,44 @@ class GLLines2D(object):
gl.glDisableVertexAttribArray(colorAttrib)
gl.glVertexAttrib4f(colorAttrib, *self.color)
- xPosAttrib = program.attributes['xPos']
+ xPosAttrib = program.attributes["xPos"]
gl.glEnableVertexAttribArray(xPosAttrib)
if isinstance(self.xVboData, VertexBufferAttrib):
self.xVboData.setVertexAttrib(xPosAttrib)
else:
- gl.glVertexAttribPointer(xPosAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.xVboData)
-
- yPosAttrib = program.attributes['yPos']
+ gl.glVertexAttribPointer(
+ xPosAttrib, 1, gl.GL_FLOAT, False, 0, self.xVboData
+ )
+
+ yPosAttrib = program.attributes["yPos"]
gl.glEnableVertexAttribArray(yPosAttrib)
if isinstance(self.yVboData, VertexBufferAttrib):
self.yVboData.setVertexAttrib(yPosAttrib)
else:
- gl.glVertexAttribPointer(yPosAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.yVboData)
-
- gl.glLineWidth(width)
+ gl.glVertexAttribPointer(
+ yPosAttrib, 1, gl.GL_FLOAT, False, 0, self.yVboData
+ )
+
+ gl.glLineWidth(self.width / 72.0 * context.dpi)
gl.glDrawArrays(self._drawMode, 0, self.xVboData.size)
gl.glDisable(gl.GL_LINE_SMOOTH)
-def distancesFromArrays(xData, yData):
+def distancesFromArrays(xData, yData, ratio: float = 1.0):
"""Returns distances between each points
:param numpy.ndarray xData: X coordinate of points
:param numpy.ndarray yData: Y coordinate of points
+ :param ratio: Y/X pixel per data resolution ratio
:rtype: numpy.ndarray
"""
# Split array into sub-shapes at not finite points
- splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
- numpy.isfinite(xData), numpy.isfinite(yData))))[0]
+ splits = numpy.nonzero(
+ numpy.logical_not(
+ numpy.logical_and(numpy.isfinite(xData), numpy.isfinite(yData))
+ )
+ )[0]
splits = numpy.concatenate(([-1], splits, [len(xData) - 1]))
# Compute distance independently for each sub-shapes,
@@ -533,23 +539,35 @@ def distancesFromArrays(xData, yData):
for begin, end in zip(splits[:-1] + 1, splits[1:] + 1):
if begin == end: # Empty shape
continue
- elif end - begin == 1: # Single element
- distances.append([0])
+ elif end - begin == 1: # Single element
+ distances.append(numpy.array([0], dtype=numpy.float32))
else:
- deltas = numpy.dstack((
- numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.)),
- numpy.ediff1d(yData[begin:end], to_begin=numpy.float32(0.))))[0]
- distances.append(
- numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1))))
+ deltas = numpy.dstack(
+ (
+ numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.0)),
+ numpy.ediff1d(
+ yData[begin:end] * ratio, to_begin=numpy.float32(0.0)
+ ),
+ )
+ )[0]
+ distances.append(numpy.cumsum(numpy.sqrt(numpy.sum(deltas**2, axis=1))))
return numpy.concatenate(distances)
# points ######################################################################
-DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = \
- 'd', 'o', 's', '+', 'x', '.', ',', '*'
+DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = (
+ "d",
+ "o",
+ "s",
+ "+",
+ "x",
+ ".",
+ ",",
+ "*",
+)
-H_LINE, V_LINE, HEART = '_', '|', u'\u2665'
+H_LINE, V_LINE, HEART = "_", "|", "\u2665"
TICK_LEFT = "tickleft"
TICK_RIGHT = "tickright"
@@ -561,7 +579,7 @@ CARET_UP = "caretup"
CARET_DOWN = "caretdown"
-class _Points2D(object):
+class Points2D(object):
"""Object rendering curve markers
:param xVboData: X coordinates VBO
@@ -573,9 +591,27 @@ class _Points2D(object):
:param List[float] offset: Translation of coordinates (ox, oy)
"""
- MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK,
- H_LINE, V_LINE, HEART, TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN,
- CARET_LEFT, CARET_RIGHT, CARET_UP, CARET_DOWN)
+ MARKERS = (
+ DIAMOND,
+ CIRCLE,
+ SQUARE,
+ PLUS,
+ X_MARKER,
+ POINT,
+ PIXEL,
+ ASTERISK,
+ H_LINE,
+ V_LINE,
+ HEART,
+ TICK_LEFT,
+ TICK_RIGHT,
+ TICK_UP,
+ TICK_DOWN,
+ CARET_LEFT,
+ CARET_RIGHT,
+ CARET_UP,
+ CARET_DOWN,
+ )
"""List of supported markers"""
_VERTEX_SHADER = """
@@ -598,47 +634,39 @@ class _Points2D(object):
"""
_FRAGMENT_SHADER_SYMBOLS = {
- DIAMOND: """
+ DIAMOND: """
float alphaSymbol(vec2 coord, float size) {
vec2 centerCoord = abs(coord - vec2(0.5, 0.5));
float f = centerCoord.x + centerCoord.y;
return clamp(size * (0.5 - f), 0.0, 1.0);
}
""",
- CIRCLE: """
+ CIRCLE: """
float alphaSymbol(vec2 coord, float size) {
float radius = 0.5;
float r = distance(coord, vec2(0.5, 0.5));
return clamp(size * (radius - r), 0.0, 1.0);
}
""",
- SQUARE: """
+ SQUARE: """
float alphaSymbol(vec2 coord, float size) {
return 1.0;
}
""",
- PLUS: """
+ PLUS: """
float alphaSymbol(vec2 coord, float size) {
vec2 d = abs(size * (coord - vec2(0.5, 0.5)));
- if (min(d.x, d.y) < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
+ return local_smoothstep(1.5, 0.5, min(d.x, d.y));
}
""",
- X_MARKER: """
+ X_MARKER: """
float alphaSymbol(vec2 coord, float size) {
vec2 pos = floor(size * coord) + 0.5;
vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
- if (min(d_x.x, d_x.y) <= 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
+ return local_smoothstep(1.5, 0.5, min(d_x.x, d_x.y));
}
""",
- ASTERISK: """
+ ASTERISK: """
float alphaSymbol(vec2 coord, float size) {
/* Combining +, x and circle */
vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5)));
@@ -654,27 +682,19 @@ class _Points2D(object):
}
}
""",
- H_LINE: """
+ H_LINE: """
float alphaSymbol(vec2 coord, float size) {
- float dy = abs(size * (coord.y - 0.5));
- if (dy < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
+ float d = abs(size * (coord.y - 0.5));
+ return local_smoothstep(1.5, 0.5, d);
}
""",
- V_LINE: """
+ V_LINE: """
float alphaSymbol(vec2 coord, float size) {
- float dx = abs(size * (coord.x - 0.5));
- if (dx < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
+ float d = abs(size * (coord.x - 0.5));
+ return local_smoothstep(1.5, 0.5, d);
}
""",
- HEART: """
+ HEART: """
float alphaSymbol(vec2 coord, float size) {
coord = (coord - 0.5) * 2.;
coord *= 0.75;
@@ -685,93 +705,89 @@ class _Points2D(object):
float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h);
float res = clamp(r-d, 0., 1.);
// antialiasing
- res = smoothstep(0.1, 0.001, res);
+ res = local_smoothstep(0.1, 0.001, res);
return res;
}
""",
- TICK_LEFT: """
+ TICK_LEFT: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float dy = abs(coord.y);
- if (dy < 0.5 && coord.x < 0.5) {
- return 1.0;
- } else {
+ if (coord.x > 0.5) {
return 0.0;
}
+ return local_smoothstep(1.5, 0.5, dy);
}
""",
- TICK_RIGHT: """
+ TICK_RIGHT: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float dy = abs(coord.y);
- if (dy < 0.5 && coord.x > -0.5) {
- return 1.0;
- } else {
+ if (coord.x < -0.5) {
return 0.0;
}
+ return local_smoothstep(1.5, 0.5, dy);
}
""",
- TICK_UP: """
+ TICK_UP: """
float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
+ coord = size * (coord - 0.5);
float dx = abs(coord.x);
- if (dx < 0.5 && coord.y < 0.5) {
- return 1.0;
- } else {
+ if (coord.y > 0.5) {
return 0.0;
}
+ return local_smoothstep(1.5, 0.5, dx);
}
""",
- TICK_DOWN: """
+ TICK_DOWN: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float dx = abs(coord.x);
- if (dx < 0.5 && coord.y > -0.5) {
- return 1.0;
- } else {
+ if (coord.y < -0.5) {
return 0.0;
}
+ return local_smoothstep(1.5, 0.5, dx);
}
""",
- CARET_LEFT: """
+ CARET_LEFT: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float d = abs(coord.x) - abs(coord.y);
if (d >= -0.1 && coord.x > 0.5) {
- return smoothstep(-0.1, 0.1, d);
+ return local_smoothstep(-0.1, 0.1, d);
} else {
return 0.0;
}
}
""",
- CARET_RIGHT: """
+ CARET_RIGHT: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float d = abs(coord.x) - abs(coord.y);
if (d >= -0.1 && coord.x < 0.5) {
- return smoothstep(-0.1, 0.1, d);
+ return local_smoothstep(-0.1, 0.1, d);
} else {
return 0.0;
}
}
""",
- CARET_UP: """
+ CARET_UP: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float d = abs(coord.y) - abs(coord.x);
if (d >= -0.1 && coord.y > 0.5) {
- return smoothstep(-0.1, 0.1, d);
+ return local_smoothstep(-0.1, 0.1, d);
} else {
return 0.0;
}
}
""",
- CARET_DOWN: """
+ CARET_DOWN: """
float alphaSymbol(vec2 coord, float size) {
coord = size * (coord - 0.5);
float d = abs(coord.y) - abs(coord.x);
if (d >= -0.1 && coord.y < 0.5) {
- return smoothstep(-0.1, 0.1, d);
+ return local_smoothstep(-0.1, 0.1, d);
} else {
return 0.0;
}
@@ -786,6 +802,13 @@ class _Points2D(object):
varying vec4 vColor;
+ /* smoothstep function implementation to support GLSL 1.20 */
+ float local_smoothstep(float edge0, float edge1, float x) {
+ float t;
+ t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+ return t * t * (3.0 - 2.0 * t);
+ }
+
%s
void main(void) {
@@ -800,17 +823,32 @@ class _Points2D(object):
_PROGRAMS = {}
- def __init__(self, xVboData=None, yVboData=None, colorVboData=None,
- marker=SQUARE, color=(0., 0., 0., 1.), size=7,
- offset=(0., 0.)):
+ def __init__(
+ self,
+ xVboData=None,
+ yVboData=None,
+ colorVboData=None,
+ marker=SQUARE,
+ color=(0.0, 0.0, 0.0, 1.0),
+ size=7,
+ offset=(0.0, 0.0),
+ ):
self.color = color
self._marker = None
self.marker = marker
self.size = size
self.offset = offset
+ if xVboData is not None and not isinstance(xVboData, VertexBufferAttrib):
+ xVboData = numpy.array(xVboData, copy=False, dtype=numpy.float32)
self.xVboData = xVboData
+
+ if yVboData is not None and not isinstance(yVboData, VertexBufferAttrib):
+ yVboData = numpy.array(yVboData, copy=False, dtype=numpy.float32)
self.yVboData = yVboData
+
+ if colorVboData is not None:
+ assert isinstance(colorVboData, VertexBufferAttrib)
self.colorVboData = colorVboData
self.useColorVboData = colorVboData is not None
@@ -838,17 +876,19 @@ class _Points2D(object):
if marker not in cls._PROGRAMS:
cls._PROGRAMS[marker] = Program(
vertexShader=cls._VERTEX_SHADER,
- fragmentShader=(cls._FRAGMENT_SHADER_TEMPLATE %
- cls._FRAGMENT_SHADER_SYMBOLS[marker]),
- attrib0='xPos')
+ fragmentShader=(
+ cls._FRAGMENT_SHADER_TEMPLATE % cls._FRAGMENT_SHADER_SYMBOLS[marker]
+ ),
+ attrib0="xPos",
+ )
return cls._PROGRAMS[marker]
@classmethod
def init(cls):
"""OpenGL context initialization"""
- version = gl.glGetString(gl.GL_VERSION)
- majorVersion = int(version[0])
+ version = gl.getVersion()
+ majorVersion = version[0]
assert majorVersion >= 2
gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
@@ -866,9 +906,10 @@ class _Points2D(object):
program = self._getProgram(self.marker)
program.use()
- matrix = numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32)
- gl.glUniformMatrix4fv(program.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
+ matrix = numpy.dot(context.matrix, mat4Translate(*self.offset)).astype(
+ numpy.float32
+ )
+ gl.glUniformMatrix4fv(program.uniforms["matrix"], 1, gl.GL_TRUE, matrix)
if self.marker == PIXEL:
size = 1
@@ -876,17 +917,24 @@ class _Points2D(object):
size = math.ceil(0.5 * self.size) + 1 # Mimic Matplotlib point
else:
size = self.size
- size = size / 72. * context.dpi
-
- if self.marker in (PLUS, H_LINE, V_LINE,
- TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN):
+ size = size / 72.0 * context.dpi
+
+ if self.marker in (
+ PLUS,
+ H_LINE,
+ V_LINE,
+ TICK_LEFT,
+ TICK_RIGHT,
+ TICK_UP,
+ TICK_DOWN,
+ ):
# Convert to nearest odd number
- size = size // 2 * 2 + 1.
+ size = size // 2 * 2 + 1.0
- gl.glUniform1f(program.uniforms['size'], size)
+ gl.glUniform1f(program.uniforms["size"], size)
# gl.glPointSize(self.size)
- cAttrib = program.attributes['color']
+ cAttrib = program.attributes["color"]
if self.useColorVboData and self.colorVboData is not None:
gl.glEnableVertexAttribArray(cAttrib)
self.colorVboData.setVertexAttrib(cAttrib)
@@ -894,21 +942,30 @@ class _Points2D(object):
gl.glDisableVertexAttribArray(cAttrib)
gl.glVertexAttrib4f(cAttrib, *self.color)
- xAttrib = program.attributes['xPos']
- gl.glEnableVertexAttribArray(xAttrib)
- self.xVboData.setVertexAttrib(xAttrib)
+ xPosAttrib = program.attributes["xPos"]
+ gl.glEnableVertexAttribArray(xPosAttrib)
+ if isinstance(self.xVboData, VertexBufferAttrib):
+ self.xVboData.setVertexAttrib(xPosAttrib)
+ else:
+ gl.glVertexAttribPointer(
+ xPosAttrib, 1, gl.GL_FLOAT, False, 0, self.xVboData
+ )
- yAttrib = program.attributes['yPos']
- gl.glEnableVertexAttribArray(yAttrib)
- self.yVboData.setVertexAttrib(yAttrib)
+ yPosAttrib = program.attributes["yPos"]
+ gl.glEnableVertexAttribArray(yPosAttrib)
+ if isinstance(self.yVboData, VertexBufferAttrib):
+ self.yVboData.setVertexAttrib(yPosAttrib)
+ else:
+ gl.glVertexAttribPointer(
+ yPosAttrib, 1, gl.GL_FLOAT, False, 0, self.yVboData
+ )
gl.glDrawArrays(gl.GL_POINTS, 0, self.xVboData.size)
- gl.glUseProgram(0)
-
# error bars ##################################################################
+
class _ErrorBars(object):
"""Display errors bars.
@@ -934,49 +991,58 @@ class _ErrorBars(object):
:param List[float] offset: Translation of coordinates (ox, oy)
"""
- def __init__(self, xData, yData, xError, yError,
- xMin, yMin,
- color=(0., 0., 0., 1.),
- offset=(0., 0.)):
+ def __init__(
+ self,
+ xData,
+ yData,
+ xError,
+ yError,
+ xMin,
+ yMin,
+ color=(0.0, 0.0, 0.0, 1.0),
+ offset=(0.0, 0.0),
+ ):
self._attribs = None
self._xMin, self._yMin = xMin, yMin
self.offset = offset
if xError is not None or yError is not None:
- self._xData = numpy.array(
- xData, order='C', dtype=numpy.float32, copy=False)
- self._yData = numpy.array(
- yData, order='C', dtype=numpy.float32, copy=False)
+ self._xData = numpy.array(xData, order="C", dtype=numpy.float32, copy=False)
+ self._yData = numpy.array(yData, order="C", dtype=numpy.float32, copy=False)
# This also works if xError, yError is a float/int
self._xError = numpy.array(
- xError, order='C', dtype=numpy.float32, copy=False)
+ xError, order="C", dtype=numpy.float32, copy=False
+ )
self._yError = numpy.array(
- yError, order='C', dtype=numpy.float32, copy=False)
+ yError, order="C", dtype=numpy.float32, copy=False
+ )
else:
self._xData, self._yData = None, None
self._xError, self._yError = None, None
self._lines = GLLines2D(
- None, None, color=color, drawMode=gl.GL_LINES, offset=offset)
- self._xErrPoints = _Points2D(
- None, None, color=color, marker=V_LINE, offset=offset)
- self._yErrPoints = _Points2D(
- None, None, color=color, marker=H_LINE, offset=offset)
+ None, None, color=color, drawMode=gl.GL_LINES, offset=offset
+ )
+ self._xErrPoints = Points2D(
+ None, None, color=color, marker=V_LINE, offset=offset
+ )
+ self._yErrPoints = Points2D(
+ None, None, color=color, marker=H_LINE, offset=offset
+ )
def _buildVertices(self):
"""Generates error bars vertices"""
- nbLinesPerDataPts = (0 if self._xError is None else 2) + \
- (0 if self._yError is None else 2)
+ nbLinesPerDataPts = (0 if self._xError is None else 2) + (
+ 0 if self._yError is None else 2
+ )
nbDataPts = len(self._xData)
# interleave coord+error, coord-error.
# xError vertices first if any, then yError vertices if any.
- xCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2,
- dtype=numpy.float32)
- yCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2,
- dtype=numpy.float32)
+ xCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2, dtype=numpy.float32)
+ yCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2, dtype=numpy.float32)
if self._xError is not None: # errors on the X axis
if len(self._xError.shape) == 2:
@@ -988,15 +1054,15 @@ class _ErrorBars(object):
# Interleave vertices for xError
endXError = 4 * nbDataPts
with numpy.errstate(invalid="ignore"):
- xCoords[0:endXError-3:4] = self._xData + xErrorPlus
- xCoords[1:endXError-2:4] = self._xData
- xCoords[2:endXError-1:4] = self._xData
+ xCoords[0 : endXError - 3 : 4] = self._xData + xErrorPlus
+ xCoords[1 : endXError - 2 : 4] = self._xData
+ xCoords[2 : endXError - 1 : 4] = self._xData
with numpy.errstate(invalid="ignore"):
xCoords[3:endXError:4] = self._xData - xErrorMinus
- yCoords[0:endXError-3:4] = self._yData
- yCoords[1:endXError-2:4] = self._yData
- yCoords[2:endXError-1:4] = self._yData
+ yCoords[0 : endXError - 3 : 4] = self._yData
+ yCoords[1 : endXError - 2 : 4] = self._yData
+ yCoords[2 : endXError - 1 : 4] = self._yData
yCoords[3:endXError:4] = self._yData
else:
@@ -1011,16 +1077,16 @@ class _ErrorBars(object):
# Interleave vertices for yError
xCoords[endXError::4] = self._xData
- xCoords[endXError+1::4] = self._xData
- xCoords[endXError+2::4] = self._xData
- xCoords[endXError+3::4] = self._xData
+ xCoords[endXError + 1 :: 4] = self._xData
+ xCoords[endXError + 2 :: 4] = self._xData
+ xCoords[endXError + 3 :: 4] = self._xData
with numpy.errstate(invalid="ignore"):
yCoords[endXError::4] = self._yData + yErrorPlus
- yCoords[endXError+1::4] = self._yData
- yCoords[endXError+2::4] = self._yData
+ yCoords[endXError + 1 :: 4] = self._yData
+ yCoords[endXError + 2 :: 4] = self._yData
with numpy.errstate(invalid="ignore"):
- yCoords[endXError+3::4] = self._yData - yErrorMinus
+ yCoords[endXError + 3 :: 4] = self._yData - yErrorMinus
return xCoords, yCoords
@@ -1047,12 +1113,10 @@ class _ErrorBars(object):
# Set yError points using the same VBO as lines
self._yErrPoints.xVboData = xAttrib.copy()
self._yErrPoints.xVboData.size //= 2
- self._yErrPoints.xVboData.offset += (xAttrib.itemsize *
- xAttrib.size // 2)
+ self._yErrPoints.xVboData.offset += xAttrib.itemsize * xAttrib.size // 2
self._yErrPoints.yVboData = yAttrib.copy()
self._yErrPoints.yVboData.size //= 2
- self._yErrPoints.yVboData.offset += (yAttrib.itemsize *
- yAttrib.size // 2)
+ self._yErrPoints.yVboData.offset += yAttrib.itemsize * yAttrib.size // 2
def render(self, context):
"""Perform rendering
@@ -1081,12 +1145,14 @@ class _ErrorBars(object):
# curves ######################################################################
+
def _proxyProperty(*componentsAttributes):
"""Create a property to access an attribute of attribute(s).
Useful for composition.
Supports multiple components this way:
getter returns the first found, setter sets all
"""
+
def getter(self):
for compName, attrName in componentsAttributes:
try:
@@ -1100,23 +1166,32 @@ def _proxyProperty(*componentsAttributes):
for compName, attrName in componentsAttributes:
component = getattr(self, compName)
setattr(component, attrName, value)
+
return property(getter, setter)
class GLPlotCurve2D(GLPlotItem):
- def __init__(self, xData, yData, colorData=None,
- xError=None, yError=None,
- lineStyle=SOLID,
- lineColor=(0., 0., 0., 1.),
- lineWidth=1,
- lineDashPeriod=20,
- marker=SQUARE,
- markerColor=(0., 0., 0., 1.),
- markerSize=7,
- fillColor=None,
- baseline=None,
- isYLog=False):
+ def __init__(
+ self,
+ xData,
+ yData,
+ colorData=None,
+ xError=None,
+ yError=None,
+ lineColor=(0.0, 0.0, 0.0, 1.0),
+ lineGapColor=None,
+ lineWidth=1,
+ lineDashOffset=0.0,
+ lineDashPattern=(),
+ marker=SQUARE,
+ markerColor=(0.0, 0.0, 0.0, 1.0),
+ markerSize=7,
+ fillColor=None,
+ baseline=None,
+ isYLog=False,
+ ):
super().__init__()
+ self._ratio = None
self.colorData = colorData
# Compute x bounds
@@ -1124,7 +1199,7 @@ class GLPlotCurve2D(GLPlotItem):
self.xMin, self.xMax = min_max(xData, min_positive=False)
else:
# Takes the error into account
- if hasattr(xError, 'shape') and len(xError.shape) == 2:
+ if hasattr(xError, "shape") and len(xError.shape) == 2:
xErrorMinus, xErrorPlus = xError[0], xError[1]
else:
xErrorMinus, xErrorPlus = xError, xError
@@ -1136,7 +1211,7 @@ class GLPlotCurve2D(GLPlotItem):
self.yMin, self.yMax = min_max(yData, min_positive=False)
else:
# Takes the error into account
- if hasattr(yError, 'shape') and len(yError.shape) == 2:
+ if hasattr(yError, "shape") and len(yError.shape) == 2:
yErrorMinus, yErrorPlus = yError[0], yError[1]
else:
yErrorMinus, yErrorPlus = yError, yError
@@ -1152,107 +1227,121 @@ class GLPlotCurve2D(GLPlotItem):
self.yData = (yData - self.offset[1]).astype(numpy.float32)
else: # float32
- self.offset = 0., 0.
+ self.offset = 0.0, 0.0
self.xData = xData
self.yData = yData
if fillColor is not None:
+
def deduce_baseline(baseline):
if baseline is None:
_baseline = 0
else:
_baseline = baseline
if not isinstance(_baseline, numpy.ndarray):
- _baseline = numpy.repeat(_baseline,
- len(self.xData))
+ _baseline = numpy.repeat(_baseline, len(self.xData))
if isYLog is True:
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
log_val = numpy.log10(_baseline)
- _baseline = numpy.where(_baseline>0.0, log_val, -38)
+ _baseline = numpy.where(_baseline > 0.0, log_val, -38)
return _baseline
_baseline = deduce_baseline(baseline)
# Use different baseline depending of Y log scale
- self.fill = _Fill2D(self.xData, self.yData,
- baseline=_baseline,
- color=fillColor,
- offset=self.offset)
+ self.fill = _Fill2D(
+ self.xData,
+ self.yData,
+ baseline=_baseline,
+ color=fillColor,
+ offset=self.offset,
+ )
else:
self.fill = None
- self._errorBars = _ErrorBars(self.xData, self.yData,
- xError, yError,
- self.xMin, self.yMin,
- offset=self.offset)
+ self._errorBars = _ErrorBars(
+ self.xData,
+ self.yData,
+ xError,
+ yError,
+ self.xMin,
+ self.yMin,
+ offset=self.offset,
+ )
self.lines = GLLines2D()
- self.lines.style = lineStyle
self.lines.color = lineColor
+ self.lines.gapColor = lineGapColor
self.lines.width = lineWidth
- self.lines.dashPeriod = lineDashPeriod
+ self.lines.dashOffset = lineDashOffset
+ self.lines.dashPattern = lineDashPattern
self.lines.offset = self.offset
- self.points = _Points2D()
+ self.points = Points2D()
self.points.marker = marker
self.points.color = markerColor
self.points.size = markerSize
self.points.offset = self.offset
- xVboData = _proxyProperty(('lines', 'xVboData'), ('points', 'xVboData'))
+ xVboData = _proxyProperty(("lines", "xVboData"), ("points", "xVboData"))
+
+ yVboData = _proxyProperty(("lines", "yVboData"), ("points", "yVboData"))
- yVboData = _proxyProperty(('lines', 'yVboData'), ('points', 'yVboData'))
+ colorVboData = _proxyProperty(("lines", "colorVboData"), ("points", "colorVboData"))
- colorVboData = _proxyProperty(('lines', 'colorVboData'),
- ('points', 'colorVboData'))
+ useColorVboData = _proxyProperty(
+ ("lines", "useColorVboData"), ("points", "useColorVboData")
+ )
- useColorVboData = _proxyProperty(('lines', 'useColorVboData'),
- ('points', 'useColorVboData'))
+ distVboData = _proxyProperty(("lines", "distVboData"))
- distVboData = _proxyProperty(('lines', 'distVboData'))
+ lineColor = _proxyProperty(("lines", "color"))
- lineStyle = _proxyProperty(('lines', 'style'))
+ lineGapColor = _proxyProperty(("lines", "gapColor"))
- lineColor = _proxyProperty(('lines', 'color'))
+ lineWidth = _proxyProperty(("lines", "width"))
- lineWidth = _proxyProperty(('lines', 'width'))
+ lineDashOffset = _proxyProperty(("lines", "dashOffset"))
- lineDashPeriod = _proxyProperty(('lines', 'dashPeriod'))
+ lineDashPattern = _proxyProperty(("lines", "dashPattern"))
- marker = _proxyProperty(('points', 'marker'))
+ marker = _proxyProperty(("points", "marker"))
- markerColor = _proxyProperty(('points', 'color'))
+ markerColor = _proxyProperty(("points", "color"))
- markerSize = _proxyProperty(('points', 'size'))
+ markerSize = _proxyProperty(("points", "size"))
@classmethod
def init(cls):
"""OpenGL context initialization"""
GLLines2D.init()
- _Points2D.init()
+ Points2D.init()
def prepare(self):
"""Rendering preparation: build indices and bounding box vertices"""
if self.xVboData is None:
xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None
- if self.lineStyle in (DASHED, DASHDOT, DOTTED):
- dists = distancesFromArrays(self.xData, self.yData)
+ if self.lineDashPattern:
+ dists = distancesFromArrays(self.xData, self.yData, self._ratio)
if self.colorData is None:
xAttrib, yAttrib, dAttrib = vertexBuffer(
- (self.xData, self.yData, dists))
+ (self.xData, self.yData, dists)
+ )
else:
xAttrib, yAttrib, cAttrib, dAttrib = vertexBuffer(
- (self.xData, self.yData, self.colorData, dists))
+ (self.xData, self.yData, self.colorData, dists)
+ )
elif self.colorData is None:
xAttrib, yAttrib = vertexBuffer((self.xData, self.yData))
else:
xAttrib, yAttrib, cAttrib = vertexBuffer(
- (self.xData, self.yData, self.colorData))
+ (self.xData, self.yData, self.colorData)
+ )
self.xVboData = xAttrib
self.yVboData = yAttrib
self.distVboData = dAttrib
- if cAttrib is not None and self.colorData.dtype.kind == 'u':
+ if cAttrib is not None and self.colorData.dtype.kind == "u":
cAttrib.normalization = True # Normalize uint to [0, 1]
self.colorVboData = cAttrib
self.useColorVboData = cAttrib is not None
@@ -1262,6 +1351,21 @@ class GLPlotCurve2D(GLPlotItem):
:param RenderContext context: Rendering information
"""
+ if self.lineDashPattern:
+ visibleRanges = context.plotFrame.transformedDataRanges
+ xLimits = visibleRanges.x
+ yLimits = visibleRanges.y if self.yaxis == "left" else visibleRanges.y2
+ width, height = context.plotFrame.plotSize
+ ratio = (height * (xLimits[1] - xLimits[0])) / (
+ width * (yLimits[1] - yLimits[0])
+ )
+ if (
+ self._ratio is None or abs(1.0 - ratio / self._ratio) > 0.05
+ ): # Tolerate 5% difference
+ # Rebuild curve buffers to update distances
+ self._ratio = ratio
+ self.discard()
+
self.prepare()
if self.fill is not None:
self.fill.render(context)
@@ -1284,9 +1388,11 @@ class GLPlotCurve2D(GLPlotItem):
self.fill.discard()
def isInitialized(self):
- return (self.xVboData is not None or
- self._errorBars.isInitialized() or
- (self.fill is not None and self.fill.isInitialized()))
+ return (
+ self.xVboData is not None
+ or self._errorBars.isInitialized()
+ or (self.fill is not None and self.fill.isInitialized())
+ )
def pick(self, xPickMin, yPickMin, xPickMax, yPickMax):
"""Perform picking on the curve according to its rendering.
@@ -1301,9 +1407,13 @@ class GLPlotCurve2D(GLPlotItem):
:return: The indices of the picked data
:rtype: Union[List[int],None]
"""
- if (self.marker is None and self.lineStyle is None) or \
- self.xMin > xPickMax or xPickMin > self.xMax or \
- self.yMin > yPickMax or yPickMin > self.yMax:
+ if (
+ (self.marker is None and self.lineDashPattern is None)
+ or self.xMin > xPickMax
+ or xPickMin > self.xMax
+ or self.yMin > yPickMax
+ or yPickMin > self.yMax
+ ):
return None
# offset picking bounds
@@ -1312,25 +1422,27 @@ class GLPlotCurve2D(GLPlotItem):
yPickMin = yPickMin - self.offset[1]
yPickMax = yPickMax - self.offset[1]
- if self.lineStyle is not None:
+ if self.lineDashPattern is not None:
# Using Cohen-Sutherland algorithm for line clipping
- with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings
- codes = ((self.yData > yPickMax) << 3) | \
- ((self.yData < yPickMin) << 2) | \
- ((self.xData > xPickMax) << 1) | \
- (self.xData < xPickMin)
-
- notNaN = numpy.logical_not(numpy.logical_or(
- numpy.isnan(self.xData), numpy.isnan(self.yData)))
+ with numpy.errstate(invalid="ignore"): # Ignore NaN comparison warnings
+ codes = (
+ ((self.yData > yPickMax) << 3)
+ | ((self.yData < yPickMin) << 2)
+ | ((self.xData > xPickMax) << 1)
+ | (self.xData < xPickMin)
+ )
+
+ notNaN = numpy.logical_not(
+ numpy.logical_or(numpy.isnan(self.xData), numpy.isnan(self.yData))
+ )
# Add all points that are inside the picking area
- indices = numpy.nonzero(
- numpy.logical_and(codes == 0, notNaN))[0].tolist()
+ indices = numpy.nonzero(numpy.logical_and(codes == 0, notNaN))[0].tolist()
# Segment that might cross the area with no end point inside it
- segToTestIdx = numpy.nonzero((codes[:-1] != 0) &
- (codes[1:] != 0) &
- ((codes[:-1] & codes[1:]) == 0))[0]
+ segToTestIdx = numpy.nonzero(
+ (codes[:-1] != 0) & (codes[1:] != 0) & ((codes[:-1] & codes[1:]) == 0)
+ )[0]
TOP, BOTTOM, RIGHT, LEFT = (1 << 3), (1 << 2), (1 << 1), (1 << 0)
@@ -1371,10 +1483,12 @@ class GLPlotCurve2D(GLPlotItem):
indices.sort()
else:
- with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings
- indices = numpy.nonzero((self.xData >= xPickMin) &
- (self.xData <= xPickMax) &
- (self.yData >= yPickMin) &
- (self.yData <= yPickMax))[0].tolist()
+ with numpy.errstate(invalid="ignore"): # Ignore NaN comparison warnings
+ indices = numpy.nonzero(
+ (self.xData >= xPickMin)
+ & (self.xData <= xPickMax)
+ & (self.yData >= yPickMin)
+ & (self.yData <= yPickMax)
+ )[0].tolist()
return tuple(indices) if len(indices) > 0 else None
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotFrame.py b/src/silx/gui/plot/backends/glutils/GLPlotFrame.py
index 1fccb02..42cfa50 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotFrame.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotFrame.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,6 +25,8 @@
This modules provides the rendering of plot titles, axes and grid.
"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "03/04/2017"
@@ -39,16 +40,25 @@ import datetime as dt
import math
import weakref
import logging
+import numbers
+from typing import Optional, Union
from collections import namedtuple
import numpy
+from .... import qt
from ...._glutils import gl, Program
+from ....utils.matplotlib import DefaultTickFormatter
from ..._utils import checkAxisLimits, FLOAT32_MINPOS
from .GLSupport import mat4Ortho
from .GLText import Text2D, CENTER, BOTTOM, TOP, LEFT, RIGHT, ROTATE_270
from ..._utils.ticklayout import niceNumbersAdaptative, niceNumbersForLog10
-from ..._utils.dtime_ticklayout import calcTicksAdaptive, bestFormatString
+from ..._utils.dtime_ticklayout import (
+ DtUnit,
+ bestUnit,
+ calcTicksAdaptive,
+ formatDatetimes,
+)
from ..._utils.dtime_ticklayout import timestamp
_logger = logging.getLogger(__name__)
@@ -56,36 +66,52 @@ _logger = logging.getLogger(__name__)
# PlotAxis ####################################################################
+
class PlotAxis(object):
"""Represents a 1D axis of the plot.
This class is intended to be used with :class:`GLPlotFrame`.
"""
- def __init__(self, plotFrame,
- tickLength=(0., 0.),
- foregroundColor=(0., 0., 0., 1.0),
- labelAlign=CENTER, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=CENTER,
- titleRotate=0, titleOffset=(0., 0.)):
+ def __init__(
+ self,
+ plotFrame,
+ tickLength=(0.0, 0.0),
+ foregroundColor=(0.0, 0.0, 0.0, 1.0),
+ labelAlign=CENTER,
+ labelVAlign=CENTER,
+ titleAlign=CENTER,
+ titleVAlign=CENTER,
+ orderOffsetAlign=CENTER,
+ orderOffsetVAlign=CENTER,
+ titleRotate=0,
+ titleOffset=(0.0, 0.0),
+ font: qt.QFont | None = None,
+ ):
+ self._tickFormatter = DefaultTickFormatter()
self._ticks = None
+ self._orderAndOffsetText = ""
self._plotFrameRef = weakref.ref(plotFrame)
self._isDateTime = False
self._timeZone = None
self._isLog = False
- self._dataRange = 1., 100.
- self._displayCoords = (0., 0.), (1., 0.)
- self._title = ''
+ self._dataRange = 1.0, 100.0
+ self._displayCoords = (0.0, 0.0), (1.0, 0.0)
+ self._title = ""
self._tickLength = tickLength
self._foregroundColor = foregroundColor
self._labelAlign = labelAlign
self._labelVAlign = labelVAlign
+ self._orderOffetAnchor = (1.0, 0.0)
+ self._orderOffsetAlign = orderOffsetAlign
+ self._orderOffsetVAlign = orderOffsetVAlign
self._titleAlign = titleAlign
self._titleVAlign = titleVAlign
self._titleRotate = titleRotate
self._titleOffset = titleOffset
+ self._font = font
@property
def dataRange(self):
@@ -93,6 +119,12 @@ class PlotAxis(object):
of 2 floats: (min, max)."""
return self._dataRange
+ @property
+ def font(self) -> qt.QFont:
+ if self._font is None:
+ return qt.QApplication.instance().font()
+ return self._font
+
@dataRange.setter
def dataRange(self, dataRange):
assert len(dataRange) == 2
@@ -160,7 +192,13 @@ class PlotAxis(object):
def devicePixelRatio(self):
"""Returns the ratio between qt pixels and device pixels."""
plotFrame = self._plotFrameRef()
- return plotFrame.devicePixelRatio if plotFrame is not None else 1.
+ return plotFrame.devicePixelRatio if plotFrame is not None else 1.0
+
+ @property
+ def dotsPerInch(self):
+ """Returns the screen DPI"""
+ plotFrame = self._plotFrameRef()
+ return plotFrame.dotsPerInch if plotFrame is not None else 92
@property
def title(self):
@@ -174,6 +212,17 @@ class PlotAxis(object):
self._dirtyPlotFrame()
@property
+ def orderOffetAnchor(self) -> tuple[float, float]:
+ """Anchor position for the tick order&offset text"""
+ return self._orderOffetAnchor
+
+ @orderOffetAnchor.setter
+ def orderOffetAnchor(self, position: tuple[float, float]):
+ if position != self._orderOffetAnchor:
+ self._orderOffetAnchor = position
+ self._dirtyTicks()
+
+ @property
def titleOffset(self):
"""Title offset in pixels (x: int, y: int)"""
return self._titleOffset
@@ -192,8 +241,9 @@ class PlotAxis(object):
@foregroundColor.setter
def foregroundColor(self, color):
"""Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ assert len(color) == 4, "foregroundColor must have length 4, got {}".format(
+ len(self._foregroundColor)
+ )
if self._foregroundColor != color:
self._foregroundColor = color
self._dirtyTicks()
@@ -212,7 +262,6 @@ class PlotAxis(object):
"""
vertices = list(self.displayCoords) # Add start and end points
labels = []
- tickLabelsSize = [0., 0.]
xTickLength, yTickLength = self._tickLength
xTickLength *= self.devicePixelRatio
@@ -221,27 +270,24 @@ class PlotAxis(object):
if text is None:
tickScale = 0.5
else:
- tickScale = 1.
-
- label = Text2D(text=text,
- color=self._foregroundColor,
- x=xPixel - xTickLength,
- y=yPixel - yTickLength,
- align=self._labelAlign,
- valign=self._labelVAlign,
- devicePixelRatio=self.devicePixelRatio)
-
- width, height = label.size
- if width > tickLabelsSize[0]:
- tickLabelsSize[0] = width
- if height > tickLabelsSize[1]:
- tickLabelsSize[1] = height
-
+ tickScale = 1.0
+
+ label = Text2D(
+ text=text,
+ font=self.font,
+ color=self._foregroundColor,
+ x=xPixel - xTickLength,
+ y=yPixel - yTickLength,
+ align=self._labelAlign,
+ valign=self._labelVAlign,
+ devicePixelRatio=self.devicePixelRatio,
+ )
labels.append(label)
vertices.append((xPixel, yPixel))
- vertices.append((xPixel + tickScale * xTickLength,
- yPixel + tickScale * yTickLength))
+ vertices.append(
+ (xPixel + tickScale * xTickLength, yPixel + tickScale * yTickLength)
+ )
(x0, y0), (x1, y1) = self.displayCoords
xAxisCenter = 0.5 * (x0 + x1)
@@ -256,16 +302,33 @@ class PlotAxis(object):
# yOffset = -tickLabelsSize[1] * yTickLength / tickNorm
# yOffset -= 3 * yTickLength
- axisTitle = Text2D(text=self.title,
- color=self._foregroundColor,
- x=xAxisCenter + xOffset,
- y=yAxisCenter + yOffset,
- align=self._titleAlign,
- valign=self._titleVAlign,
- rotate=self._titleRotate,
- devicePixelRatio=self.devicePixelRatio)
+ axisTitle = Text2D(
+ text=self.title,
+ font=self.font,
+ color=self._foregroundColor,
+ x=xAxisCenter + xOffset,
+ y=yAxisCenter + yOffset,
+ align=self._titleAlign,
+ valign=self._titleVAlign,
+ rotate=self._titleRotate,
+ devicePixelRatio=self.devicePixelRatio,
+ )
labels.append(axisTitle)
+ if self._orderAndOffsetText:
+ xOrderOffset, yOrderOffet = self.orderOffetAnchor
+ labels.append(
+ Text2D(
+ text=self._orderAndOffsetText,
+ font=self.font,
+ color=self._foregroundColor,
+ x=xOrderOffset,
+ y=yOrderOffet,
+ align=self._orderOffsetAlign,
+ valign=self._orderOffsetVAlign,
+ devicePixelRatio=self.devicePixelRatio,
+ )
+ )
return vertices, labels
def _dirtyPlotFrame(self):
@@ -290,19 +353,19 @@ class PlotAxis(object):
"""Generator of ticks as tuples:
((x, y) in display, dataPos, textLabel).
"""
+ self._orderAndOffsetText = ""
+
dataMin, dataMax = self.dataRange
- if self.isLog and dataMin <= 0.:
- _logger.warning(
- 'Getting ticks while isLog=True and dataRange[0]<=0.')
- dataMin = 1.
+ if self.isLog and dataMin <= 0.0:
+ _logger.warning("Getting ticks while isLog=True and dataRange[0]<=0.")
+ dataMin = 1.0
if dataMax < dataMin:
- dataMax = 1.
+ dataMax = 1.0
if dataMin != dataMax: # data range is not null
(x0, y0), (x1, y1) = self.displayCoords
if self.isLog:
-
if self.isTimeSeries:
_logger.warning("Time series not implemented for log-scale")
@@ -314,16 +377,16 @@ class PlotAxis(object):
for logPos in self._frange(tickMin, tickMax, step):
if logMin <= logPos <= logMax:
- dataPos = 10 ** logPos
+ dataPos = 10**logPos
xPixel = x0 + (logPos - logMin) * xScale
yPixel = y0 + (logPos - logMin) * yScale
- text = '1e%+03d' % logPos
+ text = "1e%+03d" % logPos
yield ((xPixel, yPixel), dataPos, text)
if step == 1:
ticks = list(self._frange(tickMin, tickMax, step))[:-1]
for logPos in ticks:
- dataOrigPos = 10 ** logPos
+ dataOrigPos = 10**logPos
for index in range(2, 10):
dataPos = dataOrigPos * index
if dataMin <= dataPos <= dataMax:
@@ -336,49 +399,67 @@ class PlotAxis(object):
xScale = (x1 - x0) / (dataMax - dataMin)
yScale = (y1 - y0) / (dataMax - dataMin)
- nbPixels = math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) / self.devicePixelRatio
+ nbPixels = (
+ math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) / self.devicePixelRatio
+ )
# Density of 1.3 label per 92 pixels
# i.e., 1.3 label per inch on a 92 dpi screen
- tickDensity = 1.3 / 92
+ tickDensity = 1.3 * self.devicePixelRatio / self.dotsPerInch
if not self.isTimeSeries:
- tickMin, tickMax, step, nbFrac = niceNumbersAdaptative(
- dataMin, dataMax, nbPixels, tickDensity)
-
- for dataPos in self._frange(tickMin, tickMax, step):
- if dataMin <= dataPos <= dataMax:
- xPixel = x0 + (dataPos - dataMin) * xScale
- yPixel = y0 + (dataPos - dataMin) * yScale
-
- if nbFrac == 0:
- text = '%g' % dataPos
- else:
- text = ('%.' + str(nbFrac) + 'f') % dataPos
- yield ((xPixel, yPixel), dataPos, text)
+ tickMin, tickMax, step, _ = niceNumbersAdaptative(
+ dataMin, dataMax, nbPixels, tickDensity
+ )
+
+ visibleTickPositions = [
+ pos
+ for pos in self._frange(tickMin, tickMax, step)
+ if dataMin <= pos <= dataMax
+ ]
+ self._tickFormatter.axis.set_view_interval(dataMin, dataMax)
+ self._tickFormatter.axis.set_data_interval(dataMin, dataMax)
+ texts = self._tickFormatter.format_ticks(visibleTickPositions)
+ self._orderAndOffsetText = self._tickFormatter.get_offset()
+
+ for dataPos, text in zip(visibleTickPositions, texts):
+ xPixel = x0 + (dataPos - dataMin) * xScale
+ yPixel = y0 + (dataPos - dataMin) * yScale
+ yield ((xPixel, yPixel), dataPos, text)
+
else:
# Time series
- dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone)
- dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone)
+ try:
+ dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone)
+ dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone)
+ except ValueError:
+ _logger.warning("Data range cannot be displayed with time axis")
+ return # Range is out of bound of the datetime
+
+ if bestUnit(
+ (dtMax - dtMin).total_seconds() == DtUnit.MICRO_SECONDS
+ ):
+ # Special case for micro seconds: Reduce tick density
+ tickDensity = 1.0 * self.devicePixelRatio / self.dotsPerInch
tickDateTimes, spacing, unit = calcTicksAdaptive(
- dtMin, dtMax, nbPixels, tickDensity)
-
- for tickDateTime in tickDateTimes:
- if dtMin <= tickDateTime <= dtMax:
-
- dataPos = timestamp(tickDateTime)
- xPixel = x0 + (dataPos - dataMin) * xScale
- yPixel = y0 + (dataPos - dataMin) * yScale
-
- fmtStr = bestFormatString(spacing, unit)
- text = tickDateTime.strftime(fmtStr)
-
- yield ((xPixel, yPixel), dataPos, text)
+ dtMin, dtMax, nbPixels, tickDensity
+ )
+ visibleDatetimes = tuple(
+ dt for dt in tickDateTimes if dtMin <= dt <= dtMax
+ )
+ ticks = formatDatetimes(visibleDatetimes, spacing, unit)
+
+ for tickDateTime, text in ticks.items():
+ dataPos = timestamp(tickDateTime)
+ xPixel = x0 + (dataPos - dataMin) * xScale
+ yPixel = y0 + (dataPos - dataMin) * yScale
+ yield ((xPixel, yPixel), dataPos, text)
# GLPlotFrame #################################################################
+
class GLPlotFrame(object):
"""Base class for rendering a 2D frame surrounded by axes."""
@@ -386,7 +467,7 @@ class GLPlotFrame(object):
_LINE_WIDTH = 1
_SHADERS = {
- 'vertex': """
+ "vertex": """
attribute vec2 position;
uniform mat4 matrix;
@@ -394,7 +475,7 @@ class GLPlotFrame(object):
gl_Position = matrix * vec4(position, 0.0, 1.0);
}
""",
- 'fragment': """
+ "fragment": """
uniform vec4 color;
uniform float tickFactor; /* = 1./tickLength or 0. for solid line */
@@ -405,15 +486,15 @@ class GLPlotFrame(object):
discard;
}
}
- """
+ """,
}
- _Margins = namedtuple('Margins', ('left', 'right', 'top', 'bottom'))
+ _Margins = namedtuple("Margins", ("left", "right", "top", "bottom"))
# Margins used when plot frame is not displayed
_NoDisplayMargins = _Margins(0, 0, 0, 0)
- def __init__(self, marginRatios, foregroundColor, gridColor):
+ def __init__(self, marginRatios, foregroundColor, gridColor, font: qt.QFont):
"""
:param List[float] marginRatios:
The ratios of margins around plot area for axis and labels.
@@ -422,6 +503,7 @@ class GLPlotFrame(object):
:type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0
:param gridColor: color used for grid lines.
:type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0
+ :param font: Font used by the axes label
"""
self._renderResources = None
@@ -434,10 +516,12 @@ class GLPlotFrame(object):
self.axes = [] # List of PlotAxis to be updated by subclasses
self._grid = False
- self._size = 0., 0.
- self._title = ''
+ self._size = 0.0, 0.0
+ self._title = ""
+ self._font: qt.QFont = font
- self._devicePixelRatio = 1.
+ self._devicePixelRatio = 1.0
+ self._dpi = 92
@property
def isDirty(self):
@@ -447,18 +531,19 @@ class GLPlotFrame(object):
GRID_NONE = 0
GRID_MAIN_TICKS = 1
GRID_SUB_TICKS = 2
- GRID_ALL_TICKS = (GRID_MAIN_TICKS + GRID_SUB_TICKS)
+ GRID_ALL_TICKS = GRID_MAIN_TICKS + GRID_SUB_TICKS
@property
def foregroundColor(self):
"""Color used for frame and labels"""
return self._foregroundColor
-
+
@foregroundColor.setter
def foregroundColor(self, color):
"""Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ assert len(color) == 4, "foregroundColor must have length 4, got {}".format(
+ len(self._foregroundColor)
+ )
if self._foregroundColor != color:
self._foregroundColor = color
for axis in self.axes:
@@ -469,20 +554,20 @@ class GLPlotFrame(object):
def gridColor(self):
"""Color used for frame and labels"""
return self._gridColor
-
+
@gridColor.setter
def gridColor(self, color):
"""Color used for frame and labels"""
- assert len(color) == 4, \
- "gridColor must have length 4, got {}".format(len(self._gridColor))
+ assert len(color) == 4, "gridColor must have length 4, got {}".format(
+ len(self._gridColor)
+ )
if self._gridColor != color:
self._gridColor = color
self._dirty()
@property
def marginRatios(self):
- """Plot margin ratios: (left, top, right, bottom) as 4 float in [0, 1].
- """
+ """Plot margin ratios: (left, top, right, bottom) as 4 float in [0, 1]."""
return self.__marginRatios
@marginRatios.setter
@@ -490,9 +575,9 @@ class GLPlotFrame(object):
ratios = tuple(float(v) for v in ratios)
assert len(ratios) == 4
for value in ratios:
- assert 0. <= value <= 1.
- assert ratios[0] + ratios[2] < 1.
- assert ratios[1] + ratios[3] < 1.
+ assert 0.0 <= value <= 1.0
+ assert ratios[0] + ratios[2] < 1.0
+ assert ratios[1] + ratios[3] < 1.0
if self.__marginRatios != ratios:
self.__marginRatios = ratios
@@ -506,10 +591,11 @@ class GLPlotFrame(object):
width, height = self.size
left, top, right, bottom = self.marginRatios
self.__marginsCache = self._Margins(
- left=int(left*width),
- right=int(right*width),
- top=int(top*height),
- bottom=int(bottom*height))
+ left=int(left * width),
+ right=int(right * width),
+ top=int(top * height),
+ bottom=int(bottom * height),
+ )
return self.__marginsCache
@property
@@ -523,6 +609,16 @@ class GLPlotFrame(object):
self._dirty()
@property
+ def dotsPerInch(self):
+ return self._dpi
+
+ @dotsPerInch.setter
+ def dotsPerInch(self, dpi):
+ if dpi != self._dpi:
+ self._dpi = dpi
+ self._dirty()
+
+ @property
def grid(self):
"""Grid display mode:
- 0: No grid.
@@ -533,8 +629,12 @@ class GLPlotFrame(object):
@grid.setter
def grid(self, grid):
- assert grid in (self.GRID_NONE, self.GRID_MAIN_TICKS,
- self.GRID_SUB_TICKS, self.GRID_ALL_TICKS)
+ assert grid in (
+ self.GRID_NONE,
+ self.GRID_MAIN_TICKS,
+ self.GRID_SUB_TICKS,
+ self.GRID_ALL_TICKS,
+ )
if grid != self._grid:
self._grid = grid
self._dirty()
@@ -590,16 +690,22 @@ class GLPlotFrame(object):
return []
elif self._grid == self.GRID_MAIN_TICKS:
+
def test(text):
return text is not None
+
elif self._grid == self.GRID_SUB_TICKS:
+
def test(text):
return text is None
+
elif self._grid == self.GRID_ALL_TICKS:
+
def test(_):
return True
+
else:
- logging.warning('Wrong grid mode: %d' % self._grid)
+ logging.warning("Wrong grid mode: %d" % self._grid)
return []
return self._buildGridVerticesWithTest(test)
@@ -621,25 +727,27 @@ class GLPlotFrame(object):
vertices = numpy.array(vertices, dtype=numpy.float32)
# Add main title
- xTitle = (self.size[0] + self.margins.left -
- self.margins.right) // 2
+ xTitle = (self.size[0] + self.margins.left - self.margins.right) // 2
yTitle = self.margins.top - self._TICK_LENGTH_IN_PIXELS
- labels.append(Text2D(text=self.title,
- color=self._foregroundColor,
- x=xTitle,
- y=yTitle,
- align=CENTER,
- valign=BOTTOM,
- devicePixelRatio=self.devicePixelRatio))
+ labels.append(
+ Text2D(
+ text=self.title,
+ font=self._font,
+ color=self._foregroundColor,
+ x=xTitle,
+ y=yTitle,
+ align=CENTER,
+ valign=BOTTOM,
+ devicePixelRatio=self.devicePixelRatio,
+ )
+ )
# grid
- gridVertices = numpy.array(self._buildGridVertices(),
- dtype=numpy.float32)
+ gridVertices = numpy.array(self._buildGridVertices(), dtype=numpy.float32)
self._renderResources = (vertices, gridVertices, labels)
- _program = Program(
- _SHADERS['vertex'], _SHADERS['fragment'], attrib0='position')
+ _program = Program(_SHADERS["vertex"], _SHADERS["fragment"], attrib0="position")
def render(self):
if self.margins == self._NoDisplayMargins:
@@ -659,22 +767,21 @@ class GLPlotFrame(object):
gl.glLineWidth(self._LINE_WIDTH)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matProj.astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], *self._foregroundColor)
- gl.glUniform1f(prog.uniforms['tickFactor'], 0.)
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, matProj.astype(numpy.float32)
+ )
+ gl.glUniform4f(prog.uniforms["color"], *self._foregroundColor)
+ gl.glUniform1f(prog.uniforms["tickFactor"], 0.0)
- gl.glEnableVertexAttribArray(prog.attributes['position'])
- gl.glVertexAttribPointer(prog.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, vertices)
+ gl.glEnableVertexAttribArray(prog.attributes["position"])
+ gl.glVertexAttribPointer(
+ prog.attributes["position"], 2, gl.GL_FLOAT, gl.GL_FALSE, 0, vertices
+ )
gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))
for label in labels:
- label.render(matProj)
+ label.render(matProj, self.dotsPerInch)
def renderGrid(self):
if self._grid == self.GRID_NONE:
@@ -693,25 +800,25 @@ class GLPlotFrame(object):
prog.use()
gl.glLineWidth(self._LINE_WIDTH)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matProj.astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], *self._gridColor)
- gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen
-
- gl.glEnableVertexAttribArray(prog.attributes['position'])
- gl.glVertexAttribPointer(prog.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, gridVertices)
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, matProj.astype(numpy.float32)
+ )
+ gl.glUniform4f(prog.uniforms["color"], *self._gridColor)
+ gl.glUniform1f(prog.uniforms["tickFactor"], 0.0) # 1/2.) # 1/tickLen
+
+ gl.glEnableVertexAttribArray(prog.attributes["position"])
+ gl.glVertexAttribPointer(
+ prog.attributes["position"], 2, gl.GL_FLOAT, gl.GL_FALSE, 0, gridVertices
+ )
gl.glDrawArrays(gl.GL_LINES, 0, len(gridVertices))
# GLPlotFrame2D ###############################################################
+
class GLPlotFrame2D(GLPlotFrame):
- def __init__(self, marginRatios, foregroundColor, gridColor):
+ def __init__(self, marginRatios, foregroundColor, gridColor, font: qt.QFont):
"""
:param List[float] marginRatios:
The ratios of margins around plot area for axis and labels.
@@ -720,38 +827,66 @@ class GLPlotFrame2D(GLPlotFrame):
:type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0
:param gridColor: color used for grid lines.
:type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0
-
+ :param font: Font used by the axes label
"""
- super(GLPlotFrame2D, self).__init__(marginRatios, foregroundColor, gridColor)
- self.axes.append(PlotAxis(self,
- tickLength=(0., -5.),
- foregroundColor=self._foregroundColor,
- labelAlign=CENTER, labelVAlign=TOP,
- titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=0))
+ super(GLPlotFrame2D, self).__init__(
+ marginRatios, foregroundColor, gridColor, font
+ )
+ self._font = font
+
+ self.axes.append(
+ PlotAxis(
+ self,
+ tickLength=(0.0, -5.0),
+ foregroundColor=self._foregroundColor,
+ labelAlign=CENTER,
+ labelVAlign=TOP,
+ orderOffsetAlign=RIGHT,
+ orderOffsetVAlign=TOP,
+ titleAlign=CENTER,
+ titleVAlign=TOP,
+ titleRotate=0,
+ font=self._font,
+ )
+ )
self._x2AxisCoords = ()
- self.axes.append(PlotAxis(self,
- tickLength=(5., 0.),
- foregroundColor=self._foregroundColor,
- labelAlign=RIGHT, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=BOTTOM,
- titleRotate=ROTATE_270))
-
- self._y2Axis = PlotAxis(self,
- tickLength=(-5., 0.),
- foregroundColor=self._foregroundColor,
- labelAlign=LEFT, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=ROTATE_270)
+ self.axes.append(
+ PlotAxis(
+ self,
+ tickLength=(5.0, 0.0),
+ foregroundColor=self._foregroundColor,
+ labelAlign=RIGHT,
+ labelVAlign=CENTER,
+ orderOffsetAlign=LEFT,
+ orderOffsetVAlign=BOTTOM,
+ titleAlign=CENTER,
+ titleVAlign=BOTTOM,
+ titleRotate=ROTATE_270,
+ font=self._font,
+ )
+ )
+
+ self._y2Axis = PlotAxis(
+ self,
+ tickLength=(-5.0, 0.0),
+ foregroundColor=self._foregroundColor,
+ labelAlign=LEFT,
+ labelVAlign=CENTER,
+ orderOffsetAlign=RIGHT,
+ orderOffsetVAlign=BOTTOM,
+ titleAlign=CENTER,
+ titleVAlign=TOP,
+ titleRotate=ROTATE_270,
+ font=self._font,
+ )
self._isYAxisInverted = False
- self._dataRanges = {
- 'x': (1., 100.), 'y': (1., 100.), 'y2': (1., 100.)}
+ self._dataRanges = {"x": (1.0, 100.0), "y": (1.0, 100.0), "y2": (1.0, 100.0)}
- self._baseVectors = (1., 0.), (0., 1.)
+ self._baseVectors = (1.0, 0.0), (0.0, 1.0)
self._transformedDataRanges = None
self._transformedDataProjMat = None
@@ -766,10 +901,12 @@ class GLPlotFrame2D(GLPlotFrame):
@property
def isDirty(self):
"""True if it need to refresh graphic rendering, False otherwise."""
- return (super(GLPlotFrame2D, self).isDirty or
- self._transformedDataRanges is None or
- self._transformedDataProjMat is None or
- self._transformedDataY2ProjMat is None)
+ return (
+ super(GLPlotFrame2D, self).isDirty
+ or self._transformedDataRanges is None
+ or self._transformedDataProjMat is None
+ or self._transformedDataY2ProjMat is None
+ )
@property
def xAxis(self):
@@ -810,7 +947,7 @@ class GLPlotFrame2D(GLPlotFrame):
self._isYAxisInverted = value
self._dirty()
- DEFAULT_BASE_VECTORS = (1., 0.), (0., 1.)
+ DEFAULT_BASE_VECTORS = (1.0, 0.0), (0.0, 1.0)
"""Values of baseVectors for orthogonal axes."""
@property
@@ -830,10 +967,9 @@ class GLPlotFrame2D(GLPlotFrame):
(xx, xy), (yx, yy) = baseVectors
vectors = (float(xx), float(xy)), (float(yx), float(yy))
- det = (vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1])
- if det == 0.:
- raise ValueError("Singular matrix for base vectors: " +
- str(vectors))
+ det = vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1]
+ if det == 0.0:
+ raise ValueError("Singular matrix for base vectors: " + str(vectors))
if vectors != self._baseVectors:
self._baseVectors = vectors
@@ -865,9 +1001,9 @@ class GLPlotFrame2D(GLPlotFrame):
Type: ((xMin, xMax), (yMin, yMax), (y2Min, y2Max))
"""
- return self._DataRanges(self._dataRanges['x'],
- self._dataRanges['y'],
- self._dataRanges['y2'])
+ return self._DataRanges(
+ self._dataRanges["x"], self._dataRanges["y"], self._dataRanges["y2"]
+ )
def setDataRanges(self, x=None, y=None, y2=None):
"""Set data range over each axes.
@@ -880,22 +1016,25 @@ class GLPlotFrame2D(GLPlotFrame):
:param y2: (min, max) data range over Y2 axis
"""
if x is not None:
- self._dataRanges['x'] = checkAxisLimits(
- x[0], x[1], self.xAxis.isLog, name='x')
+ self._dataRanges["x"] = checkAxisLimits(
+ x[0], x[1], self.xAxis.isLog, name="x"
+ )
if y is not None:
- self._dataRanges['y'] = checkAxisLimits(
- y[0], y[1], self.yAxis.isLog, name='y')
+ self._dataRanges["y"] = checkAxisLimits(
+ y[0], y[1], self.yAxis.isLog, name="y"
+ )
if y2 is not None:
- self._dataRanges['y2'] = checkAxisLimits(
- y2[0], y2[1], self.y2Axis.isLog, name='y2')
+ self._dataRanges["y2"] = checkAxisLimits(
+ y2[0], y2[1], self.y2Axis.isLog, name="y2"
+ )
- self.xAxis.dataRange = self._dataRanges['x']
- self.yAxis.dataRange = self._dataRanges['y']
- self.y2Axis.dataRange = self._dataRanges['y2']
+ self.xAxis.dataRange = self._dataRanges["x"]
+ self.yAxis.dataRange = self._dataRanges["y"]
+ self.y2Axis.dataRange = self._dataRanges["y2"]
- _DataRanges = namedtuple('dataRanges', ('x', 'y', 'y2'))
+ _DataRanges = namedtuple("dataRanges", ("x", "y", "y2"))
@property
def transformedDataRanges(self):
@@ -911,39 +1050,40 @@ class GLPlotFrame2D(GLPlotFrame):
try:
xMin = math.log10(xMin)
except ValueError:
- _logger.info('xMin: warning log10(%f)', xMin)
- xMin = 0.
+ _logger.info("xMin: warning log10(%f)", xMin)
+ xMin = 0.0
try:
xMax = math.log10(xMax)
except ValueError:
- _logger.info('xMax: warning log10(%f)', xMax)
- xMax = 0.
+ _logger.info("xMax: warning log10(%f)", xMax)
+ xMax = 0.0
if self.yAxis.isLog:
try:
yMin = math.log10(yMin)
except ValueError:
- _logger.info('yMin: warning log10(%f)', yMin)
- yMin = 0.
+ _logger.info("yMin: warning log10(%f)", yMin)
+ yMin = 0.0
try:
yMax = math.log10(yMax)
except ValueError:
- _logger.info('yMax: warning log10(%f)', yMax)
- yMax = 0.
+ _logger.info("yMax: warning log10(%f)", yMax)
+ yMax = 0.0
try:
y2Min = math.log10(y2Min)
except ValueError:
- _logger.info('yMin: warning log10(%f)', y2Min)
- y2Min = 0.
+ _logger.info("yMin: warning log10(%f)", y2Min)
+ y2Min = 0.0
try:
y2Max = math.log10(y2Max)
except ValueError:
- _logger.info('yMax: warning log10(%f)', y2Max)
- y2Max = 0.
+ _logger.info("yMax: warning log10(%f)", y2Max)
+ y2Max = 0.0
self._transformedDataRanges = self._DataRanges(
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max))
+ (xMin, xMax), (yMin, yMax), (y2Min, y2Max)
+ )
return self._transformedDataRanges
@@ -984,26 +1124,38 @@ class GLPlotFrame2D(GLPlotFrame):
return self._transformedDataY2ProjMat
- def dataToPixel(self, x, y, axis='left'):
- """Convert data coordinate to widget pixel coordinate.
- """
- assert axis in ('left', 'right')
+ @staticmethod
+ def __applyLog(
+ data: Union[float, numpy.ndarray], isLog: bool
+ ) -> Optional[Union[float, numpy.ndarray]]:
+ """Apply log to data filtering out"""
+ if not isLog:
+ return data
+
+ if isinstance(data, numbers.Real):
+ return None if data < FLOAT32_MINPOS else math.log10(data)
+
+ isBelowMin = data < FLOAT32_MINPOS
+ if numpy.any(isBelowMin):
+ data = numpy.array(data, copy=True, dtype=numpy.float64)
+ data[isBelowMin] = numpy.nan
+
+ with numpy.errstate(divide="ignore"):
+ return numpy.log10(data)
+
+ def dataToPixel(self, x, y, axis="left"):
+ """Convert data coordinate to widget pixel coordinate."""
+ assert axis in ("left", "right")
trBounds = self.transformedDataRanges
- if self.xAxis.isLog:
- if x < FLOAT32_MINPOS:
- return None
- xDataTr = math.log10(x)
- else:
- xDataTr = x
+ xDataTr = self.__applyLog(x, self.xAxis.isLog)
+ if xDataTr is None:
+ return None
- if self.yAxis.isLog:
- if y < FLOAT32_MINPOS:
- return None
- yDataTr = math.log10(y)
- else:
- yDataTr = y
+ yDataTr = self.__applyLog(y, self.yAxis.isLog)
+ if yDataTr is None:
+ return None
# Non-orthogonal axes
if self.baseVectors != self.DEFAULT_BASE_VECTORS:
@@ -1015,20 +1167,26 @@ class GLPlotFrame2D(GLPlotFrame):
plotWidth, plotHeight = self.plotSize
- xPixel = int(self.margins.left +
- plotWidth * (xDataTr - trBounds.x[0]) /
- (trBounds.x[1] - trBounds.x[0]))
+ xPixel = self.margins.left + plotWidth * (xDataTr - trBounds.x[0]) / (
+ trBounds.x[1] - trBounds.x[0]
+ )
usedAxis = trBounds.y if axis == "left" else trBounds.y2
- yOffset = (plotHeight * (yDataTr - usedAxis[0]) /
- (usedAxis[1] - usedAxis[0]))
+ yOffset = plotHeight * (yDataTr - usedAxis[0]) / (usedAxis[1] - usedAxis[0])
if self.isYAxisInverted:
- yPixel = int(self.margins.top + yOffset)
+ yPixel = self.margins.top + yOffset
else:
- yPixel = int(self.size[1] - self.margins.bottom - yOffset)
+ yPixel = self.size[1] - self.margins.bottom - yOffset
- return xPixel, yPixel
+ return (
+ int(xPixel)
+ if isinstance(xPixel, numbers.Real)
+ else xPixel.astype(numpy.int64),
+ int(yPixel)
+ if isinstance(yPixel, numbers.Real)
+ else yPixel.astype(numpy.int64),
+ )
def pixelToData(self, x, y, axis="left"):
"""Convert pixel position to data coordinates.
@@ -1083,8 +1241,7 @@ class GLPlotFrame2D(GLPlotFrame):
if axis == self.xAxis:
vertices.append((xPixel, self.margins.top))
elif axis == self.yAxis:
- vertices.append((self.size[0] - self.margins.right,
- yPixel))
+ vertices.append((self.size[0] - self.margins.right, yPixel))
else: # axis == self.y2Axis
vertices.append((self.margins.left, yPixel))
@@ -1093,28 +1250,33 @@ class GLPlotFrame2D(GLPlotFrame):
plotLeft, plotTop = self.plotOrigin
plotWidth, plotHeight = self.plotSize
- corners = [(plotLeft, plotTop),
- (plotLeft, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop)]
+ corners = [
+ (plotLeft, plotTop),
+ (plotLeft, plotTop + plotHeight),
+ (plotLeft + plotWidth, plotTop + plotHeight),
+ (plotLeft + plotWidth, plotTop),
+ ]
for axis in self.axes:
if axis == self.xAxis:
- cornersInData = numpy.array([
- self.pixelToData(x, y) for (x, y) in corners])
- borders = ((cornersInData[0], cornersInData[3]), # top
- (cornersInData[1], cornersInData[0]), # left
- (cornersInData[3], cornersInData[2])) # right
+ cornersInData = numpy.array(
+ [self.pixelToData(x, y) for (x, y) in corners]
+ )
+ borders = (
+ (cornersInData[0], cornersInData[3]), # top
+ (cornersInData[1], cornersInData[0]), # left
+ (cornersInData[3], cornersInData[2]),
+ ) # right
for (xPixel, yPixel), data, text in axis.ticks:
if test(text):
for (x0, y0), (x1, y1) in borders:
if min(x0, x1) <= data < max(x0, x1):
- yIntersect = (data - x0) * \
- (y1 - y0) / (x1 - x0) + y0
+ yIntersect = (data - x0) * (y1 - y0) / (
+ x1 - x0
+ ) + y0
- pixelPos = self.dataToPixel(
- data, yIntersect)
+ pixelPos = self.dataToPixel(data, yIntersect)
if pixelPos is not None:
vertices.append((xPixel, yPixel))
vertices.append(pixelPos)
@@ -1122,32 +1284,38 @@ class GLPlotFrame2D(GLPlotFrame):
else: # y or y2 axes
if axis == self.yAxis:
- axis_name = 'left'
- cornersInData = numpy.array([
- self.pixelToData(x, y) for (x, y) in corners])
+ axis_name = "left"
+ cornersInData = numpy.array(
+ [self.pixelToData(x, y) for (x, y) in corners]
+ )
borders = (
(cornersInData[3], cornersInData[2]), # right
(cornersInData[0], cornersInData[3]), # top
- (cornersInData[2], cornersInData[1])) # bottom
+ (cornersInData[2], cornersInData[1]),
+ ) # bottom
else: # axis == self.y2Axis
- axis_name = 'right'
- corners = numpy.array([self.pixelToData(
- x, y, axis='right') for (x, y) in corners])
+ axis_name = "right"
+ corners = numpy.array(
+ [self.pixelToData(x, y, axis="right") for (x, y) in corners]
+ )
borders = (
(cornersInData[1], cornersInData[0]), # left
(cornersInData[0], cornersInData[3]), # top
- (cornersInData[2], cornersInData[1])) # bottom
+ (cornersInData[2], cornersInData[1]),
+ ) # bottom
for (xPixel, yPixel), data, text in axis.ticks:
if test(text):
for (x0, y0), (x1, y1) in borders:
if min(y0, y1) <= data < max(y0, y1):
- xIntersect = (data - y0) * \
- (x1 - x0) / (y1 - y0) + x0
+ xIntersect = (data - y0) * (x1 - x0) / (
+ y1 - y0
+ ) + x0
pixelPos = self.dataToPixel(
- xIntersect, data, axis=axis_name)
+ xIntersect, data, axis=axis_name
+ )
if pixelPos is not None:
vertices.append((xPixel, yPixel))
vertices.append(pixelPos)
@@ -1158,26 +1326,47 @@ class GLPlotFrame2D(GLPlotFrame):
def _buildVerticesAndLabels(self):
width, height = self.size
- xCoords = (self.margins.left - 0.5,
- width - self.margins.right + 0.5)
- yCoords = (height - self.margins.bottom + 0.5,
- self.margins.top - 0.5)
-
- self.axes[0].displayCoords = ((xCoords[0], yCoords[0]),
- (xCoords[1], yCoords[0]))
-
- self._x2AxisCoords = ((xCoords[0], yCoords[1]),
- (xCoords[1], yCoords[1]))
+ xCoords = (self.margins.left - 0.5, width - self.margins.right + 0.5)
+ yCoords = (height - self.margins.bottom + 0.5, self.margins.top - 0.5)
+
+ self.axes[0].displayCoords = (
+ (xCoords[0], yCoords[0]),
+ (xCoords[1], yCoords[0]),
+ )
+
+ self._x2AxisCoords = ((xCoords[0], yCoords[1]), (xCoords[1], yCoords[1]))
+
+ # Set order&offset anchor **before** handling Y axis inversion
+ fontPixelSize = self._font.pixelSize()
+ if fontPixelSize == -1:
+ fontPixelSize = self._font.pointSizeF() / 72.0 * self.dotsPerInch
+
+ self.axes[0].orderOffetAnchor = (
+ xCoords[1],
+ yCoords[0] + fontPixelSize * 1.2,
+ )
+ self.axes[1].orderOffetAnchor = (
+ xCoords[0],
+ yCoords[1] - 4 * self.devicePixelRatio,
+ )
+ self._y2Axis.orderOffetAnchor = (
+ xCoords[1],
+ yCoords[1] - 4 * self.devicePixelRatio,
+ )
if self.isYAxisInverted:
# Y axes are inverted, axes coordinates are inverted
yCoords = yCoords[1], yCoords[0]
- self.axes[1].displayCoords = ((xCoords[0], yCoords[0]),
- (xCoords[0], yCoords[1]))
+ self.axes[1].displayCoords = (
+ (xCoords[0], yCoords[0]),
+ (xCoords[0], yCoords[1]),
+ )
- self._y2Axis.displayCoords = ((xCoords[1], yCoords[0]),
- (xCoords[1], yCoords[1]))
+ self._y2Axis.displayCoords = (
+ (xCoords[1], yCoords[0]),
+ (xCoords[1], yCoords[1]),
+ )
super(GLPlotFrame2D, self)._buildVerticesAndLabels()
@@ -1189,8 +1378,7 @@ class GLPlotFrame2D(GLPlotFrame):
if not self.isY2Axis:
extraVertices += self._y2Axis.displayCoords
- extraVertices = numpy.array(
- extraVertices, copy=False, dtype=numpy.float32)
+ extraVertices = numpy.array(extraVertices, copy=False, dtype=numpy.float32)
vertices = numpy.append(vertices, extraVertices, axis=0)
self._renderResources = (vertices, gridVertices, labels)
@@ -1203,8 +1391,9 @@ class GLPlotFrame2D(GLPlotFrame):
@foregroundColor.setter
def foregroundColor(self, color):
"""Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
+ assert len(color) == 4, "foregroundColor must have length 4, got {}".format(
+ len(self._foregroundColor)
+ )
if self._foregroundColor != color:
self._y2Axis.foregroundColor = color
- GLPlotFrame.foregroundColor.fset(self, color) # call parent property
+ GLPlotFrame.foregroundColor.fset(self, color) # call parent property
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotImage.py b/src/silx/gui/plot/backends/glutils/GLPlotImage.py
index 3ad94b9..0973c47 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotImage.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotImage.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,8 +33,6 @@ __date__ = "03/04/2017"
import math
import numpy
-from silx.math.combo import min_max
-
from ...._glutils import gl, Program, Texture
from ..._utils import FLOAT32_MINPOS
from .GLSupport import mat4Translate, mat4Scale
@@ -65,29 +62,28 @@ class _GLPlotData2D(GLPlotItem):
@property
def xMin(self):
ox, sx = self.origin[0], self.scale[0]
- return ox if sx >= 0. else ox + sx * self.data.shape[1]
+ return ox if sx >= 0.0 else ox + sx * self.data.shape[1]
@property
def yMin(self):
oy, sy = self.origin[1], self.scale[1]
- return oy if sy >= 0. else oy + sy * self.data.shape[0]
+ return oy if sy >= 0.0 else oy + sy * self.data.shape[0]
@property
def xMax(self):
ox, sx = self.origin[0], self.scale[0]
- return ox + sx * self.data.shape[1] if sx >= 0. else ox
+ return ox + sx * self.data.shape[1] if sx >= 0.0 else ox
@property
def yMax(self):
oy, sy = self.origin[1], self.scale[1]
- return oy + sy * self.data.shape[0] if sy >= 0. else oy
+ return oy + sy * self.data.shape[0] if sy >= 0.0 else oy
class GLPlotColormap(_GLPlotData2D):
-
_SHADERS = {
- 'linear': {
- 'vertex': """
+ "linear": {
+ "vertex": """
#version 120
uniform mat4 matrix;
@@ -101,14 +97,14 @@ class GLPlotColormap(_GLPlotData2D):
gl_Position = matrix * vec4(position, 0.0, 1.0);
}
""",
- 'fragTransform': """
+ "fragTransform": """
vec2 textureCoords(void) {
return coords;
}
- """},
-
- 'log': {
- 'vertex': """
+ """,
+ },
+ "log": {
+ "vertex": """
#version 120
attribute vec2 position;
@@ -132,7 +128,7 @@ class GLPlotColormap(_GLPlotData2D):
gl_Position = matrix * dataPos;
}
""",
- 'fragTransform': """
+ "fragTransform": """
uniform bvec2 isLog;
uniform vec2 bounds_oneOverRange;
uniform vec2 bounds_originOverRange;
@@ -148,9 +144,9 @@ class GLPlotColormap(_GLPlotData2D):
return pos * bounds_oneOverRange - bounds_originOverRange;
// TODO texture coords in range different from [0, 1]
}
- """},
-
- 'fragment': """
+ """,
+ },
+ "fragment": """
#version 120
/* isnan declaration for compatibility with GLSL 1.20 */
@@ -159,6 +155,7 @@ class GLPlotColormap(_GLPlotData2D):
}
uniform sampler2D data;
+ uniform float data_scale;
uniform sampler2D cmap_texture;
uniform int cmap_normalization;
uniform float cmap_parameter;
@@ -174,42 +171,42 @@ class GLPlotColormap(_GLPlotData2D):
const float oneOverLog10 = 0.43429448190325176;
void main(void) {
- float data = texture2D(data, textureCoords()).r;
- float value = data;
+ float raw_data = texture2D(data, textureCoords()).r * data_scale;
+ float value = 0.;
if (cmap_normalization == 1) { /*Logarithm mapping*/
- if (value > 0.) {
+ if (raw_data > 0.) {
value = clamp(cmap_oneOverRange *
- (oneOverLog10 * log(value) - cmap_min),
+ (oneOverLog10 * log(raw_data) - cmap_min),
0., 1.);
} else {
value = 0.;
}
} else if (cmap_normalization == 2) { /*Square root mapping*/
- if (value >= 0.) {
- value = clamp(cmap_oneOverRange * (sqrt(value) - cmap_min),
+ if (raw_data >= 0.) {
+ value = clamp(cmap_oneOverRange * (sqrt(raw_data) - cmap_min),
0., 1.);
} else {
value = 0.;
}
} else if (cmap_normalization == 3) { /*Gamma correction mapping*/
value = pow(
- clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.),
+ clamp(cmap_oneOverRange * (raw_data - cmap_min), 0., 1.),
cmap_parameter);
} else if (cmap_normalization == 4) { /* arcsinh mapping */
/* asinh = log(x + sqrt(x*x + 1) for compatibility with GLSL 1.20 */
- value = clamp(cmap_oneOverRange * (log(value + sqrt(value*value + 1.0)) - cmap_min), 0., 1.);
+ value = clamp(cmap_oneOverRange * (log(raw_data + sqrt(raw_data*raw_data + 1.0)) - cmap_min), 0., 1.);
} else { /*Linear mapping and fallback*/
- value = clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.);
+ value = clamp(cmap_oneOverRange * (raw_data - cmap_min), 0., 1.);
}
- if (isnan(data)) {
+ if (isnan(raw_data)) {
gl_FragColor = nancolor;
} else {
gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5));
}
gl_FragColor.a *= alpha;
}
- """
+ """,
}
_DATA_TEX_UNIT = 0
@@ -223,21 +220,32 @@ class GLPlotColormap(_GLPlotData2D):
numpy.dtype(numpy.uint8): gl.GL_R8,
}
- _linearProgram = Program(_SHADERS['linear']['vertex'],
- _SHADERS['fragment'] %
- _SHADERS['linear']['fragTransform'],
- attrib0='position')
-
- _logProgram = Program(_SHADERS['log']['vertex'],
- _SHADERS['fragment'] %
- _SHADERS['log']['fragTransform'],
- attrib0='position')
-
- SUPPORTED_NORMALIZATIONS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh'
-
- def __init__(self, data, origin, scale,
- colormap, normalization='linear', gamma=0., cmapRange=None,
- alpha=1.0, nancolor=(1., 1., 1., 0.)):
+ _linearProgram = Program(
+ _SHADERS["linear"]["vertex"],
+ _SHADERS["fragment"] % _SHADERS["linear"]["fragTransform"],
+ attrib0="position",
+ )
+
+ _logProgram = Program(
+ _SHADERS["log"]["vertex"],
+ _SHADERS["fragment"] % _SHADERS["log"]["fragTransform"],
+ attrib0="position",
+ )
+
+ SUPPORTED_NORMALIZATIONS = "linear", "log", "sqrt", "gamma", "arcsinh"
+
+ def __init__(
+ self,
+ data,
+ origin,
+ scale,
+ colormap,
+ normalization="linear",
+ gamma=0.0,
+ cmapRange=None,
+ alpha=1.0,
+ nancolor=(1.0, 1.0, 1.0, 0.0),
+ ):
"""Create a 2D colormap
:param data: The 2D scalar data array to display
@@ -267,10 +275,10 @@ class GLPlotColormap(_GLPlotData2D):
self.colormap = numpy.array(colormap, copy=False)
self.normalization = normalization
self.gamma = gamma
- self._cmapRange = (1., 10.) # Colormap range
+ self._cmapRange = (1.0, 10.0) # Colormap range
self.cmapRange = cmapRange # Update _cmapRange
- self._alpha = numpy.clip(alpha, 0., 1.)
- self._nancolor = numpy.clip(nancolor, 0., 1.)
+ self._alpha = numpy.clip(alpha, 0.0, 1.0)
+ self._nancolor = numpy.clip(nancolor, 0.0, 1.0)
self._cmap_texture = None
self._texture = None
@@ -287,15 +295,14 @@ class GLPlotColormap(_GLPlotData2D):
self._textureIsDirty = False
def isInitialized(self):
- return (self._cmap_texture is not None or
- self._texture is not None)
+ return self._cmap_texture is not None or self._texture is not None
@property
def cmapRange(self):
- if self.normalization == 'log':
- assert self._cmapRange[0] > 0. and self._cmapRange[1] > 0.
- elif self.normalization == 'sqrt':
- assert self._cmapRange[0] >= 0. and self._cmapRange[1] >= 0.
+ if self.normalization == "log":
+ assert self._cmapRange[0] > 0.0 and self._cmapRange[1] > 0.0
+ elif self.normalization == "sqrt":
+ assert self._cmapRange[0] >= 0.0 and self._cmapRange[1] >= 0.0
return self._cmapRange
@cmapRange.setter
@@ -314,8 +321,7 @@ class GLPlotColormap(_GLPlotData2D):
self.data = data
if self._texture is not None:
- if (self.data.shape != oldData.shape or
- self.data.dtype != oldData.dtype):
+ if self.data.shape != oldData.shape or self.data.dtype != oldData.dtype:
self.discard()
else:
self._textureIsDirty = True
@@ -324,72 +330,77 @@ class GLPlotColormap(_GLPlotData2D):
if self._cmap_texture is None:
# TODO share cmap texture accross Images
# put all cmaps in one texture
- colormap = numpy.empty((16, 256, self.colormap.shape[1]),
- dtype=self.colormap.dtype)
+ colormap = numpy.empty(
+ (16, 256, self.colormap.shape[1]), dtype=self.colormap.dtype
+ )
colormap[:] = self.colormap
format_ = gl.GL_RGBA if colormap.shape[-1] == 4 else gl.GL_RGB
- self._cmap_texture = Texture(internalFormat=format_,
- data=colormap,
- format_=format_,
- texUnit=self._CMAP_TEX_UNIT,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
+ self._cmap_texture = Texture(
+ internalFormat=format_,
+ data=colormap,
+ format_=format_,
+ texUnit=self._CMAP_TEX_UNIT,
+ minFilter=gl.GL_NEAREST,
+ magFilter=gl.GL_NEAREST,
+ wrap=(gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE),
+ )
self._cmap_texture.prepare()
if self._texture is None:
internalFormat = self._INTERNAL_FORMATS[self.data.dtype]
- self._texture = Image(internalFormat,
- self.data,
- format_=gl.GL_RED,
- texUnit=self._DATA_TEX_UNIT)
+ self._texture = Image(
+ internalFormat,
+ self.data,
+ format_=gl.GL_RED,
+ texUnit=self._DATA_TEX_UNIT,
+ )
elif self._textureIsDirty:
self._textureIsDirty = True
self._texture.updateAll(format_=gl.GL_RED, data=self.data)
def _setCMap(self, prog):
dataMin, dataMax = self.cmapRange # If log, it is stricly positive
- param = 0.
+ param = 0.0
if self.data.dtype in (numpy.uint16, numpy.uint8):
# Using unsigned int as normalized integer in OpenGL
- # So normalize range
- maxInt = float(numpy.iinfo(self.data.dtype).max)
- dataMin, dataMax = dataMin / maxInt, dataMax / maxInt
+ # So revert normalization in the shader
+ dataScale = float(numpy.iinfo(self.data.dtype).max)
+ else:
+ dataScale = 1.0
- if self.normalization == 'log':
+ if self.normalization == "log":
dataMin = math.log10(dataMin)
dataMax = math.log10(dataMax)
normID = 1
- elif self.normalization == 'sqrt':
+ elif self.normalization == "sqrt":
dataMin = math.sqrt(dataMin)
dataMax = math.sqrt(dataMax)
normID = 2
- elif self.normalization == 'gamma':
+ elif self.normalization == "gamma":
# Keep dataMin, dataMax as is
param = self.gamma
normID = 3
- elif self.normalization == 'arcsinh':
+ elif self.normalization == "arcsinh":
dataMin = numpy.arcsinh(dataMin)
dataMax = numpy.arcsinh(dataMax)
normID = 4
else: # Linear and fallback
normID = 0
- gl.glUniform1i(prog.uniforms['cmap_texture'],
- self._cmap_texture.texUnit)
- gl.glUniform1i(prog.uniforms['cmap_normalization'], normID)
- gl.glUniform1f(prog.uniforms['cmap_parameter'], param)
- gl.glUniform1f(prog.uniforms['cmap_min'], dataMin)
+ gl.glUniform1f(prog.uniforms["data_scale"], dataScale)
+ gl.glUniform1i(prog.uniforms["cmap_texture"], self._cmap_texture.texUnit)
+ gl.glUniform1i(prog.uniforms["cmap_normalization"], normID)
+ gl.glUniform1f(prog.uniforms["cmap_parameter"], param)
+ gl.glUniform1f(prog.uniforms["cmap_min"], dataMin)
if dataMax > dataMin:
- oneOverRange = 1. / (dataMax - dataMin)
+ oneOverRange = 1.0 / (dataMax - dataMin)
else:
- oneOverRange = 0. # Fall-back
- gl.glUniform1f(prog.uniforms['cmap_oneOverRange'], oneOverRange)
+ oneOverRange = 0.0 # Fall-back
+ gl.glUniform1f(prog.uniforms["cmap_oneOverRange"], oneOverRange)
- gl.glUniform4f(prog.uniforms['nancolor'], *self._nancolor)
+ gl.glUniform4f(prog.uniforms["nancolor"], *self._nancolor)
self._cmap_texture.bind()
@@ -403,21 +414,25 @@ class GLPlotColormap(_GLPlotData2D):
prog = self._linearProgram
prog.use()
- gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
+ gl.glUniform1i(prog.uniforms["data"], self._DATA_TEX_UNIT)
- mat = numpy.dot(numpy.dot(context.matrix,
- mat4Translate(*self.origin)),
- mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ mat = numpy.dot(
+ numpy.dot(context.matrix, mat4Translate(*self.origin)),
+ mat4Scale(*self.scale),
+ )
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
+ gl.glUniform1f(prog.uniforms["alpha"], self.alpha)
self._setCMap(prog)
- self._texture.render(prog.attributes['position'],
- prog.attributes['texCoords'],
- self._DATA_TEX_UNIT)
+ self._texture.render(
+ prog.attributes["position"],
+ prog.attributes["texCoords"],
+ self._DATA_TEX_UNIT,
+ )
def _renderLog10(self, context):
"""Perform rendering when one axis has log scale
@@ -425,8 +440,9 @@ class GLPlotColormap(_GLPlotData2D):
:param RenderContext context: Rendering information
"""
xMin, yMin = self.xMin, self.yMin
- if ((context.isXLog and xMin < FLOAT32_MINPOS) or
- (context.isYLog and yMin < FLOAT32_MINPOS)):
+ if (context.isXLog and xMin < FLOAT32_MINPOS) or (
+ context.isYLog and yMin < FLOAT32_MINPOS
+ ):
# Do not render images that are partly or totally <= 0
return
@@ -437,27 +453,33 @@ class GLPlotColormap(_GLPlotData2D):
ox, oy = self.origin
- gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
+ gl.glUniform1i(prog.uniforms["data"], self._DATA_TEX_UNIT)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, context.matrix.astype(numpy.float32)
+ )
mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matOffset"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.isYLog)
+ gl.glUniform2i(prog.uniforms["isLog"], context.isXLog, context.isYLog)
ex = ox + self.scale[0] * self.data.shape[1]
ey = oy + self.scale[1] * self.data.shape[0]
- xOneOverRange = 1. / (ex - ox)
- yOneOverRange = 1. / (ey - oy)
- gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
- ox * xOneOverRange, oy * yOneOverRange)
- gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
- xOneOverRange, yOneOverRange)
+ xOneOverRange = 1.0 / (ex - ox)
+ yOneOverRange = 1.0 / (ey - oy)
+ gl.glUniform2f(
+ prog.uniforms["bounds_originOverRange"],
+ ox * xOneOverRange,
+ oy * yOneOverRange,
+ )
+ gl.glUniform2f(
+ prog.uniforms["bounds_oneOverRange"], xOneOverRange, yOneOverRange
+ )
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
+ gl.glUniform1f(prog.uniforms["alpha"], self.alpha)
self._setCMap(prog)
@@ -467,20 +489,19 @@ class GLPlotColormap(_GLPlotData2D):
raise RuntimeError("No texture, discard has already been called")
if len(tiles) > 1:
raise NotImplementedError(
- "Image over multiple textures not supported with log scale")
+ "Image over multiple textures not supported with log scale"
+ )
texture, vertices, info = tiles[0]
texture.bind(self._DATA_TEX_UNIT)
- posAttrib = prog.attributes['position']
+ posAttrib = prog.attributes["position"]
stride = vertices.shape[-1] * vertices.itemsize
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, stride, vertices
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
@@ -501,11 +522,11 @@ class GLPlotColormap(_GLPlotData2D):
# image #######################################################################
-class GLPlotRGBAImage(_GLPlotData2D):
+class GLPlotRGBAImage(_GLPlotData2D):
_SHADERS = {
- 'linear': {
- 'vertex': """
+ "linear": {
+ "vertex": """
#version 120
attribute vec2 position;
@@ -519,7 +540,7 @@ class GLPlotRGBAImage(_GLPlotData2D):
coords = texCoords;
}
""",
- 'fragment': """
+ "fragment": """
#version 120
uniform sampler2D tex;
@@ -531,10 +552,10 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl_FragColor = texture2D(tex, coords);
gl_FragColor.a *= alpha;
}
- """},
-
- 'log': {
- 'vertex': """
+ """,
+ },
+ "log": {
+ "vertex": """
#version 120
attribute vec2 position;
@@ -558,7 +579,7 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl_Position = matrix * dataPos;
}
""",
- 'fragment': """
+ "fragment": """
#version 120
uniform sampler2D tex;
@@ -585,22 +606,25 @@ class GLPlotRGBAImage(_GLPlotData2D):
gl_FragColor = texture2D(tex, textureCoords());
gl_FragColor.a *= alpha;
}
- """}
+ """,
+ },
}
_DATA_TEX_UNIT = 0
- _SUPPORTED_DTYPES = (numpy.dtype(numpy.float32),
- numpy.dtype(numpy.uint8),
- numpy.dtype(numpy.uint16))
+ _SUPPORTED_DTYPES = (
+ numpy.dtype(numpy.float32),
+ numpy.dtype(numpy.uint8),
+ numpy.dtype(numpy.uint16),
+ )
- _linearProgram = Program(_SHADERS['linear']['vertex'],
- _SHADERS['linear']['fragment'],
- attrib0='position')
+ _linearProgram = Program(
+ _SHADERS["linear"]["vertex"], _SHADERS["linear"]["fragment"], attrib0="position"
+ )
- _logProgram = Program(_SHADERS['log']['vertex'],
- _SHADERS['log']['fragment'],
- attrib0='position')
+ _logProgram = Program(
+ _SHADERS["log"]["vertex"], _SHADERS["log"]["fragment"], attrib0="position"
+ )
def __init__(self, data, origin, scale, alpha):
"""Create a 2D RGB(A) image from data
@@ -619,7 +643,7 @@ class GLPlotRGBAImage(_GLPlotData2D):
super(GLPlotRGBAImage, self).__init__(data, origin, scale)
self._texture = None
self._textureIsDirty = False
- self._alpha = numpy.clip(alpha, 0., 1.)
+ self._alpha = numpy.clip(alpha, 0.0, 1.0)
@property
def alpha(self):
@@ -647,17 +671,16 @@ class GLPlotRGBAImage(_GLPlotData2D):
def prepare(self):
if self._texture is None:
- formatName = 'GL_RGBA' if self.data.shape[2] == 4 else 'GL_RGB'
+ formatName = "GL_RGBA" if self.data.shape[2] == 4 else "GL_RGB"
format_ = getattr(gl, formatName)
if self.data.dtype == numpy.uint16:
- formatName += '16' # Use sized internal format for uint16
+ formatName += "16" # Use sized internal format for uint16
internalFormat = getattr(gl, formatName)
- self._texture = Image(internalFormat,
- self.data,
- format_=format_,
- texUnit=self._DATA_TEX_UNIT)
+ self._texture = Image(
+ internalFormat, self.data, format_=format_, texUnit=self._DATA_TEX_UNIT
+ )
elif self._textureIsDirty:
self._textureIsDirty = False
@@ -675,18 +698,23 @@ class GLPlotRGBAImage(_GLPlotData2D):
prog = self._linearProgram
prog.use()
- gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
+ gl.glUniform1i(prog.uniforms["tex"], self._DATA_TEX_UNIT)
- mat = numpy.dot(numpy.dot(context.matrix, mat4Translate(*self.origin)),
- mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ mat = numpy.dot(
+ numpy.dot(context.matrix, mat4Translate(*self.origin)),
+ mat4Scale(*self.scale),
+ )
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
+ gl.glUniform1f(prog.uniforms["alpha"], self.alpha)
- self._texture.render(prog.attributes['position'],
- prog.attributes['texCoords'],
- self._DATA_TEX_UNIT)
+ self._texture.render(
+ prog.attributes["position"],
+ prog.attributes["texCoords"],
+ self._DATA_TEX_UNIT,
+ )
def _renderLog(self, context):
"""Perform rendering with axes having log scale
@@ -700,27 +728,33 @@ class GLPlotRGBAImage(_GLPlotData2D):
ox, oy = self.origin
- gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
+ gl.glUniform1i(prog.uniforms["tex"], self._DATA_TEX_UNIT)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, context.matrix.astype(numpy.float32)
+ )
mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matOffset"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.isYLog)
+ gl.glUniform2i(prog.uniforms["isLog"], context.isXLog, context.isYLog)
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
+ gl.glUniform1f(prog.uniforms["alpha"], self.alpha)
ex = ox + self.scale[0] * self.data.shape[1]
ey = oy + self.scale[1] * self.data.shape[0]
- xOneOverRange = 1. / (ex - ox)
- yOneOverRange = 1. / (ey - oy)
- gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
- ox * xOneOverRange, oy * yOneOverRange)
- gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
- xOneOverRange, yOneOverRange)
+ xOneOverRange = 1.0 / (ex - ox)
+ yOneOverRange = 1.0 / (ey - oy)
+ gl.glUniform2f(
+ prog.uniforms["bounds_originOverRange"],
+ ox * xOneOverRange,
+ oy * yOneOverRange,
+ )
+ gl.glUniform2f(
+ prog.uniforms["bounds_oneOverRange"], xOneOverRange, yOneOverRange
+ )
try:
tiles = self._texture.tiles
@@ -728,20 +762,19 @@ class GLPlotRGBAImage(_GLPlotData2D):
raise RuntimeError("No texture, discard has already been called")
if len(tiles) > 1:
raise NotImplementedError(
- "Image over multiple textures not supported with log scale")
+ "Image over multiple textures not supported with log scale"
+ )
texture, vertices, info = tiles[0]
texture.bind(self._DATA_TEX_UNIT)
- posAttrib = prog.attributes['position']
+ posAttrib = prog.attributes["position"]
stride = vertices.shape[-1] * vertices.itemsize
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, stride, vertices
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotItem.py b/src/silx/gui/plot/backends/glutils/GLPlotItem.py
index ae13091..0287ad5 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotItem.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotItem.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2020-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2020-2022 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
@@ -40,13 +39,16 @@ class RenderContext:
:param float dpi: Number of device pixels per inch
"""
- def __init__(self, matrix=None, isXLog=False, isYLog=False, dpi=96.):
+ def __init__(
+ self, matrix=None, isXLog=False, isYLog=False, dpi=96.0, plotFrame=None
+ ):
self.matrix = matrix
"""Current transformation matrix"""
self.__isXLog = isXLog
self.__isYLog = isYLog
self.__dpi = dpi
+ self.__plotFrame = plotFrame
@property
def isXLog(self):
@@ -63,12 +65,17 @@ class RenderContext:
"""Number of device pixels per inch"""
return self.__dpi
+ @property
+ def plotFrame(self):
+ """Current PlotFrame"""
+ return self.__plotFrame
+
class GLPlotItem:
"""Base class for primitives used in the PlotWidget OpenGL backend"""
def __init__(self):
- self.yaxis = 'left'
+ self.yaxis = "left"
"YAxis this item is attached to (either 'left' or 'right')"
def pick(self, x, y):
@@ -94,6 +101,5 @@ class GLPlotItem:
pass
def isInitialized(self) -> bool:
- """Returns True if resources where initialized and requires `discard`.
- """
+ """Returns True if resources where initialized and requires `discard`."""
return True
diff --git a/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py b/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py
index fbe9e02..e8a8e4a 100644
--- a/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py
+++ b/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2019-2021 European Synchrotron Radiation Facility
@@ -71,9 +70,10 @@ class GLPlotTriangles(GLPlotItem):
gl_FragColor.a *= alpha;
}
""",
- attrib0='xPos')
+ attrib0="xPos",
+ )
- def __init__(self, x, y, color, triangles, alpha=1.):
+ def __init__(self, x, y, color, triangles, alpha=1.0):
"""
:param numpy.ndarray x: X coordinates of triangle corners
@@ -98,14 +98,14 @@ class GLPlotTriangles(GLPlotItem):
elif numpy.issubdtype(color.dtype, numpy.integer):
color = numpy.array(color, dtype=numpy.uint8, copy=False)
else:
- raise ValueError('Unsupported color type')
+ raise ValueError("Unsupported color type")
assert triangles.ndim == 2 and triangles.shape[1] == 3
self.__x_y_color = x, y, color
self.xMin, self.xMax = min_max(x, finite=True)
self.yMin, self.yMax = min_max(y, finite=True)
self.__triangles = triangles
- self.__alpha = numpy.clip(float(alpha), 0., 1.)
+ self.__alpha = numpy.clip(float(alpha), 0.0, 1.0)
self.__vbos = None
self.__indicesVbo = None
self.__picking_triangles = None
@@ -118,21 +118,22 @@ class GLPlotTriangles(GLPlotItem):
:return: List of picked data point indices
:rtype: Union[List[int],None]
"""
- if (x < self.xMin or x > self.xMax or
- y < self.yMin or y > self.yMax):
+ if x < self.xMin or x > self.xMax or y < self.yMin or y > self.yMax:
return None
xPts, yPts = self.__x_y_color[:2]
if self.__picking_triangles is None:
self.__picking_triangles = numpy.zeros(
- self.__triangles.shape + (3,), dtype=numpy.float32)
+ self.__triangles.shape + (3,), dtype=numpy.float32
+ )
self.__picking_triangles[:, :, 0] = xPts[self.__triangles]
self.__picking_triangles[:, :, 1] = yPts[self.__triangles]
segment = numpy.array(((x, y, -1), (x, y, 1)), dtype=numpy.float32)
# Picked triangle indices
indices = glutils.segmentTrianglesIntersection(
- segment, self.__picking_triangles)[0]
+ segment, self.__picking_triangles
+ )[0]
# Point indices
indices = numpy.unique(numpy.ravel(self.__triangles[indices]))
@@ -164,7 +165,8 @@ class GLPlotTriangles(GLPlotItem):
self.__indicesVbo = glutils.VertexBuffer(
numpy.ravel(self.__triangles),
usage=gl.GL_STATIC_DRAW,
- target=gl.GL_ELEMENT_ARRAY_BUFFER)
+ target=gl.GL_ELEMENT_ARRAY_BUFFER,
+ )
def render(self, context):
"""Perform rendering
@@ -178,20 +180,24 @@ class GLPlotTriangles(GLPlotItem):
self._PROGRAM.use()
- gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'],
- 1,
- gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ self._PROGRAM.uniforms["matrix"],
+ 1,
+ gl.GL_TRUE,
+ context.matrix.astype(numpy.float32),
+ )
- gl.glUniform1f(self._PROGRAM.uniforms['alpha'], self.__alpha)
+ gl.glUniform1f(self._PROGRAM.uniforms["alpha"], self.__alpha)
- for index, name in enumerate(('xPos', 'yPos', 'color')):
+ for index, name in enumerate(("xPos", "yPos", "color")):
attr = self._PROGRAM.attributes[name]
gl.glEnableVertexAttribArray(attr)
self.__vbos[index].setVertexAttrib(attr)
with self.__indicesVbo:
- gl.glDrawElements(gl.GL_TRIANGLES,
- self.__triangles.size,
- glutils.numpyToGLType(self.__triangles.dtype),
- ctypes.c_void_p(0))
+ gl.glDrawElements(
+ gl.GL_TRIANGLES,
+ self.__triangles.size,
+ glutils.numpyToGLType(self.__triangles.dtype),
+ ctypes.c_void_p(0),
+ )
diff --git a/src/silx/gui/plot/backends/glutils/GLSupport.py b/src/silx/gui/plot/backends/glutils/GLSupport.py
index da6dffa..c9afda0 100644
--- a/src/silx/gui/plot/backends/glutils/GLSupport.py
+++ b/src/silx/gui/plot/backends/glutils/GLSupport.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
@@ -55,8 +54,7 @@ def buildFillMaskIndices(nIndices, dtype=None):
splitIndex = lastIndex // 2 + 1
indices = numpy.empty(nIndices, dtype=dtype)
indices[::2] = numpy.arange(0, splitIndex, step=1, dtype=dtype)
- indices[1::2] = numpy.arange(lastIndex, splitIndex - 1, step=-1,
- dtype=dtype)
+ indices[1::2] = numpy.arange(lastIndex, splitIndex - 1, step=-1, dtype=dtype)
return indices
@@ -64,16 +62,17 @@ class FilledShape2D(object):
_NO_HATCH = 0
_HATCH_STEP = 20
- def __init__(self, points, style='solid', color=(0., 0., 0., 1.)):
+ def __init__(self, points, style="solid", color=(0.0, 0.0, 0.0, 1.0)):
self.vertices = numpy.array(points, dtype=numpy.float32, copy=False)
self._indices = buildFillMaskIndices(len(self.vertices))
tVertex = numpy.transpose(self.vertices)
xMin, xMax = min(tVertex[0]), max(tVertex[0])
yMin, yMax = min(tVertex[1]), max(tVertex[1])
- self.bboxVertices = numpy.array(((xMin, yMin), (xMin, yMax),
- (xMax, yMin), (xMax, yMax)),
- dtype=numpy.float32)
+ self.bboxVertices = numpy.array(
+ ((xMin, yMin), (xMin, yMax), (xMax, yMin), (xMax, yMax)),
+ dtype=numpy.float32,
+ )
self._xMin, self._xMax = xMin, xMax
self._yMin, self._yMax = yMin, yMax
@@ -81,18 +80,16 @@ class FilledShape2D(object):
self.color = color
def render(self, posAttrib, colorUnif, hatchStepUnif):
- assert self.style in ('hatch', 'solid')
+ assert self.style in ("hatch", "solid")
gl.glUniform4f(colorUnif, *self.color)
- step = self._HATCH_STEP if self.style == 'hatch' else self._NO_HATCH
+ step = self._HATCH_STEP if self.style == "hatch" else self._NO_HATCH
gl.glUniform1i(hatchStepUnif, step)
# Prepare fill mask
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, self.vertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, self.vertices
+ )
gl.glEnable(gl.GL_STENCIL_TEST)
gl.glStencilMask(1)
@@ -101,8 +98,12 @@ class FilledShape2D(object):
gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
gl.glDepthMask(gl.GL_FALSE)
- gl.glDrawElements(gl.GL_TRIANGLE_STRIP, len(self._indices),
- gl.GL_UNSIGNED_SHORT, self._indices)
+ gl.glDrawElements(
+ gl.GL_TRIANGLE_STRIP,
+ len(self._indices),
+ gl.GL_UNSIGNED_SHORT,
+ self._indices,
+ )
gl.glStencilFunc(gl.GL_EQUAL, 1, 1)
# Reset stencil while drawing
@@ -110,11 +111,9 @@ class FilledShape2D(object):
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_TRUE)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, self.bboxVertices)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, self.bboxVertices
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self.bboxVertices))
gl.glDisable(gl.GL_STENCIL_TEST)
@@ -122,37 +121,54 @@ class FilledShape2D(object):
# matrix ######################################################################
+
def mat4Ortho(left, right, bottom, top, near, far):
"""Orthographic projection matrix (row-major)"""
- return numpy.array((
- (2./(right - left), 0., 0., -(right+left)/float(right-left)),
- (0., 2./(top - bottom), 0., -(top+bottom)/float(top-bottom)),
- (0., 0., -2./(far-near), -(far+near)/float(far-near)),
- (0., 0., 0., 1.)), dtype=numpy.float64)
-
-
-def mat4Translate(x=0., y=0., z=0.):
+ return numpy.array(
+ (
+ (2.0 / (right - left), 0.0, 0.0, -(right + left) / float(right - left)),
+ (0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / float(top - bottom)),
+ (0.0, 0.0, -2.0 / (far - near), -(far + near) / float(far - near)),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float64,
+ )
+
+
+def mat4Translate(x=0.0, y=0.0, z=0.0):
"""Translation matrix (row-major)"""
- return numpy.array((
- (1., 0., 0., x),
- (0., 1., 0., y),
- (0., 0., 1., z),
- (0., 0., 0., 1.)), dtype=numpy.float64)
-
-
-def mat4Scale(sx=1., sy=1., sz=1.):
+ return numpy.array(
+ (
+ (1.0, 0.0, 0.0, x),
+ (0.0, 1.0, 0.0, y),
+ (0.0, 0.0, 1.0, z),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float64,
+ )
+
+
+def mat4Scale(sx=1.0, sy=1.0, sz=1.0):
"""Scale matrix (row-major)"""
- return numpy.array((
- (sx, 0., 0., 0.),
- (0., sy, 0., 0.),
- (0., 0., sz, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64)
+ return numpy.array(
+ (
+ (sx, 0.0, 0.0, 0.0),
+ (0.0, sy, 0.0, 0.0),
+ (0.0, 0.0, sz, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float64,
+ )
def mat4Identity():
"""Identity matrix"""
- return numpy.array((
- (1., 0., 0., 0.),
- (0., 1., 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64)
+ return numpy.array(
+ (
+ (1.0, 0.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float64,
+ )
diff --git a/src/silx/gui/plot/backends/glutils/GLText.py b/src/silx/gui/plot/backends/glutils/GLText.py
index d6ae6fa..15d7a70 100644
--- a/src/silx/gui/plot/backends/glutils/GLText.py
+++ b/src/silx/gui/plot/backends/glutils/GLText.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,6 +26,8 @@ This module provides minimalistic text support for OpenGL.
It provides Latin-1 (ISO8859-1) characters for one monospace font at one size.
"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "03/04/2017"
@@ -37,14 +38,13 @@ import weakref
import numpy
+from .... import qt
from ...._glutils import font, gl, Context, Program, Texture
from .GLSupport import mat4Translate
+from silx.gui.colors import RGBAColorType
-# TODO: Font should be configurable by the main program: using mpl.rcParams?
-
-
-class _Cache(object):
+class _Cache:
"""LRU (Least Recent Used) cache.
:param int maxsize: Maximum number of (key, value) pairs in the cache
@@ -56,7 +56,7 @@ class _Cache(object):
def __init__(self, maxsize=128, callback=None):
self._maxsize = int(maxsize)
self._callback = callback
- self._cache = OrderedDict()
+ self._cache = OrderedDict() # Needed for popitem(last=False)
def __contains__(self, item):
return item in self._cache
@@ -85,15 +85,14 @@ class _Cache(object):
# Text2D ######################################################################
-LEFT, CENTER, RIGHT = 'left', 'center', 'right'
-TOP, BASELINE, BOTTOM = 'top', 'baseline', 'bottom'
+LEFT, CENTER, RIGHT = "left", "center", "right"
+TOP, BASELINE, BOTTOM = "top", "baseline", "bottom"
ROTATE_90, ROTATE_180, ROTATE_270 = 90, 180, 270
-class Text2D(object):
-
+class Text2D:
_SHADERS = {
- 'vertex': """
+ "vertex": """
#version 120
attribute vec2 position;
@@ -107,7 +106,7 @@ class Text2D(object):
vCoords = texCoords;
}
""",
- 'fragment': """
+ "fragment": """
#version 120
uniform sampler2D texText;
@@ -117,171 +116,182 @@ class Text2D(object):
varying vec2 vCoords;
void main(void) {
- gl_FragColor = mix(bgColor, color, texture2D(texText, vCoords).r);
+ if (vCoords.x < 0.0 || vCoords.x > 1.0 || vCoords.y < 0.0 || vCoords.y > 1.0) {
+ gl_FragColor = bgColor;
+ } else {
+ gl_FragColor = mix(bgColor, color, texture2D(texText, vCoords).r);
+ }
}
- """
+ """,
}
- _TEX_COORDS = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32).ravel()
-
- _program = Program(_SHADERS['vertex'],
- _SHADERS['fragment'],
- attrib0='position')
+ _program = Program(_SHADERS["vertex"], _SHADERS["fragment"], attrib0="position")
# Discard texture objects when removed from the cache
_textures = weakref.WeakKeyDictionary()
"""Cache already created textures"""
- _sizes = _Cache()
- """Cache already computed sizes"""
-
- def __init__(self, text, x=0, y=0,
- color=(0., 0., 0., 1.),
- bgColor=None,
- align=LEFT, valign=BASELINE,
- rotate=0,
- devicePixelRatio= 1.):
+ def __init__(
+ self,
+ text: str,
+ font: qt.QFont,
+ x: float = 0.0,
+ y: float = 0.0,
+ color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 1.0),
+ bgColor: RGBAColorType | None = None,
+ align: str = LEFT,
+ valign: str = BASELINE,
+ rotate: float = 0.0,
+ devicePixelRatio: float = 1.0,
+ padding: int = 0,
+ ):
self.devicePixelRatio = devicePixelRatio
+ self.font = font
self._vertices = None
self._text = text
+ self._padding = padding
self.x = x
self.y = y
self.color = color
self.bgColor = bgColor
if align not in (LEFT, CENTER, RIGHT):
- raise ValueError(
- "Horizontal alignment not supported: {0}".format(align))
+ raise ValueError("Horizontal alignment not supported: {0}".format(align))
self._align = align
if valign not in (TOP, CENTER, BASELINE, BOTTOM):
- raise ValueError(
- "Vertical alignment not supported: {0}".format(valign))
+ raise ValueError("Vertical alignment not supported: {0}".format(valign))
self._valign = valign
self._rotate = numpy.radians(rotate)
- def _getTexture(self, text, devicePixelRatio):
+ def _getTexture(self, dotsPerInch: float) -> tuple[Texture, int]:
# Retrieve/initialize texture cache for current context
- textureKey = text, devicePixelRatio
+ key = self.text, self.font.key(), dotsPerInch
context = Context.getCurrent()
if context not in self._textures:
self._textures[context] = _Cache(
- callback=lambda key, value: value[0].discard())
+ callback=lambda key, value: value[0].discard()
+ )
textures = self._textures[context]
- if textureKey not in textures:
- image, offset = font.rasterText(
- text,
- font.getDefaultFontFamily(),
- devicePixelRatio=self.devicePixelRatio)
- if textureKey not in self._sizes:
- self._sizes[textureKey] = image.shape[1], image.shape[0]
+ if key not in textures:
+ image, offset = font.rasterText(self.text, self.font, dotsPerInch)
texture = Texture(
gl.GL_RED,
data=image,
minFilter=gl.GL_NEAREST,
magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
+ wrap=(gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE),
+ )
texture.prepare()
- textures[textureKey] = texture, offset
+ textures[key] = texture, offset
- return textures[textureKey]
+ return textures[key]
@property
- def text(self):
+ def text(self) -> str:
return self._text
@property
- def size(self):
- textureKey = self.text, self.devicePixelRatio
- if textureKey not in self._sizes:
- image, offset = font.rasterText(
- self.text,
- font.getDefaultFontFamily(),
- devicePixelRatio=self.devicePixelRatio)
- self._sizes[textureKey] = image.shape[1], image.shape[0]
- return self._sizes[textureKey]
-
- def getVertices(self, offset, shape):
+ def padding(self) -> int:
+ return self._padding
+
+ def getVertices(self, offset: int, shape: tuple[int, int]) -> numpy.ndarray:
height, width = shape
if self._align == LEFT:
xOrig = 0
elif self._align == RIGHT:
- xOrig = - width
+ xOrig = -width
else: # CENTER
- xOrig = - width // 2
+ xOrig = -width // 2
if self._valign == BASELINE:
- yOrig = - offset
+ yOrig = -offset
elif self._valign == TOP:
yOrig = 0
elif self._valign == BOTTOM:
- yOrig = - height
+ yOrig = -height
else: # CENTER
- yOrig = - height // 2
-
- vertices = numpy.array((
- (xOrig, yOrig),
- (xOrig + width, yOrig),
- (xOrig, yOrig + height),
- (xOrig + width, yOrig + height)), dtype=numpy.float32)
+ yOrig = -height // 2
+
+ vertices = numpy.array(
+ (
+ (xOrig, yOrig),
+ (xOrig + width, yOrig),
+ (xOrig, yOrig + height),
+ (xOrig + width, yOrig + height),
+ ),
+ dtype=numpy.float32,
+ )
cos, sin = numpy.cos(self._rotate), numpy.sin(self._rotate)
- vertices = numpy.ascontiguousarray(numpy.transpose(numpy.array((
- cos * vertices[:, 0] - sin * vertices[:, 1],
- sin * vertices[:, 0] + cos * vertices[:, 1]),
- dtype=numpy.float32)))
+ vertices = numpy.ascontiguousarray(
+ numpy.transpose(
+ numpy.array(
+ (
+ cos * vertices[:, 0] - sin * vertices[:, 1],
+ sin * vertices[:, 0] + cos * vertices[:, 1],
+ ),
+ dtype=numpy.float32,
+ )
+ )
+ )
return vertices
- def render(self, matrix):
- if not self.text:
+ def render(self, matrix: numpy.ndarray, dotsPerInch: float):
+ if not self.text.strip():
return
prog = self._program
prog.use()
texUnit = 0
- texture, offset = self._getTexture(self.text, self.devicePixelRatio)
+ texture, offset = self._getTexture(dotsPerInch)
- gl.glUniform1i(prog.uniforms['texText'], texUnit)
+ gl.glUniform1i(prog.uniforms["texText"], texUnit)
mat = numpy.dot(matrix, mat4Translate(int(self.x), int(self.y)))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
+ gl.glUniformMatrix4fv(
+ prog.uniforms["matrix"], 1, gl.GL_TRUE, mat.astype(numpy.float32)
+ )
- gl.glUniform4f(prog.uniforms['color'], *self.color)
+ gl.glUniform4f(prog.uniforms["color"], *self.color)
if self.bgColor is not None:
bgColor = self.bgColor
else:
- bgColor = self.color[0], self.color[1], self.color[2], 0.
- gl.glUniform4f(prog.uniforms['bgColor'], *bgColor)
+ bgColor = self.color[0], self.color[1], self.color[2], 0.0
+ gl.glUniform4f(prog.uniforms["bgColor"], *bgColor)
- vertices = self.getVertices(offset, texture.shape)
+ paddingOffset = max(0, int(self.padding * self.devicePixelRatio))
+ height, width = texture.shape
+ vertices = self.getVertices(
+ offset, (height + 2 * paddingOffset, width + 2 * paddingOffset)
+ )
- posAttrib = prog.attributes['position']
+ posAttrib = prog.attributes["position"]
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- vertices)
-
- texAttrib = prog.attributes['texCoords']
+ gl.glVertexAttribPointer(posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, vertices)
+
+ xoffset = paddingOffset / width
+ yoffset = paddingOffset / height
+ texCoords = numpy.array(
+ (
+ (-xoffset, -yoffset),
+ (1.0 + xoffset, -yoffset),
+ (-xoffset, 1.0 + yoffset),
+ (1.0 + xoffset, 1.0 + yoffset),
+ ),
+ dtype=numpy.float32,
+ ).ravel()
+
+ texAttrib = prog.attributes["texCoords"]
gl.glEnableVertexAttribArray(texAttrib)
- gl.glVertexAttribPointer(texAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._TEX_COORDS)
+ gl.glVertexAttribPointer(texAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, 0, texCoords)
with texture:
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
diff --git a/src/silx/gui/plot/backends/glutils/GLTexture.py b/src/silx/gui/plot/backends/glutils/GLTexture.py
index 37fbdd0..cbbe7ac 100644
--- a/src/silx/gui/plot/backends/glutils/GLTexture.py
+++ b/src/silx/gui/plot/backends/glutils/GLTexture.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
@@ -40,29 +39,33 @@ from ...._glutils import gl, Texture, numpyToGLType
_logger = logging.getLogger(__name__)
-def _checkTexture2D(internalFormat, shape,
- format_=None, type_=gl.GL_FLOAT, border=0):
+def _checkTexture2D(internalFormat, shape, format_=None, type_=gl.GL_FLOAT, border=0):
"""Check if texture size with provided parameters is supported
:rtype: bool
"""
height, width = shape
- gl.glTexImage2D(gl.GL_PROXY_TEXTURE_2D, 0, internalFormat,
- width, height, border,
- format_ or internalFormat,
- type_, c_void_p(0))
- width = gl.glGetTexLevelParameteriv(
- gl.GL_PROXY_TEXTURE_2D, 0, gl.GL_TEXTURE_WIDTH)
+ gl.glTexImage2D(
+ gl.GL_PROXY_TEXTURE_2D,
+ 0,
+ internalFormat,
+ width,
+ height,
+ border,
+ format_ or internalFormat,
+ type_,
+ c_void_p(0),
+ )
+ width = gl.glGetTexLevelParameteriv(gl.GL_PROXY_TEXTURE_2D, 0, gl.GL_TEXTURE_WIDTH)
return bool(width)
MIN_TEXTURE_SIZE = 64
-def _getMaxSquareTexture2DSize(internalFormat=gl.GL_RGBA,
- format_=None,
- type_=gl.GL_FLOAT,
- border=0):
+def _getMaxSquareTexture2DSize(
+ internalFormat=gl.GL_RGBA, format_=None, type_=gl.GL_FLOAT, border=0
+):
"""Returns a supported size for a corresponding square texture
:returns: GL_MAX_TEXTURE_SIZE or a smaller supported size (not optimal)
@@ -70,16 +73,15 @@ def _getMaxSquareTexture2DSize(internalFormat=gl.GL_RGBA,
"""
# Is this useful?
maxTexSize = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE)
- while maxTexSize > MIN_TEXTURE_SIZE and \
- not _checkTexture2D(internalFormat, (maxTexSize, maxTexSize),
- format_, type_, border):
+ while maxTexSize > MIN_TEXTURE_SIZE and not _checkTexture2D(
+ internalFormat, (maxTexSize, maxTexSize), format_, type_, border
+ ):
maxTexSize //= 2
return max(MIN_TEXTURE_SIZE, maxTexSize)
class Image(object):
- """Image of any size eventually using multiple textures or larger texture
- """
+ """Image of any size eventually using multiple textures or larger texture"""
_WRAP = (gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE)
_MIN_FILTER = gl.GL_NEAREST
@@ -91,34 +93,48 @@ class Image(object):
type_ = numpyToGLType(data.dtype)
if _checkTexture2D(internalFormat, data.shape[0:2], format_, type_):
- texture = Texture(internalFormat,
- data,
- format_,
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
+ texture = Texture(
+ internalFormat,
+ data,
+ format_,
+ texUnit=texUnit,
+ minFilter=self._MIN_FILTER,
+ magFilter=self._MAG_FILTER,
+ wrap=self._WRAP,
+ )
texture.prepare()
- vertices = numpy.array((
- (0., 0., 0., 0.),
- (self.width, 0., 1., 0.),
- (0., self.height, 0., 1.),
- (self.width, self.height, 1., 1.)), dtype=numpy.float32)
- self.tiles = ((texture, vertices,
- {'xOrigData': 0, 'yOrigData': 0,
- 'wData': self.width, 'hData': self.height}),)
+ vertices = numpy.array(
+ (
+ (0.0, 0.0, 0.0, 0.0),
+ (self.width, 0.0, 1.0, 0.0),
+ (0.0, self.height, 0.0, 1.0),
+ (self.width, self.height, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+ self.tiles = (
+ (
+ texture,
+ vertices,
+ {
+ "xOrigData": 0,
+ "yOrigData": 0,
+ "wData": self.width,
+ "hData": self.height,
+ },
+ ),
+ )
else:
# Handle dimension too large: make tiles
- maxTexSize = _getMaxSquareTexture2DSize(internalFormat,
- format_, type_)
+ maxTexSize = _getMaxSquareTexture2DSize(internalFormat, format_, type_)
- nCols = (self.width+maxTexSize-1) // maxTexSize
+ nCols = (self.width + maxTexSize - 1) // maxTexSize
colWidths = [self.width // nCols] * nCols
colWidths[-1] += self.width % nCols
- nRows = (self.height+maxTexSize-1) // maxTexSize
- rowHeights = [self.height//nRows] * nRows
+ nRows = (self.height + maxTexSize - 1) // maxTexSize
+ rowHeights = [self.height // nRows] * nRows
rowHeights[-1] += self.height % nRows
tiles = []
@@ -126,30 +142,32 @@ class Image(object):
for hData in rowHeights:
xOrig = 0
for wData in colWidths:
- if (hData < MIN_TEXTURE_SIZE or wData < MIN_TEXTURE_SIZE) \
- and not _checkTexture2D(internalFormat,
- (hData, wData),
- format_,
- type_):
+ if (
+ hData < MIN_TEXTURE_SIZE or wData < MIN_TEXTURE_SIZE
+ ) and not _checkTexture2D(
+ internalFormat, (hData, wData), format_, type_
+ ):
# Ensure texture size is at least MIN_TEXTURE_SIZE
tH = max(hData, MIN_TEXTURE_SIZE)
tW = max(wData, MIN_TEXTURE_SIZE)
- uMax, vMax = float(wData)/tW, float(hData)/tH
+ uMax, vMax = float(wData) / tW, float(hData) / tH
# TODO issue with type_ and alignment
- texture = Texture(internalFormat,
- data=None,
- format_=format_,
- shape=(tH, tW),
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
+ texture = Texture(
+ internalFormat,
+ data=None,
+ format_=format_,
+ shape=(tH, tW),
+ texUnit=texUnit,
+ minFilter=self._MIN_FILTER,
+ magFilter=self._MAG_FILTER,
+ wrap=self._WRAP,
+ )
# TODO handle unpack
- texture.update(format_,
- data[yOrig:yOrig+hData,
- xOrig:xOrig+wData])
+ texture.update(
+ format_, data[yOrig : yOrig + hData, xOrig : xOrig + wData]
+ )
# texture.update(format_, type_, data,
# width=wData, height=hData,
# unpackRowLength=width,
@@ -160,28 +178,41 @@ class Image(object):
# TODO issue with type_ and unpacking tiles
# TODO idea to handle unpack: use array strides
# As it is now, it will make a copy
- texture = Texture(internalFormat,
- data[yOrig:yOrig+hData,
- xOrig:xOrig+wData],
- format_,
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
+ texture = Texture(
+ internalFormat,
+ data[yOrig : yOrig + hData, xOrig : xOrig + wData],
+ format_,
+ texUnit=texUnit,
+ minFilter=self._MIN_FILTER,
+ magFilter=self._MAG_FILTER,
+ wrap=self._WRAP,
+ )
# TODO
# unpackRowLength=width,
# unpackSkipPixels=xOrig,
# unpackSkipRows=yOrig)
- vertices = numpy.array((
- (xOrig, yOrig, 0., 0.),
- (xOrig + wData, yOrig, uMax, 0.),
- (xOrig, yOrig + hData, 0., vMax),
- (xOrig + wData, yOrig + hData, uMax, vMax)),
- dtype=numpy.float32)
+ vertices = numpy.array(
+ (
+ (xOrig, yOrig, 0.0, 0.0),
+ (xOrig + wData, yOrig, uMax, 0.0),
+ (xOrig, yOrig + hData, 0.0, vMax),
+ (xOrig + wData, yOrig + hData, uMax, vMax),
+ ),
+ dtype=numpy.float32,
+ )
texture.prepare()
- tiles.append((texture, vertices,
- {'xOrigData': xOrig, 'yOrigData': yOrig,
- 'wData': wData, 'hData': hData}))
+ tiles.append(
+ (
+ texture,
+ vertices,
+ {
+ "xOrigData": xOrig,
+ "yOrigData": yOrig,
+ "wData": wData,
+ "hData": hData,
+ },
+ )
+ )
xOrig += wData
yOrig += hData
self.tiles = tuple(tiles)
@@ -192,7 +223,7 @@ class Image(object):
del self.tiles
def updateAll(self, format_, data, texUnit=0):
- if not hasattr(self, 'tiles'):
+ if not hasattr(self, "tiles"):
raise RuntimeError("No texture, discard has already been called")
assert data.shape[:2] == (self.height, self.width)
@@ -200,11 +231,13 @@ class Image(object):
self.tiles[0][0].update(format_, data, texUnit=texUnit)
else:
for texture, _, info in self.tiles:
- yOrig, xOrig = info['yOrigData'], info['xOrigData']
- height, width = info['hData'], info['wData']
- texture.update(format_,
- data[yOrig:yOrig+height, xOrig:xOrig+width],
- texUnit=texUnit)
+ yOrig, xOrig = info["yOrigData"], info["xOrigData"]
+ height, width = info["hData"], info["wData"]
+ texture.update(
+ format_,
+ data[yOrig : yOrig + height, xOrig : xOrig + width],
+ texUnit=texUnit,
+ )
texture.prepare()
# TODO check
# width=info['wData'], height=info['hData'],
@@ -224,18 +257,13 @@ class Image(object):
stride = vertices.shape[-1] * vertices.itemsize
gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
-
- texCoordsPtr = c_void_p(vertices.ctypes.data +
- 2 * vertices.itemsize)
+ gl.glVertexAttribPointer(
+ posAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, stride, vertices
+ )
+
+ texCoordsPtr = c_void_p(vertices.ctypes.data + 2 * vertices.itemsize)
gl.glEnableVertexAttribArray(texAttrib)
- gl.glVertexAttribPointer(texAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, texCoordsPtr)
+ gl.glVertexAttribPointer(
+ texAttrib, 2, gl.GL_FLOAT, gl.GL_FALSE, stride, texCoordsPtr
+ )
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
diff --git a/src/silx/gui/plot/backends/glutils/PlotImageFile.py b/src/silx/gui/plot/backends/glutils/PlotImageFile.py
index 5fb6853..1622122 100644
--- a/src/silx/gui/plot/backends/glutils/PlotImageFile.py
+++ b/src/silx/gui/plot/backends/glutils/PlotImageFile.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,12 +30,14 @@ __date__ = "03/04/2017"
import base64
import struct
-import sys
import zlib
+from fabio.TiffIO import TiffIO
+
# Image writer ################################################################
+
def convertRGBDataToPNG(data):
"""Convert a RGB bitmap to PNG.
@@ -54,29 +55,42 @@ def convertRGBDataToPNG(data):
colorType = 2 # 'truecolor' = RGB
interlace = 0 # No
- IHDRdata = struct.pack(">ccccIIBBBBB", b'I', b'H', b'D', b'R',
- width, height, depth, colorType,
- 0, 0, interlace)
+ IHDRdata = struct.pack(
+ ">ccccIIBBBBB",
+ b"I",
+ b"H",
+ b"D",
+ b"R",
+ width,
+ height,
+ depth,
+ colorType,
+ 0,
+ 0,
+ interlace,
+ )
# Add filter 'None' before each scanline
- preparedData = b'\x00' + b'\x00'.join(line.tobytes() for line in data)
+ preparedData = b"\x00" + b"\x00".join(line.tobytes() for line in data)
compressedData = zlib.compress(preparedData, 8)
- IDATdata = struct.pack("cccc", b'I', b'D', b'A', b'T')
+ IDATdata = struct.pack("cccc", b"I", b"D", b"A", b"T")
IDATdata += compressedData
- return b''.join([
- b'\x89PNG\r\n\x1a\n', # PNG signature
- # IHDR chunk: Image Header
- struct.pack(">I", 13), # length
- IHDRdata,
- struct.pack(">I", zlib.crc32(IHDRdata) & 0xffffffff), # CRC
- # IDAT chunk: Payload
- struct.pack(">I", len(compressedData)),
- IDATdata,
- struct.pack(">I", zlib.crc32(IDATdata) & 0xffffffff), # CRC
- b'\x00\x00\x00\x00IEND\xaeB`\x82' # IEND chunk: footer
- ])
+ return b"".join(
+ [
+ b"\x89PNG\r\n\x1a\n", # PNG signature
+ # IHDR chunk: Image Header
+ struct.pack(">I", 13), # length
+ IHDRdata,
+ struct.pack(">I", zlib.crc32(IHDRdata) & 0xFFFFFFFF), # CRC
+ # IDAT chunk: Payload
+ struct.pack(">I", len(compressedData)),
+ IDATdata,
+ struct.pack(">I", zlib.crc32(IDATdata) & 0xFFFFFFFF), # CRC
+ b"\x00\x00\x00\x00IEND\xaeB`\x82", # IEND chunk: footer
+ ]
+ )
def saveImageToFile(data, fileNameOrObj, fileFormat):
@@ -90,64 +104,56 @@ def saveImageToFile(data, fileNameOrObj, fileFormat):
"""
assert len(data.shape) == 3
assert data.shape[2] == 3
- assert fileFormat in ('png', 'ppm', 'svg', 'tiff')
+ assert fileFormat in ("png", "ppm", "svg", "tif", "tiff")
- if not hasattr(fileNameOrObj, 'write'):
- if sys.version_info < (3, ):
+ if not hasattr(fileNameOrObj, "write"):
+ if fileFormat in ("png", "ppm", "tiff"):
+ # Open in binary mode
fileObj = open(fileNameOrObj, "wb")
else:
- if fileFormat in ('png', 'ppm', 'tiff'):
- # Open in binary mode
- fileObj = open(fileNameOrObj, 'wb')
- else:
- fileObj = open(fileNameOrObj, 'w', newline='')
+ fileObj = open(fileNameOrObj, "w", newline="")
else: # Use as a file-like object
fileObj = fileNameOrObj
- if fileFormat == 'svg':
+ if fileFormat == "svg":
height, width = data.shape[:2]
base64Data = base64.b64encode(convertRGBDataToPNG(data))
- fileObj.write(
- '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n')
+ fileObj.write('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n')
fileObj.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n')
- fileObj.write(
- ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
+ fileObj.write(' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
fileObj.write('<svg xmlns:xlink="http://www.w3.org/1999/xlink"\n')
fileObj.write(' xmlns="http://www.w3.org/2000/svg"\n')
fileObj.write(' version="1.1"\n')
fileObj.write(' width="%d"\n' % width)
fileObj.write(' height="%d">\n' % height)
fileObj.write(' <image xlink:href="data:image/png;base64,')
- fileObj.write(base64Data.decode('ascii'))
+ fileObj.write(base64Data.decode("ascii"))
fileObj.write('"\n')
fileObj.write(' x="0"\n')
fileObj.write(' y="0"\n')
fileObj.write(' width="%d"\n' % width)
fileObj.write(' height="%d"\n' % height)
fileObj.write(' id="image" />\n')
- fileObj.write('</svg>')
+ fileObj.write("</svg>")
- elif fileFormat == 'ppm':
+ elif fileFormat == "ppm":
height, width = data.shape[:2]
- fileObj.write(b'P6\n')
- fileObj.write(b'%d %d\n' % (width, height))
- fileObj.write(b'255\n')
+ fileObj.write(b"P6\n")
+ fileObj.write(b"%d %d\n" % (width, height))
+ fileObj.write(b"255\n")
fileObj.write(data.tobytes())
- elif fileFormat == 'png':
+ elif fileFormat == "png":
fileObj.write(convertRGBDataToPNG(data))
- elif fileFormat == 'tiff':
+ elif fileFormat in ("tif", "tiff"):
if fileObj == fileNameOrObj:
- raise NotImplementedError(
- 'Save TIFF to a file-like object not implemented')
-
- from silx.third_party.TiffIO import TiffIO
+ raise NotImplementedError("Save TIFF to a file-like object not implemented")
- tif = TiffIO(fileNameOrObj, mode='wb+')
- tif.writeImage(data, info={'Title': 'OpenGL Plot Snapshot'})
+ tif = TiffIO(fileNameOrObj, mode="wb+")
+ tif.writeImage(data, info={"Title": "OpenGL Plot Snapshot"})
if fileObj != fileNameOrObj:
fileObj.close()
diff --git a/src/silx/gui/plot/backends/glutils/__init__.py b/src/silx/gui/plot/backends/glutils/__init__.py
index f87d7c1..bc15b78 100644
--- a/src/silx/gui/plot/backends/glutils/__init__.py
+++ b/src/silx/gui/plot/backends/glutils/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/items/__init__.py b/src/silx/gui/plot/items/__init__.py
index 0fe29c2..bbb4220 100644
--- a/src/silx/gui/plot/items/__init__.py
+++ b/src/silx/gui/plot/items/__init__.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2022 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,22 +31,50 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "22/06/2017"
-from .core import (Item, DataItem, # noqa
- LabelsMixIn, DraggableMixIn, ColormapMixIn, # noqa
- SymbolMixIn, ColorMixIn, YAxisMixIn, FillMixIn, # noqa
- AlphaMixIn, LineMixIn, ScatterVisualizationMixIn, # noqa
- ComplexMixIn, ItemChangedType, PointsBase) # noqa
+from .core import (
+ Item,
+ DataItem, # noqa
+ LabelsMixIn,
+ DraggableMixIn,
+ ColormapMixIn,
+ LineGapColorMixIn, # noqa
+ SymbolMixIn,
+ ColorMixIn,
+ YAxisMixIn,
+ FillMixIn, # noqa
+ AlphaMixIn,
+ LineMixIn,
+ ScatterVisualizationMixIn, # noqa
+ ComplexMixIn,
+ ItemChangedType,
+ PointsBase,
+) # noqa
from .complex import ImageComplexData # noqa
from .curve import Curve, CurveStyle # noqa
from .histogram import Histogram # noqa
-from .image import ImageBase, ImageData, ImageDataBase, ImageRgba, ImageStack, MaskImageData # noqa
+from .image import (
+ ImageBase,
+ ImageData,
+ ImageDataBase,
+ ImageRgba,
+ ImageStack,
+ MaskImageData,
+) # noqa
from .image_aggregated import ImageDataAggregated # noqa
-from .shape import Shape, BoundingRect, XAxisExtent, YAxisExtent # noqa
+from .shape import Line, Shape, BoundingRect, XAxisExtent, YAxisExtent # noqa
from .scatter import Scatter # noqa
from .marker import MarkerBase, Marker, XMarker, YMarker # noqa
from .axis import Axis, XAxis, YAxis, YRightAxis
-DATA_ITEMS = (ImageComplexData, Curve, Histogram, ImageBase, Scatter,
- BoundingRect, XAxisExtent, YAxisExtent)
+DATA_ITEMS = (
+ ImageComplexData,
+ Curve,
+ Histogram,
+ ImageBase,
+ Scatter,
+ BoundingRect,
+ XAxisExtent,
+ YAxisExtent,
+)
"""Classes of items representing data and to consider to compute data bounds.
"""
diff --git a/src/silx/gui/plot/items/_arc_roi.py b/src/silx/gui/plot/items/_arc_roi.py
index 23416ec..658573a 100644
--- a/src/silx/gui/plot/items/_arc_roi.py
+++ b/src/silx/gui/plot/items/_arc_roi.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,6 +30,8 @@ __date__ = "28/06/2018"
import logging
import numpy
+import enum
+from typing import Tuple
from ... import utils
from .. import items
@@ -51,8 +52,18 @@ class _ArcGeometry:
The aim is is to switch between consistent state without dealing with
intermediate values.
"""
- def __init__(self, center, startPoint, endPoint, radius,
- weight, startAngle, endAngle, closed=False):
+
+ def __init__(
+ self,
+ center,
+ startPoint,
+ endPoint,
+ radius,
+ weight,
+ startAngle,
+ endAngle,
+ closed=False,
+ ):
"""Constructor for a consistent arc geometry.
There is also specific class method to create different kind of arc
@@ -69,46 +80,59 @@ class _ArcGeometry:
@classmethod
def createEmpty(cls):
- """Create an arc geometry from an empty shape
- """
+ """Create an arc geometry from an empty shape"""
zero = numpy.array([0, 0])
return cls(zero, zero.copy(), zero.copy(), 0, 0, 0, 0)
@classmethod
def createRect(cls, startPoint, endPoint, weight):
- """Create an arc geometry from a definition of a rectangle
- """
+ """Create an arc geometry from a definition of a rectangle"""
return cls(None, startPoint, endPoint, None, weight, None, None, False)
@classmethod
- def createCircle(cls, center, startPoint, endPoint, radius,
- weight, startAngle, endAngle):
- """Create an arc geometry from a definition of a circle
- """
- return cls(center, startPoint, endPoint, radius,
- weight, startAngle, endAngle, True)
+ def createCircle(
+ cls, center, startPoint, endPoint, radius, weight, startAngle, endAngle
+ ):
+ """Create an arc geometry from a definition of a circle"""
+ return cls(
+ center, startPoint, endPoint, radius, weight, startAngle, endAngle, True
+ )
def withWeight(self, weight):
- """Return a new geometry based on this object, with a specific weight
- """
- return _ArcGeometry(self.center, self.startPoint, self.endPoint,
- self.radius, weight,
- self.startAngle, self.endAngle, self._closed)
+ """Return a new geometry based on this object, with a specific weight"""
+ return _ArcGeometry(
+ self.center,
+ self.startPoint,
+ self.endPoint,
+ self.radius,
+ weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
def withRadius(self, radius):
"""Return a new geometry based on this object, with a specific radius.
The weight and the center is conserved.
"""
- startPoint = self.center + (self.startPoint - self.center) / self.radius * radius
+ startPoint = (
+ self.center + (self.startPoint - self.center) / self.radius * radius
+ )
endPoint = self.center + (self.endPoint - self.center) / self.radius * radius
- return _ArcGeometry(self.center, startPoint, endPoint,
- radius, self.weight,
- self.startAngle, self.endAngle, self._closed)
+ return _ArcGeometry(
+ self.center,
+ startPoint,
+ endPoint,
+ radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
def withStartAngle(self, startAngle):
- """Return a new geometry based on this object, with a specific start angle
- """
+ """Return a new geometry based on this object, with a specific start angle"""
vector = numpy.array([numpy.cos(startAngle), numpy.sin(startAngle)])
startPoint = self.center + vector * self.radius
@@ -132,8 +156,7 @@ class _ArcGeometry:
)
def withEndAngle(self, endAngle):
- """Return a new geometry based on this object, with a specific end angle
- """
+ """Return a new geometry based on this object, with a specific end angle"""
vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
endPoint = self.center + vector * self.radius
@@ -162,9 +185,16 @@ class _ArcGeometry:
center = None if self.center is None else self.center + delta
startPoint = None if self.startPoint is None else self.startPoint + delta
endPoint = None if self.endPoint is None else self.endPoint + delta
- return _ArcGeometry(center, startPoint, endPoint,
- self.radius, self.weight,
- self.startAngle, self.endAngle, self._closed)
+ return _ArcGeometry(
+ center,
+ startPoint,
+ endPoint,
+ self.radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
def getKind(self):
"""Returns the kind of shape defined"""
@@ -192,14 +222,18 @@ class _ArcGeometry:
return self._closed
def __str__(self):
- return str((self.center,
- self.startPoint,
- self.endPoint,
- self.radius,
- self.weight,
- self.startAngle,
- self.endAngle,
- self._closed))
+ return str(
+ (
+ self.center,
+ self.startPoint,
+ self.endPoint,
+ self.radius,
+ self.weight,
+ self.startAngle,
+ self.endAngle,
+ self._closed,
+ )
+ )
class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
@@ -211,19 +245,37 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
- 1 anchor to translate the shape.
"""
- ICON = 'add-shape-arc'
- NAME = 'arc ROI'
+ ICON = "add-shape-arc"
+ NAME = "arc ROI"
SHORT_NAME = "arc"
"""Metadata for this kind of ROI"""
_plotShape = "line"
"""Plot shape which is used for the first interaction"""
- ThreePointMode = RoiInteractionMode("3 points", "Provides 3 points to define the main radius circle")
- PolarMode = RoiInteractionMode("Polar", "Provides anchors to edit the ROI in polar coords")
+ ThreePointMode = RoiInteractionMode(
+ "3 points", "Provides 3 points to define the main radius circle"
+ )
+ PolarMode = RoiInteractionMode(
+ "Polar", "Provides anchors to edit the ROI in polar coords"
+ )
# FIXME: MoveMode was designed cause there is too much anchors
# FIXME: It would be good replace it by a dnd on the shape
- MoveMode = RoiInteractionMode("Translation", "Provides anchors to only move the ROI")
+ MoveMode = RoiInteractionMode(
+ "Translation", "Provides anchors to only move the ROI"
+ )
+
+ class Role(enum.Enum):
+ """Identify a set of roles which can be used for now to reach some positions"""
+
+ START = 0
+ """Location of the anchor at the start of the arc"""
+ STOP = 1
+ """Location of the anchor at the stop of the arc"""
+ MIDDLE = 2
+ """Location of the anchor at the middle of the arc"""
+ CENTER = 3
+ """Location of the center of the circle"""
def __init__(self, parent=None):
HandleBasedROI.__init__(self, parent=parent)
@@ -266,22 +318,28 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
:param RoiInteractionMode modeId:
"""
if modeId is self.ThreePointMode:
+ self._handleStart.setVisible(True)
+ self._handleEnd.setVisible(True)
+ self._handleWeight.setVisible(True)
self._handleStart.setSymbol("s")
self._handleMid.setSymbol("s")
self._handleEnd.setSymbol("s")
self._handleWeight.setSymbol("d")
self._handleMove.setSymbol("+")
elif modeId is self.PolarMode:
+ self._handleStart.setVisible(True)
+ self._handleEnd.setVisible(True)
+ self._handleWeight.setVisible(True)
self._handleStart.setSymbol("o")
self._handleMid.setSymbol("o")
self._handleEnd.setSymbol("o")
self._handleWeight.setSymbol("d")
self._handleMove.setSymbol("+")
elif modeId is self.MoveMode:
- self._handleStart.setSymbol("")
+ self._handleStart.setVisible(False)
+ self._handleEnd.setVisible(False)
+ self._handleWeight.setVisible(False)
self._handleMid.setSymbol("+")
- self._handleEnd.setSymbol("")
- self._handleWeight.setSymbol("")
self._handleMove.setSymbol("+")
else:
assert False
@@ -303,7 +361,7 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self.__shape.setLineWidth(style.getLineWidth())
def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
+ """Initialize the ROI using the points from the first interaction.
This interaction is constrained by the plot API and only supports few
shapes.
@@ -368,7 +426,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
elif geometry.center is not None:
midAngle = (geometry.startAngle + geometry.endAngle) * 0.5
vector = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
- weightPos = geometry.center + (geometry.radius + geometry.weight * 0.5) * vector
+ weightPos = (
+ geometry.center + (geometry.radius + geometry.weight * 0.5) * vector
+ )
with utils.blockSignals(self._handleWeight):
self._handleWeight.setPosition(*weightPos)
@@ -394,7 +454,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self._updateWeightHandle()
self._updateShape()
- def _updateCurvature(self, start, mid, end, updateCurveHandles, checkClosed=False, updateStart=False):
+ def _updateCurvature(
+ self, start, mid, end, updateCurveHandles, checkClosed=False, updateStart=False
+ ):
"""Update the curvature using 3 control points in the curve
:param bool updateCurveHandles: If False curve handles are already at
@@ -419,7 +481,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
self._handleEnd.setPosition(*end)
weight = self._geometry.weight
- geometry = self._createGeometryFromControlPoints(start, mid, end, weight, closed=closed)
+ geometry = self._createGeometryFromControlPoints(
+ start, mid, end, weight, closed=closed
+ )
self._geometry = geometry
self._updateWeightHandle()
@@ -434,10 +498,10 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
sign = 1 if geometry.startAngle < geometry.endAngle else -1
if updateStart:
geometry.startPoint = geometry.endPoint
- geometry.startAngle = geometry.endAngle - sign * 2*numpy.pi
+ geometry.startAngle = geometry.endAngle - sign * 2 * numpy.pi
else:
geometry.endPoint = geometry.startPoint
- geometry.endAngle = geometry.startAngle + sign * 2*numpy.pi
+ geometry.endAngle = geometry.startAngle + sign * 2 * numpy.pi
def handleDragUpdated(self, handle, origin, previous, current):
modeId = self.getInteractionMode()
@@ -446,8 +510,12 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
mid = numpy.array(self._handleMid.getPosition())
end = numpy.array(self._handleEnd.getPosition())
self._updateCurvature(
- current, mid, end, checkClosed=True, updateStart=True,
- updateCurveHandles=False
+ current,
+ mid,
+ end,
+ checkClosed=True,
+ updateStart=True,
+ updateCurveHandles=False,
)
elif modeId is self.PolarMode:
v = current - self._geometry.center
@@ -478,8 +546,12 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
start = numpy.array(self._handleStart.getPosition())
mid = numpy.array(self._handleMid.getPosition())
self._updateCurvature(
- start, mid, current, checkClosed=True, updateStart=False,
- updateCurveHandles=False
+ start,
+ mid,
+ current,
+ checkClosed=True,
+ updateStart=False,
+ updateCurveHandles=False,
)
elif modeId is self.PolarMode:
v = current - self._geometry.center
@@ -512,8 +584,7 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) < 15
def _normalizeGeometry(self):
- """Keep the same phisical geometry, but with normalized parameters.
- """
+ """Keep the same phisical geometry, but with normalized parameters."""
geometry = self._geometry
if geometry.weight * 0.5 >= geometry.radius:
radius = (geometry.weight * 0.5 + geometry.radius) * 0.5
@@ -583,8 +654,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
if endAngle > startAngle:
endAngle -= 2 * numpy.pi
- return _ArcGeometry(center, start, end,
- radius, weight, startAngle, endAngle)
+ return _ArcGeometry(
+ center, start, end, radius, weight, startAngle, endAngle
+ )
def _createShapeFromGeometry(self, geometry):
kind = geometry.getKind()
@@ -596,11 +668,14 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
distance = numpy.linalg.norm(normal)
if distance != 0:
normal /= distance
- points = numpy.array([
- geometry.startPoint + normal * geometry.weight * 0.5,
- geometry.endPoint + normal * geometry.weight * 0.5,
- geometry.endPoint - normal * geometry.weight * 0.5,
- geometry.startPoint - normal * geometry.weight * 0.5])
+ points = numpy.array(
+ [
+ geometry.startPoint + normal * geometry.weight * 0.5,
+ geometry.endPoint + normal * geometry.weight * 0.5,
+ geometry.endPoint - normal * geometry.weight * 0.5,
+ geometry.startPoint - normal * geometry.weight * 0.5,
+ ]
+ )
elif kind == "point":
# It is not an arc
# but we can display it as an intermediate shape
@@ -635,7 +710,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
innerRadius = geometry.radius - geometry.weight * 0.5
outerRadius = geometry.radius + geometry.weight * 0.5
- delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
+ sign = numpy.sign(geometry.endAngle - geometry.startAngle)
+ delta = min(0.1, abs(geometry.startAngle - geometry.endAngle) / 100) * sign
+
if geometry.startAngle == geometry.endAngle:
# Degenerated, it's a line (single radius)
angle = geometry.startAngle
@@ -654,7 +731,6 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
points = []
points.append(geometry.center)
points.append(geometry.startPoint)
- delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
for angle in angles:
direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
points.append(geometry.center + direction * outerRadius)
@@ -712,7 +788,29 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
geometry = self._geometry
if geometry.center is None:
raise ValueError("This ROI can't be represented as a section of circle")
- return geometry.center, self.getInnerRadius(), self.getOuterRadius(), geometry.startAngle, geometry.endAngle
+ return (
+ geometry.center,
+ self.getInnerRadius(),
+ self.getOuterRadius(),
+ geometry.startAngle,
+ geometry.endAngle,
+ )
+
+ def getPosition(self, role: Role = Role.CENTER) -> Tuple[float, float]:
+ """Returns a position by it's role.
+
+ By default returns the center of the circle of the arc ROI.
+ """
+ if role == self.Role.START:
+ return self._handleStart.getPosition()
+ if role == self.Role.STOP:
+ return self._handleEnd.getPosition()
+ if role == self.Role.MIDDLE:
+ return self._handleMid.getPosition()
+ if role == self.Role.CENTER:
+ p = self.getCenter()
+ return p[0], p[1]
+ raise ValueError(f"{role} is not supported")
def isClosed(self):
"""Returns true if the arc is a closed shape, like a circle or a donut.
@@ -795,9 +893,16 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
endPoint = center + vector * radius
- geometry = _ArcGeometry(center, startPoint, endPoint,
- radius, weight,
- startAngle, endAngle, closed=None)
+ geometry = _ArcGeometry(
+ center,
+ startPoint,
+ endPoint,
+ radius,
+ weight,
+ startAngle,
+ endAngle,
+ closed=None,
+ )
self._geometry = geometry
self._updateHandles()
@@ -805,7 +910,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
def contains(self, position):
# first check distance, fastest
center = self.getCenter()
- distance = numpy.sqrt((position[1] - center[1]) ** 2 + ((position[0] - center[0])) ** 2)
+ distance = numpy.sqrt(
+ (position[1] - center[1]) ** 2 + ((position[0] - center[0])) ** 2
+ )
is_in_distance = self.getInnerRadius() <= distance <= self.getOuterRadius()
if not is_in_distance:
return False
@@ -871,8 +978,15 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
def __str__(self):
try:
center, innerRadius, outerRadius, startAngle, endAngle = self.getGeometry()
- params = center[0], center[1], innerRadius, outerRadius, startAngle, endAngle
- params = 'center: %f %f; radius: %f %f; angles: %f %f' % params
+ params = (
+ center[0],
+ center[1],
+ innerRadius,
+ outerRadius,
+ startAngle,
+ endAngle,
+ )
+ params = "center: %f %f; radius: %f %f; angles: %f %f" % params
except ValueError:
params = "invalid"
return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/src/silx/gui/plot/items/_band_roi.py b/src/silx/gui/plot/items/_band_roi.py
new file mode 100644
index 0000000..0d2ad4e
--- /dev/null
+++ b/src/silx/gui/plot/items/_band_roi.py
@@ -0,0 +1,376 @@
+# /*##########################################################################
+#
+# Copyright (c) 2022 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.
+#
+# ###########################################################################*/
+"""Rectangular ROI that can be rotated"""
+
+import functools
+import logging
+from typing import Iterable, List, NamedTuple, Optional, Sequence, Tuple
+import numpy
+
+from ... import qt, utils
+from .. import items
+from ...colors import rgba
+from silx.image.shapes import Polygon
+from ....utils.proxy import docstring
+from ._roi_base import _RegionOfInterestBase
+from ._roi_base import HandleBasedROI
+from ._roi_base import InteractionModeMixIn
+from ._roi_base import RoiInteractionMode
+
+
+logger = logging.getLogger(__name__)
+
+
+class Point(NamedTuple):
+ x: float
+ y: float
+
+
+class BandGeometry(NamedTuple):
+ begin: Point
+ end: Point
+ width: float
+
+ @staticmethod
+ def create(
+ begin: Sequence[float] = (0.0, 0.0),
+ end: Sequence[float] = (0.0, 0.0),
+ width: Optional[float] = None,
+ ):
+ begin = Point(float(begin[0]), float(begin[1]))
+ end = Point(float(end[0]), float(end[1]))
+ if width is None:
+ width = 0.1 * numpy.linalg.norm(numpy.array(end) - begin)
+ return BandGeometry(begin, end, max(0.0, float(width)))
+
+ @property
+ @functools.lru_cache()
+ def normal(self) -> Point:
+ vector = numpy.array(self.end) - self.begin
+ length = numpy.linalg.norm(vector)
+ if length == 0:
+ return Point(0.0, 0.0)
+ return Point(-vector[1] / length, vector[0] / length)
+
+ @property
+ @functools.lru_cache()
+ def center(self) -> Point:
+ return Point(*(0.5 * (numpy.array(self.begin) + self.end)))
+
+ @property
+ @functools.lru_cache()
+ def corners(self) -> Tuple[Point, Point, Point, Point]:
+ """Returns a 4-uple of (x,y) position in float"""
+ offset = 0.5 * self.width * numpy.array(self.normal)
+ return tuple(
+ map(
+ lambda p: Point(*p),
+ (
+ self.begin - offset,
+ self.begin + offset,
+ self.end + offset,
+ self.end - offset,
+ ),
+ )
+ )
+
+ @property
+ @functools.lru_cache()
+ def slope(self) -> float:
+ """Slope of the line (begin, end), infinity for a vertical line"""
+ if self.begin.x == self.end.x:
+ return float("inf")
+ return (self.end.y - self.begin.y) / (self.end.x - self.begin.x)
+
+ @property
+ @functools.lru_cache()
+ def intercept(self) -> float:
+ """Intercept of the line (begin, end) or value of x for vertical line"""
+ if self.begin.x == self.end.x:
+ return self.begin.x
+ return self.begin.y - self.slope * self.begin.x
+
+ @property
+ @functools.lru_cache()
+ def edgesIntercept(self) -> Tuple[float, float]:
+ """Intercepts of lines describing band edges"""
+ offset = 0.5 * self.width * numpy.array(self.normal)
+ if self.begin.x == self.end.x:
+ return self.begin.x - offset[0], self.begin.x + offset[0]
+ return (
+ self.begin.y - offset[1] - self.slope * (self.begin.x - offset[0]),
+ self.begin.y + offset[1] - self.slope * (self.begin.x + offset[0]),
+ )
+
+ def contains(self, position: Sequence[float]) -> bool:
+ return Polygon(self.corners).is_inside(*position)
+
+
+class BandROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
+ """A ROI identifying a line in a 2D plot.
+
+ This ROI provides 1 anchor for each boundary of the line, plus an center
+ in the center to translate the full ROI.
+ """
+
+ ICON = "add-shape-rotated-rectangle"
+ NAME = "band ROI"
+ SHORT_NAME = "band"
+ """Metadata for this kind of ROI"""
+
+ _plotShape = "line"
+ """Plot shape which is used for the first interaction"""
+
+ BoundedMode = RoiInteractionMode("Bounded", "Band is bounded on both sides")
+ """Interaction mode for a rectangular band ROI"""
+
+ UnboundedMode = RoiInteractionMode("Unbounded", "Band is unbounded on both sides")
+ """Interaction mode for unlimited band ROI """
+
+ def __init__(self, parent=None):
+ HandleBasedROI.__init__(self, parent=parent)
+ items.LineMixIn.__init__(self)
+ self.__availableInteractionModes = set((self.BoundedMode, self.UnboundedMode))
+ InteractionModeMixIn.__init__(self)
+
+ self.__handleBegin = self.addHandle()
+ self.__handleEnd = self.addHandle()
+ self.__handleCenter = self.addTranslateHandle()
+ self.__handleLabel = self.addLabelHandle()
+ self.__handleWidthUp = self.addHandle()
+ self.__handleWidthUp._setConstraint(self.__handleWidthUpConstraint)
+ self.__handleWidthUp.setSymbol("d")
+ self.__handleWidthDown = self.addHandle()
+ self.__handleWidthDown._setConstraint(self.__handleWidthDownConstraint)
+ self.__handleWidthDown.setSymbol("d")
+
+ self.__geometry = BandGeometry.create()
+
+ self.__lineUp = items.Line()
+ self.__lineUp.setVisible(False)
+ self.__lineMiddle = items.Line()
+ self.__lineMiddle.setLineWidth(1)
+ self.__lineMiddle.setVisible(False)
+ self.__lineDown = items.Line()
+ self.__lineDown.setVisible(False)
+
+ self.__shape = items.Shape("polygon")
+ self.__shape.setPoints(self.__geometry.corners)
+ self.__shape.setFill(False)
+
+ for item in (self.__lineUp, self.__lineMiddle, self.__lineDown, self.__shape):
+ item.setColor(rgba(self.getColor()))
+ item.setOverlay(True)
+ item.setLineStyle(self.getLineStyle())
+ if item != self.__lineMiddle:
+ item.setLineWidth(self.getLineWidth())
+ self.addItem(item)
+
+ self._initInteractionMode(self.BoundedMode)
+ self._interactiveModeUpdated(self.BoundedMode)
+
+ def availableInteractionModes(self) -> List[RoiInteractionMode]:
+ """Returns the list of available interaction modes"""
+ return list(self.__availableInteractionModes)
+
+ def setAvailableInteractionModes(self, modes: Iterable[RoiInteractionMode]) -> None:
+ """Allows to restrict interaction modes of the ROI.
+
+ :param modes: Subset of BandROI interaction modes:
+ :attr:`BoundedMode` and :attr:`UnboundedMode`.
+ """
+ modes = set(modes)
+ if not modes <= set((self.BoundedMode, self.UnboundedMode)):
+ raise ValueError("Unsupported interaction modes")
+ self.__availableInteractionModes = set(modes)
+ if self.getInteractionMode() not in self.__availableInteractionModes:
+ self.setInteractionMode(self.availableInteractionModes()[0])
+
+ def _interactiveModeUpdated(self, modeId: RoiInteractionMode):
+ """Set the interaction mode."""
+ if modeId is self.BoundedMode:
+ self.__lineDown.setVisible(False)
+ self.__lineMiddle.setVisible(False)
+ self.__lineUp.setVisible(False)
+ self.__shape.setVisible(True)
+ elif modeId is self.UnboundedMode:
+ self.__lineDown.setVisible(True)
+ self.__lineMiddle.setVisible(True)
+ self.__lineUp.setVisible(True)
+ self.__shape.setVisible(False)
+ else:
+ raise RuntimeError("Unsupported interactive mode")
+
+ def _updated(self, event=None, checkVisibility=True):
+ if event == items.ItemChangedType.VISIBLE:
+ if self.isVisible():
+ self._interactiveModeUpdated(self.getInteractionMode())
+ else:
+ self.__lineDown.setVisible(False)
+ self.__lineMiddle.setVisible(False)
+ self.__lineUp.setVisible(False)
+ self.__shape.setVisible(False)
+ super()._updated(event, checkVisibility)
+
+ def _updatedStyle(self, event, style):
+ super()._updatedStyle(event, style)
+ for item in (self.__lineUp, self.__lineMiddle, self.__lineDown, self.__shape):
+ item.setColor(style.getColor())
+ item.setLineStyle(style.getLineStyle())
+ if item != self.__lineMiddle:
+ item.setLineWidth(style.getLineWidth())
+
+ def setFirstShapePoints(self, points):
+ assert len(points) == 2
+ self.setGeometry(*points)
+
+ def _updateText(self, text):
+ self.__handleLabel.setText(text)
+
+ def getGeometry(self) -> BandGeometry:
+ """Returns the geometric description of the ROI"""
+ return self.__geometry
+
+ def setGeometry(
+ self,
+ begin: Sequence[float],
+ end: Sequence[float],
+ width: Optional[float] = None,
+ ):
+ """Set the geometry of the ROI
+
+ :param begin: Starting point as (x, y)
+ :paran end: Closing point as (x, y)
+ :param width: Width of the ROI
+ """
+ geometry = BandGeometry.create(begin, end, width)
+ if self.__geometry == geometry:
+ return
+
+ self.__geometry = geometry
+
+ with utils.blockSignals(self.__handleBegin):
+ self.__handleBegin.setPosition(*geometry.begin)
+ with utils.blockSignals(self.__handleEnd):
+ self.__handleEnd.setPosition(*geometry.end)
+ with utils.blockSignals(self.__handleCenter):
+ self.__handleCenter.setPosition(*geometry.center)
+ with utils.blockSignals(self.__handleLabel):
+ lowerCorner = geometry.corners[numpy.array(geometry.corners)[:, 1].argmin()]
+ self.__handleLabel.setPosition(*lowerCorner)
+
+ delta = 0.5 * geometry.width * numpy.array(geometry.normal)
+ with utils.blockSignals(self.__handleWidthUp):
+ self.__handleWidthUp.setPosition(*(geometry.center + delta))
+ with utils.blockSignals(self.__handleWidthDown):
+ self.__handleWidthDown.setPosition(*(geometry.center - delta))
+
+ self.__lineDown.setSlope(geometry.slope)
+ self.__lineDown.setIntercept(geometry.edgesIntercept[0])
+ self.__lineMiddle.setSlope(geometry.slope)
+ self.__lineMiddle.setIntercept(geometry.intercept)
+ self.__lineUp.setSlope(geometry.slope)
+ self.__lineUp.setIntercept(geometry.edgesIntercept[1])
+ self.__shape.setPoints(geometry.corners)
+ self.sigRegionChanged.emit()
+
+ def __updateGeometry(
+ self,
+ begin: Optional[Sequence[float]] = None,
+ end: Optional[Sequence[float]] = None,
+ width: Optional[float] = None,
+ ):
+ geometry = self.getGeometry()
+ self.setGeometry(
+ geometry.begin if begin is None else begin,
+ geometry.end if end is None else end,
+ geometry.width if width is None else width,
+ )
+
+ @staticmethod
+ def __snap(
+ point: Tuple[float, float], fixed: Tuple[float, float]
+ ) -> Tuple[float, float]:
+ """Snap point so that vector [point, fixed] snap to direction 0, 45 or 90 degrees
+
+ :return: the snapped point position.
+ """
+ vector = point[0] - fixed[0], point[1] - fixed[1]
+ angle = numpy.arctan2(vector[1], vector[0])
+ snapAngle = numpy.pi / 4 * numpy.round(angle / (numpy.pi / 4))
+ length = numpy.linalg.norm(vector)
+ return (
+ fixed[0] + length * numpy.cos(snapAngle),
+ fixed[1] + length * numpy.sin(snapAngle),
+ )
+
+ def handleDragUpdated(self, handle, origin, previous, current):
+ geometry = self.getGeometry()
+ if handle is self.__handleBegin:
+ if qt.QApplication.keyboardModifiers() & qt.Qt.ShiftModifier:
+ self.__updateGeometry(begin=self.__snap(current, geometry.end))
+ return
+ self.__updateGeometry(begin=current)
+ return
+ if handle is self.__handleEnd:
+ if qt.QApplication.keyboardModifiers() & qt.Qt.ShiftModifier:
+ self.__updateGeometry(end=self.__snap(current, geometry.begin))
+ return
+ self.__updateGeometry(end=current)
+ return
+ if handle is self.__handleCenter:
+ delta = current - previous
+ self.__updateGeometry(geometry.begin + delta, geometry.end + delta)
+ return
+ if handle in (self.__handleWidthUp, self.__handleWidthDown):
+ offset = numpy.dot(geometry.normal, current - previous)
+ if handle is self.__handleWidthDown:
+ offset *= -1
+ self.__updateGeometry(
+ geometry.begin,
+ geometry.end,
+ geometry.width + 2 * offset,
+ )
+
+ def __handleWidthUpConstraint(self, x: float, y: float) -> Tuple[float, float]:
+ geometry = self.getGeometry()
+ offset = max(
+ 0, numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center)
+ )
+ return tuple(geometry.center + offset * numpy.array(geometry.normal))
+
+ def __handleWidthDownConstraint(self, x: float, y: float) -> Tuple[float, float]:
+ geometry = self.getGeometry()
+ offset = max(
+ 0, -numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center)
+ )
+ return tuple(geometry.center - offset * numpy.array(geometry.normal))
+
+ @docstring(_RegionOfInterestBase)
+ def contains(self, position):
+ return self.getGeometry().contains(position)
+
+ def __str__(self):
+ begin, end, width = self.getGeometry()
+ return f"{self.__class__.__name__}(begin=({begin[0]:g}, {begin[1]:g}), end=({end[0]:g}, {end[1]:g}), width={width:g})"
diff --git a/src/silx/gui/plot/items/_pick.py b/src/silx/gui/plot/items/_pick.py
index 8c8e781..631a30a 100644
--- a/src/silx/gui/plot/items/_pick.py
+++ b/src/silx/gui/plot/items/_pick.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2019-2020 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/items/_roi_base.py b/src/silx/gui/plot/items/_roi_base.py
index 3eb6cf4..43c5381 100644
--- a/src/silx/gui/plot/items/_roi_base.py
+++ b/src/silx/gui/plot/items/_roi_base.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -38,14 +37,14 @@ __date__ = "28/06/2018"
import logging
import numpy
import weakref
+import functools
+from typing import Optional
from ....utils.weakref import WeakList
from ... import qt
from .. import items
from ..items import core
from ...colors import rgba
-import silx.utils.deprecation
-from ....utils.proxy import docstring
logger = logging.getLogger(__name__)
@@ -69,8 +68,10 @@ class _RegionOfInterestBase(qt.QObject):
"""
def __init__(self, parent=None):
- qt.QObject.__init__(self, parent=parent)
- self.__name = ''
+ qt.QObject.__init__(self)
+ if parent is not None:
+ self.setParent(parent)
+ self.__name = ""
def getName(self):
"""Returns the name of the ROI
@@ -121,10 +122,12 @@ class RoiInteractionMode(object):
@property
def label(self):
+ """Short name"""
return self._label
@property
def description(self):
+ """Longer description of the interaction mode"""
return self._description
@@ -189,6 +192,28 @@ class InteractionModeMixIn(object):
"""
return self.__modeId
+ def createMenuForInteractionMode(self, parent: qt.QWidget) -> qt.QMenu:
+ """Create a menu providing access to the different interaction modes"""
+ availableModes = self.availableInteractionModes()
+ currentMode = self.getInteractionMode()
+ submenu = qt.QMenu(parent)
+ modeGroup = qt.QActionGroup(parent)
+ modeGroup.setExclusive(True)
+ for mode in availableModes:
+ action = qt.QAction(parent)
+ action.setText(mode.label)
+ action.setToolTip(mode.description)
+ action.setCheckable(True)
+ if mode is currentMode:
+ action.setChecked(True)
+ else:
+ callback = functools.partial(self.setInteractionMode, mode)
+ action.triggered.connect(callback)
+ modeGroup.addAction(action)
+ submenu.addAction(action)
+ submenu.setTitle("Interaction mode")
+ return submenu
+
class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""Object describing a region of interest in a plot.
@@ -197,10 +222,10 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
The RegionOfInterestManager that created this object
"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the curve"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the curve"""
_DEFAULT_HIGHLIGHT_STYLE = items.CurveStyle(linewidth=2)
@@ -226,15 +251,18 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
def __init__(self, parent=None):
# Avoid circular dependency
from ..tools import roi as roi_tools
+
assert parent is None or isinstance(parent, roi_tools.RegionOfInterestManager)
+ # Must be done before _RegionOfInterestBase.__init__
+ self._child = WeakList()
_RegionOfInterestBase.__init__(self, parent)
core.HighlightedMixIn.__init__(self)
- self._color = rgba('red')
+ self.__text = None
+ self._color = rgba("red")
self._editable = False
self._selectable = False
self._focusProxy = None
self._visible = True
- self._child = WeakList()
def _connectToPlot(self, plot):
"""Called after connection to a plot"""
@@ -264,8 +292,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
# Avoid circular dependency
from ..tools import roi as roi_tools
- if (parent is not None and not isinstance(parent, roi_tools.RegionOfInterestManager)):
- raise ValueError('Unsupported parent')
+
+ if parent is not None and not isinstance(
+ parent, roi_tools.RegionOfInterestManager
+ ):
+ raise ValueError("Unsupported parent")
previousParent = self.parent()
if previousParent is not None:
@@ -293,7 +324,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
assert item is not None
self._child.append(item)
- if item.getName() == '':
+ if item.getName() == "":
self._setItemName(item)
manager = self.parent()
if manager is not None:
@@ -353,26 +384,6 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
self._color = color
self._updated(items.ItemChangedType.COLOR)
- @silx.utils.deprecation.deprecated(reason='API modification',
- replacement='getName()',
- since_version=0.12)
- def getLabel(self):
- """Returns the label displayed for this ROI.
-
- :rtype: str
- """
- return self.getName()
-
- @silx.utils.deprecation.deprecated(reason='API modification',
- replacement='setName(name)',
- since_version=0.12)
- def setLabel(self, label):
- """Set the label displayed with this ROI.
-
- :param str label: The text label to display
- """
- self.setName(name=label)
-
def isEditable(self):
"""Returns whether the ROI is editable by the user or not.
@@ -458,6 +469,26 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
self._visible = visible
self._updated(items.ItemChangedType.VISIBLE)
+ def getText(self) -> str:
+ """Returns the currently displayed text for this ROI"""
+ return self.getName() if self.__text is None else self.__text
+
+ def setText(self, text: Optional[str] = None) -> None:
+ """Set the displayed text for this ROI.
+
+ If None (the default), the ROI name is used.
+ """
+ if self.__text != text:
+ self.__text = text
+ self._updated(items.ItemChangedType.TEXT)
+
+ def _updateText(self, text: str) -> None:
+ """Update the text displayed by this ROI
+
+ Override in subclass to custom text display
+ """
+ pass
+
@classmethod
def showFirstInteractionShape(cls):
"""Returns True if the shape created by the first interaction and
@@ -479,7 +510,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
return cls._plotShape
def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
+ """Initialize the ROI using the points from the first interaction.
This interaction is constrained by the plot API and only supports few
shapes.
@@ -487,13 +518,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
raise NotImplementedError()
def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
+ """Called when the ROI creation interaction was started."""
pass
def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
+ """Called when the ROI creation interaction was finalized."""
pass
def _updateItemProperty(self, event, source, destination):
@@ -545,15 +574,23 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
assert False
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.HIGHLIGHTED:
+ if event == items.ItemChangedType.TEXT:
+ self._updateText(self.getText())
+ elif event == items.ItemChangedType.HIGHLIGHTED:
+ for item in self.getItems():
+ zoffset = 1000 if self.isHighlighted() else 0
+ item.setZValue(item._DEFAULT_Z_LAYER + zoffset)
+
style = self.getCurrentStyle()
self._updatedStyle(event, style)
else:
- styleEvents = [items.ItemChangedType.COLOR,
- items.ItemChangedType.LINE_STYLE,
- items.ItemChangedType.LINE_WIDTH,
- items.ItemChangedType.SYMBOL,
- items.ItemChangedType.SYMBOL_SIZE]
+ styleEvents = [
+ items.ItemChangedType.COLOR,
+ items.ItemChangedType.LINE_STYLE,
+ items.ItemChangedType.LINE_WIDTH,
+ items.ItemChangedType.SYMBOL,
+ items.ItemChangedType.SYMBOL_SIZE,
+ ]
if self.isHighlighted():
styleEvents.append(items.ItemChangedType.HIGHLIGHTED_STYLE)
@@ -563,7 +600,11 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
super(RegionOfInterest, self)._updated(event, checkVisibility)
- def _updatedStyle(self, event, style):
+ # Displayed text has changed, send a text event
+ if event == items.ItemChangedType.NAME and self.__text is None:
+ self._updated(items.ItemChangedType.TEXT, checkVisibility)
+
+ def _updatedStyle(self, event, style: items.CurveStyle):
"""Called when the current displayed style of the ROI was changed.
:param event: The event responsible of the change of the style
@@ -571,7 +612,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
"""
pass
- def getCurrentStyle(self):
+ def getCurrentStyle(self) -> items.CurveStyle:
"""Returns the current curve style.
Curve style depends on curve highlighting
@@ -589,7 +630,7 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
baseSymbol = self.getSymbol()
baseSymbolsize = self.getSymbolSize()
else:
- baseSymbol = 'o'
+ baseSymbol = "o"
baseSymbolsize = 1
if self.isHighlighted():
@@ -605,13 +646,16 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
linestyle=baseLinestyle if linestyle is None else linestyle,
linewidth=baseLinewidth if linewidth is None else linewidth,
symbol=baseSymbol if symbol is None else symbol,
- symbolsize=baseSymbolsize if symbolsize is None else symbolsize)
+ symbolsize=baseSymbolsize if symbolsize is None else symbolsize,
+ )
else:
- return items.CurveStyle(color=baseColor,
- linestyle=baseLinestyle,
- linewidth=baseLinewidth,
- symbol=baseSymbol,
- symbolsize=baseSymbolsize)
+ return items.CurveStyle(
+ color=baseColor,
+ linestyle=baseLinestyle,
+ linewidth=baseLinewidth,
+ symbol=baseSymbol,
+ symbolsize=baseSymbolsize,
+ )
def _editingStarted(self):
assert self._editable is True
@@ -620,6 +664,10 @@ class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
def _editingFinished(self):
self.sigEditingFinished.emit()
+ def populateContextMenu(self, menu: qt.QMenu):
+ """Populate a menu used as a context menu"""
+ pass
+
class HandleBasedROI(RegionOfInterest):
"""Manage a ROI based on a set of handles"""
@@ -731,9 +779,7 @@ class HandleBasedROI(RegionOfInterest):
See :class:`~silx.gui.plot.items.Item._updated`
"""
- if event == items.ItemChangedType.NAME:
- self._updateText(self.getName())
- elif event == items.ItemChangedType.VISIBLE:
+ if event == items.ItemChangedType.VISIBLE:
for item, role in self._handles:
visible = self.isVisible()
editionVisible = visible and self.isEditable()
@@ -755,9 +801,9 @@ class HandleBasedROI(RegionOfInterest):
color = rgba(self.getColor())
handleColor = self._computeHandleColor(color)
for item, role in self._handles:
- if role == 'user':
+ if role == "user":
pass
- elif role == 'label':
+ elif role == "label":
item.setColor(color)
else:
item.setColor(handleColor)
@@ -826,10 +872,3 @@ class HandleBasedROI(RegionOfInterest):
:rtype: Union[numpy.array,Tuple,List]
"""
return color[:3] + (0.5,)
-
- def _updateText(self, text):
- """Update the text displayed by this ROI
-
- :param str text: A text
- """
- pass
diff --git a/src/silx/gui/plot/items/axis.py b/src/silx/gui/plot/items/axis.py
index c73323e..1ae1ef1 100644
--- a/src/silx/gui/plot/items/axis.py
+++ b/src/silx/gui/plot/items/axis.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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,28 +24,28 @@
"""This module provides the class for axes of the :class:`PlotWidget`.
"""
+from __future__ import annotations
+
__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "22/11/2018"
import datetime as dt
import enum
-import logging
+from typing import Optional
import dateutil.tz
-import numpy
+from ....utils.proxy import docstring
from ... import qt
from .. import _utils
-_logger = logging.getLogger(__name__)
-
-
class TickMode(enum.Enum):
"""Determines if ticks are regular number or datetimes."""
- DEFAULT = 0 # Ticks are regular numbers
- TIME_SERIES = 1 # Ticks are datetime objects
+
+ DEFAULT = 0 # Ticks are regular numbers
+ TIME_SERIES = 1 # Ticks are datetime objects
class Axis(qt.QObject):
@@ -54,6 +53,7 @@ class Axis(qt.QObject):
Note: This is an abstract class.
"""
+
# States are half-stored on the backend of the plot, and half-stored on this
# object.
# TODO It would be good to store all the states of an axis in this object.
@@ -92,10 +92,10 @@ class Axis(qt.QObject):
self._scale = self.LINEAR
self._isAutoScale = True
# Store default labels provided to setGraph[X|Y]Label
- self._defaultLabel = ''
+ self._defaultLabel = ""
# Store currently displayed labels
# Current label can differ from input one with active curve handling
- self._currentLabel = ''
+ self._currentLabel = ""
def _getPlot(self):
"""Returns the PlotWidget this Axis belongs to.
@@ -151,7 +151,12 @@ class Axis(qt.QObject):
:rtype: 2-tuple of float
"""
return _utils.checkAxisLimits(
- vmin, vmax, isLog=self._isLogarithmic(), name=self._defaultLabel)
+ vmin, vmax, isLog=self._isLogarithmic(), name=self._defaultLabel
+ )
+
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ """Returns the range of data items over this axis as (vmin, vmax)"""
+ raise NotImplementedError()
def isInverted(self):
"""Return True if the axis is inverted (top to bottom for the y-axis),
@@ -173,6 +178,10 @@ class Axis(qt.QObject):
return
raise NotImplementedError()
+ def isVisible(self) -> bool:
+ """Returns whether the axis is displayed or not"""
+ return True
+
def getLabel(self):
"""Return the current displayed label of this axis.
@@ -200,10 +209,10 @@ class Axis(qt.QObject):
:param str label: Currently displayed label
"""
- if label is None or label == '':
+ if label is None or label == "":
label = self._defaultLabel
if label is None:
- label = ''
+ label = ""
self._currentLabel = label
self._internalSetCurrentLabel(label)
@@ -219,7 +228,7 @@ class Axis(qt.QObject):
:param str scale: Name of the scale ("log", or "linear")
"""
- assert(scale in self._SCALES)
+ assert scale in self._SCALES
if self._scale == scale:
return
@@ -228,6 +237,8 @@ class Axis(qt.QObject):
self._scale = scale
+ vmin, vmax = self.getLimits()
+
# TODO hackish way of forcing update of curves and images
plot = self._getPlot()
for item in plot.getItems():
@@ -236,13 +247,20 @@ class Axis(qt.QObject):
if scale == self.LOGARITHMIC:
self._internalSetLogarithmic(True)
+ if vmin <= 0:
+ dataRange = self._getDataRange()
+ if dataRange is None:
+ self.setLimits(1.0, 100.0)
+ else:
+ if vmax > 0 and dataRange[0] < vmax:
+ self.setLimits(dataRange[0], vmax)
+ else:
+ self.setLimits(*dataRange)
elif scale == self.LINEAR:
self._internalSetLogarithmic(False)
else:
raise ValueError("Scale %s unsupported" % scale)
- plot._forceResetZoom()
-
self.sigScaleChanged.emit(self._scale)
if emitLog:
self._sigLogarithmicChanged.emit(self._scale == self.LOGARITHMIC)
@@ -329,7 +347,7 @@ class Axis(qt.QObject):
plot = self._getPlot()
xMin, xMax = plot.getXAxis().getLimits()
yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
+ y2Min, y2Max = plot.getYAxis("right").getLimits()
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
return updated
@@ -352,7 +370,7 @@ class Axis(qt.QObject):
plot = self._getPlot()
xMin, xMax = plot.getXAxis().getLimits()
yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
+ y2Min, y2Max = plot.getYAxis("right").getLimits()
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
return updated
@@ -369,7 +387,7 @@ class XAxis(Axis):
def setTimeZone(self, tz):
if isinstance(tz, str) and tz.upper() == "UTC":
tz = dateutil.tz.tzutc()
- elif not(tz is None or isinstance(tz, dt.tzinfo)):
+ elif not (tz is None or isinstance(tz, dt.tzinfo)):
raise TypeError("tz must be a dt.tzinfo object, None or 'UTC'.")
self._getBackend().setXAxisTimeZone(tz)
@@ -411,6 +429,11 @@ class XAxis(Axis):
updated = constrains.update(minXRange=minRange, maxXRange=maxRange)
return updated
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.x
+
class YAxis(Axis):
"""Axis class defining primitives for the Y axis"""
@@ -419,13 +442,13 @@ class YAxis(Axis):
# specialised implementations (prefixel by '_internal')
def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='left')
+ self._getBackend().setGraphYLabel(label, axis="left")
def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='left')
+ return self._getBackend().getGraphYLimits(axis="left")
def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='left')
+ self._getBackend().setGraphYLimits(ymin, ymax, axis="left")
def _internalSetLogarithmic(self, flag):
self._getBackend().setYAxisLogarithmic(flag)
@@ -463,6 +486,11 @@ class YAxis(Axis):
updated = constrains.update(minYRange=minRange, maxYRange=maxRange)
return updated
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.y
+
class YRightAxis(Axis):
"""Proxy axis for the secondary Y axes. It manages it own label and limit
@@ -480,35 +508,19 @@ class YRightAxis(Axis):
"""
Axis.__init__(self, plot)
self.__mainAxis = mainAxis
-
- @property
- def sigInvertedChanged(self):
- """Signal emitted when axis orientation has changed"""
- return self.__mainAxis.sigInvertedChanged
-
- @property
- def sigScaleChanged(self):
- """Signal emitted when axis scale has changed"""
- return self.__mainAxis.sigScaleChanged
-
- @property
- def _sigLogarithmicChanged(self):
- """Signal emitted when axis scale has changed to or from logarithmic"""
- return self.__mainAxis._sigLogarithmicChanged
-
- @property
- def sigAutoScaleChanged(self):
- """Signal emitted when axis autoscale has changed"""
- return self.__mainAxis.sigAutoScaleChanged
+ self.__mainAxis.sigInvertedChanged.connect(self.sigInvertedChanged.emit)
+ self.__mainAxis.sigScaleChanged.connect(self.sigScaleChanged.emit)
+ self.__mainAxis._sigLogarithmicChanged.connect(self._sigLogarithmicChanged.emit)
+ self.__mainAxis.sigAutoScaleChanged.connect(self.sigAutoScaleChanged.emit)
def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='right')
+ self._getBackend().setGraphYLabel(label, axis="right")
def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='right')
+ return self._getBackend().getGraphYLimits(axis="right")
def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='right')
+ self._getBackend().setGraphYLimits(ymin, ymax, axis="right")
def setInverted(self, flag=True):
"""Set the Y axis orientation.
@@ -522,6 +534,10 @@ class YRightAxis(Axis):
"""Return True if Y axis goes from top to bottom, False otherwise."""
return self.__mainAxis.isInverted()
+ def isVisible(self) -> bool:
+ """Returns whether the axis is displayed or not"""
+ return self._getBackend().isYRightAxisVisible()
+
def getScale(self):
"""Return the name of the scale used by this axis.
@@ -558,3 +574,8 @@ class YRightAxis(Axis):
False to disable it.
"""
return self.__mainAxis.setAutoScale(flag)
+
+ @docstring(Axis)
+ def _getDataRange(self) -> Optional[tuple[float, float]]:
+ ranges = self._getPlot().getDataRange()
+ return ranges.y2
diff --git a/src/silx/gui/plot/items/complex.py b/src/silx/gui/plot/items/complex.py
index abb64ad..d10767f 100644
--- a/src/silx/gui/plot/items/complex.py
+++ b/src/silx/gui/plot/items/complex.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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,8 +24,6 @@
"""This module provides the :class:`ImageComplexData` of the :class:`Plot`.
"""
-from __future__ import absolute_import
-
__authors__ = ["Vincent Favre-Nicolin", "T. Vincent"]
__license__ = "MIT"
__date__ = "14/06/2018"
@@ -37,7 +34,6 @@ import logging
import numpy
from ....utils.proxy import docstring
-from ....utils.deprecation import deprecated
from ...colors import Colormap
from .core import ColormapMixIn, ComplexMixIn, ItemChangedType
from .image import ImageBase
@@ -48,6 +44,7 @@ _logger = logging.getLogger(__name__)
# Complex colormap functions
+
def _phase2rgb(colormap, data):
"""Creates RGBA image with colour-coded phase.
@@ -63,7 +60,7 @@ def _phase2rgb(colormap, data):
return colormap.applyToData(phase)
-def _complex2rgbalog(phaseColormap, data, amin=0., dlogs=2, smax=None):
+def _complex2rgbalog(phaseColormap, data, amin=0.0, dlogs=2, smax=None):
"""Returns RGBA colors: colour-coded phases and log10(amplitude) in alpha.
:param Colormap phaseColormap: Colormap to use for the phase
@@ -120,7 +117,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
ComplexMixIn.ComplexMode.IMAGINARY,
ComplexMixIn.ComplexMode.AMPLITUDE_PHASE,
ComplexMixIn.ComplexMode.LOG10_AMPLITUDE_PHASE,
- ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
+ ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE,
+ )
"""Overrides supported ComplexMode"""
def __init__(self):
@@ -133,10 +131,7 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
# Use default from ColormapMixIn
colormap = super(ImageComplexData, self).getColormap()
- phaseColormap = Colormap(
- name='hsv',
- vmin=-numpy.pi,
- vmax=numpy.pi)
+ phaseColormap = Colormap(name="hsv", vmin=-numpy.pi, vmax=numpy.pi)
self._colormaps = { # Default colormaps for all modes
self.ComplexMode.ABSOLUTE: colormap,
@@ -157,8 +152,10 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
return None
mode = self.getComplexMode()
- if mode in (self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
+ if mode in (
+ self.ComplexMode.AMPLITUDE_PHASE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ ):
# For those modes, compute RGBA image here
colormap = None
data = self.getRgbaImageData(copy=False)
@@ -174,11 +171,13 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
if data.size == 0:
return None # No data to display
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=colormap,
- alpha=self.getAlpha())
+ return backend.addImage(
+ data,
+ origin=self.getOrigin(),
+ scale=self.getScale(),
+ colormap=colormap,
+ alpha=self.getAlpha(),
+ )
@docstring(ComplexMixIn)
def setComplexMode(self, mode):
@@ -250,7 +249,7 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
return self._colormaps[mode]
def setData(self, data, copy=True):
- """"Set the image complex data
+ """Set the image complex data
:param numpy.ndarray data: 2D array of complex with 2 dimensions (h, w)
:param bool copy: True (Default) to get a copy,
@@ -260,7 +259,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
assert data.ndim == 2
if not numpy.issubdtype(data.dtype, numpy.complexfloating):
_logger.warning(
- 'Image is not complex, converting it to complex to plot it.')
+ "Image is not complex, converting it to complex to plot it."
+ )
data = numpy.array(data, dtype=numpy.complex64)
# Compute current mode data and set colormap data
@@ -277,8 +277,9 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
if event in (ItemChangedType.DATA, ItemChangedType.MASK):
# Color-mapped data is NOT the `getValueData` for some modes
if self.getComplexMode() in (
- self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
+ self.ComplexMode.AMPLITUDE_PHASE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ ):
data = self.getData(copy=False, mode=self.ComplexMode.PHASE)
mask = self.getMaskData(copy=False)
if mask is not None:
@@ -311,16 +312,18 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
return numpy.real(data)
elif mode is self.ComplexMode.IMAGINARY:
return numpy.imag(data)
- elif mode in (self.ComplexMode.ABSOLUTE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE,
- self.ComplexMode.AMPLITUDE_PHASE):
+ elif mode in (
+ self.ComplexMode.ABSOLUTE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE,
+ self.ComplexMode.AMPLITUDE_PHASE,
+ ):
return numpy.absolute(data)
elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
return numpy.absolute(data) ** 2
else:
_logger.error(
- 'Unsupported conversion mode: %s, fallback to absolute',
- str(mode))
+ "Unsupported conversion mode: %s, fallback to absolute", str(mode)
+ )
return numpy.absolute(data)
def getData(self, copy=True, mode=None):
@@ -343,7 +346,8 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
if mode not in self._dataByModesCache:
self._dataByModesCache[mode] = self.__convertComplexData(
- self.getComplexData(copy=False), mode)
+ self.getComplexData(copy=False), mode
+ )
return numpy.array(self._dataByModesCache[mode], copy=copy)
@@ -376,11 +380,3 @@ class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
# Backward compatibility
Mode = ComplexMixIn.ComplexMode
-
- @deprecated(replacement='setComplexMode', since_version='0.11.0')
- def setVisualizationMode(self, mode):
- return self.setComplexMode(mode)
-
- @deprecated(replacement='getComplexMode', since_version='0.11.0')
- def getVisualizationMode(self):
- return self.getComplexMode()
diff --git a/src/silx/gui/plot/items/core.py b/src/silx/gui/plot/items/core.py
index fa3b8cf..7d754a7 100644
--- a/src/silx/gui/plot/items/core.py
+++ b/src/silx/gui/plot/items/core.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,32 +23,28 @@
# ###########################################################################*/
"""This module provides the base class for items of the :class:`Plot`.
"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "08/12/2020"
-import collections
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
from copy import deepcopy
import logging
import enum
-from typing import Optional, Tuple
-import warnings
+from typing import Optional, Tuple, Union
import weakref
import numpy
-from ....utils.deprecation import deprecated
from ....utils.proxy import docstring
from ....utils.enum import Enum as _Enum
from ....math.combo import min_max
from ... import qt
from ... import colors
-from ...colors import Colormap
+from ...colors import Colormap, _Colormappable
from ._pick import PickingResult
from silx import config
@@ -60,98 +55,109 @@ _logger = logging.getLogger(__name__)
@enum.unique
class ItemChangedType(enum.Enum):
"""Type of modification provided by :attr:`Item.sigItemChanged` signal."""
+
# Private setters and setInfo are not emitting sigItemChanged signal.
# Signals to consider:
# COLORMAP_SET emitted when setColormap is called but not forward colormap object signal
# CURRENT_COLOR_CHANGED emitted current color changed because highlight changed,
# highlighted color changed or color changed depending on hightlight state.
- VISIBLE = 'visibleChanged'
+ VISIBLE = "visibleChanged"
"""Item's visibility changed flag."""
- ZVALUE = 'zValueChanged'
+ ZVALUE = "zValueChanged"
"""Item's Z value changed flag."""
- COLORMAP = 'colormapChanged' # Emitted when set + forward events from the colormap object
+ COLORMAP = (
+ "colormapChanged" # Emitted when set + forward events from the colormap object
+ )
"""Item's colormap changed flag.
This is emitted both when setting a new colormap and
when the current colormap object is updated.
"""
- SYMBOL = 'symbolChanged'
+ SYMBOL = "symbolChanged"
"""Item's symbol changed flag."""
- SYMBOL_SIZE = 'symbolSizeChanged'
+ SYMBOL_SIZE = "symbolSizeChanged"
"""Item's symbol size changed flag."""
- LINE_WIDTH = 'lineWidthChanged'
+ LINE_WIDTH = "lineWidthChanged"
"""Item's line width changed flag."""
- LINE_STYLE = 'lineStyleChanged'
+ LINE_STYLE = "lineStyleChanged"
"""Item's line style changed flag."""
- COLOR = 'colorChanged'
+ COLOR = "colorChanged"
"""Item's color changed flag."""
- LINE_BG_COLOR = 'lineBgColorChanged'
- """Item's line background color changed flag."""
+ LINE_BG_COLOR = "lineBgColorChanged" # Deprecated, use LINE_GAP_COLOR
+
+ LINE_GAP_COLOR = "lineGapColorChanged"
+ """Item's dashed line gap color changed flag."""
- YAXIS = 'yAxisChanged'
+ YAXIS = "yAxisChanged"
"""Item's Y axis binding changed flag."""
- FILL = 'fillChanged'
+ FILL = "fillChanged"
"""Item's fill changed flag."""
- ALPHA = 'alphaChanged'
+ ALPHA = "alphaChanged"
"""Item's transparency alpha changed flag."""
- DATA = 'dataChanged'
+ DATA = "dataChanged"
"""Item's data changed flag"""
- MASK = 'maskChanged'
+ MASK = "maskChanged"
"""Item's mask changed flag"""
- HIGHLIGHTED = 'highlightedChanged'
+ HIGHLIGHTED = "highlightedChanged"
"""Item's highlight state changed flag."""
- HIGHLIGHTED_COLOR = 'highlightedColorChanged'
+ HIGHLIGHTED_COLOR = "highlightedColorChanged"
"""Deprecated, use HIGHLIGHTED_STYLE instead."""
- HIGHLIGHTED_STYLE = 'highlightedStyleChanged'
+ HIGHLIGHTED_STYLE = "highlightedStyleChanged"
"""Item's highlighted style changed flag."""
- SCALE = 'scaleChanged'
+ SCALE = "scaleChanged"
"""Item's scale changed flag."""
- TEXT = 'textChanged'
+ TEXT = "textChanged"
"""Item's text changed flag."""
- POSITION = 'positionChanged'
+ POSITION = "positionChanged"
"""Item's position changed flag.
This is emitted when a marker position changed and
when an image origin changed.
"""
- OVERLAY = 'overlayChanged'
+ OVERLAY = "overlayChanged"
"""Item's overlay state changed flag."""
- VISUALIZATION_MODE = 'visualizationModeChanged'
+ VISUALIZATION_MODE = "visualizationModeChanged"
"""Item's visualization mode changed flag."""
- COMPLEX_MODE = 'complexModeChanged'
+ COMPLEX_MODE = "complexModeChanged"
"""Item's complex data visualization mode changed flag."""
- NAME = 'nameChanged'
+ NAME = "nameChanged"
"""Item's name changed flag."""
- EDITABLE = 'editableChanged'
+ EDITABLE = "editableChanged"
"""Item's editable state changed flags."""
- SELECTABLE = 'selectableChanged'
+ SELECTABLE = "selectableChanged"
"""Item's selectable state changed flags."""
+ FONT = "fontChanged"
+ """Item's text font changed flag."""
+
+ BACKGROUND_COLOR = "backgroundColorChanged"
+ """Item's text background color changed flag."""
+
class Item(qt.QObject):
"""Description of an item of the plot"""
@@ -186,7 +192,7 @@ class Item(qt.QObject):
self._info = None
self._xlabel = None
self._ylabel = None
- self.__name = ''
+ self.__name = ""
self.__visibleBoundsTracking = False
self.__previousVisibleBounds = None
@@ -208,7 +214,7 @@ class Item(qt.QObject):
:param Union[~silx.gui.plot.PlotWidget,None] plot: The Plot instance.
"""
if plot is not None and self._plotRef is not None:
- raise RuntimeError('Trying to add a node at two places.')
+ raise RuntimeError("Trying to add a node at two places.")
self.__disconnectFromPlotWidget()
self._plotRef = None if plot is None else weakref.ref(plot)
self.__connectToPlotWidget()
@@ -242,8 +248,9 @@ class Item(qt.QObject):
if visible != self._visible:
self._visible = visible
# When visibility has changed, always mark as dirty
- self._updated(ItemChangedType.VISIBLE,
- checkVisibility=False)
+ self._updated(ItemChangedType.VISIBLE, checkVisibility=False)
+ if visible:
+ self._visibleBoundsChanged()
def isOverlay(self):
"""Return true if item is drawn as an overlay.
@@ -268,8 +275,7 @@ class Item(qt.QObject):
name = str(name)
if self.__name != name:
if self.getPlot() is not None:
- raise RuntimeError(
- "Cannot change name while item is in a PlotWidget")
+ raise RuntimeError("Cannot change name while item is in a PlotWidget")
self.__name = name
self._updated(ItemChangedType.NAME)
@@ -277,11 +283,6 @@ class Item(qt.QObject):
def getLegend(self): # Replaced by getName for API consistency
return self.getName()
- @deprecated(replacement='setName', since_version='0.13')
- def _setLegend(self, legend):
- legend = str(legend) if legend is not None else ''
- self.setName(legend)
-
def isSelectable(self):
"""Returns true if item is selectable (bool)"""
return self._selectable
@@ -332,7 +333,8 @@ class Item(qt.QObject):
xmin, xmax = numpy.clip(bounds[:2], *plot.getXAxis().getLimits())
ymin, ymax = numpy.clip(
- bounds[2:], *plot.getYAxis(self.__getYAxis()).getLimits())
+ bounds[2:], *plot.getYAxis(self.__getYAxis()).getLimits()
+ )
if xmin == xmax or ymin == ymax: # Outside the plot area
return None
@@ -360,7 +362,7 @@ class Item(qt.QObject):
def __getYAxis(self) -> str:
"""Returns current Y axis ('left' or 'right')"""
- return self.getYAxis() if isinstance(self, YAxisMixIn) else 'left'
+ return self.getYAxis() if isinstance(self, YAxisMixIn) else "left"
def __connectToPlotWidget(self) -> None:
"""Connect to PlotWidget signals and install event filter"""
@@ -486,13 +488,14 @@ class Item(qt.QObject):
class DataItem(Item):
"""Item with a data extent in the plot"""
- def _boundsChanged(self, checkVisibility: bool=True) -> None:
+ def _boundsChanged(self, checkVisibility: bool = True) -> None:
"""Call this method in subclass when data bounds has changed.
:param bool checkVisibility:
"""
if not checkVisibility or self.isVisible():
- self._visibleBoundsChanged()
+ if self.isVisible():
+ self._visibleBoundsChanged()
# TODO hackish data range implementation
plot = self.getPlot()
@@ -505,6 +508,7 @@ class DataItem(Item):
self._boundsChanged(checkVisibility=False)
super().setVisible(visible)
+
# Mix-in classes ##############################################################
@@ -521,8 +525,7 @@ class ItemMixInBase(object):
:param bool checkVisibility: True to only mark as dirty if visible,
False to always mark as dirty.
"""
- raise RuntimeError(
- "Issue with Mix-In class inheritance order")
+ raise RuntimeError("Issue with Mix-In class inheritance order")
class LabelsMixIn(ItemMixInBase):
@@ -596,7 +599,7 @@ class DraggableMixIn(ItemMixInBase):
raise NotImplementedError("Must be implemented in subclass")
-class ColormapMixIn(ItemMixInBase):
+class ColormapMixIn(_Colormappable, ItemMixInBase):
"""Mix-in class for items with colormap"""
def __init__(self):
@@ -630,8 +633,9 @@ class ColormapMixIn(ItemMixInBase):
"""Handle updates of the colormap"""
self._updated(ItemChangedType.COLORMAP)
- def _setColormappedData(self, data, copy=True,
- min_=None, minPositive=None, max_=None):
+ def _setColormappedData(
+ self, data, copy=True, min_=None, minPositive=None, max_=None
+ ):
"""Set the data used to compute the colormapped display.
It also resets the cache of data ranges.
@@ -652,7 +656,10 @@ class ColormapMixIn(ItemMixInBase):
if min_ is not None and numpy.isfinite(min_):
self.__cacheColormapRange[Colormap.LINEAR, Colormap.MINMAX] = min_, max_
if minPositive is not None and numpy.isfinite(minPositive):
- self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = minPositive, max_
+ self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = (
+ minPositive,
+ max_,
+ )
colormap = self.getColormap()
if None in (colormap.getVMin(), colormap.getVMax()):
@@ -704,26 +711,29 @@ class SymbolMixIn(ItemMixInBase):
_DEFAULT_SYMBOL_SIZE = config.DEFAULT_PLOT_SYMBOL_SIZE
"""Default marker size of the item"""
- _SUPPORTED_SYMBOLS = collections.OrderedDict((
- ('o', 'Circle'),
- ('d', 'Diamond'),
- ('s', 'Square'),
- ('+', 'Plus'),
- ('x', 'Cross'),
- ('.', 'Point'),
- (',', 'Pixel'),
- ('|', 'Vertical line'),
- ('_', 'Horizontal line'),
- ('tickleft', 'Tick left'),
- ('tickright', 'Tick right'),
- ('tickup', 'Tick up'),
- ('tickdown', 'Tick down'),
- ('caretleft', 'Caret left'),
- ('caretright', 'Caret right'),
- ('caretup', 'Caret up'),
- ('caretdown', 'Caret down'),
- (u'\u2665', 'Heart'),
- ('', 'None')))
+ _SUPPORTED_SYMBOLS = dict(
+ (
+ ("o", "Circle"),
+ ("d", "Diamond"),
+ ("s", "Square"),
+ ("+", "Plus"),
+ ("x", "Cross"),
+ (".", "Point"),
+ (",", "Pixel"),
+ ("|", "Vertical line"),
+ ("_", "Horizontal line"),
+ ("tickleft", "Tick left"),
+ ("tickright", "Tick right"),
+ ("tickup", "Tick up"),
+ ("tickdown", "Tick down"),
+ ("caretleft", "Caret left"),
+ ("caretright", "Caret right"),
+ ("caretup", "Caret up"),
+ ("caretdown", "Caret down"),
+ ("\u2665", "Heart"),
+ ("", "None"),
+ )
+ )
"""Dict of supported symbols"""
def __init__(self):
@@ -798,7 +808,7 @@ class SymbolMixIn(ItemMixInBase):
symbol = symbolCode
break
else:
- raise ValueError('Unsupported symbol %s' % str(symbol))
+ raise ValueError("Unsupported symbol %s" % str(symbol))
if symbol != self._symbol:
self._symbol = symbol
@@ -825,50 +835,74 @@ class SymbolMixIn(ItemMixInBase):
self._updated(ItemChangedType.SYMBOL_SIZE)
+LineStyleType = Union[
+ str,
+ Tuple[Union[float, int], None],
+ Tuple[Union[float, int], Tuple[Union[float, int], Union[float, int]]],
+ Tuple[Union[float, int], Tuple[Union[float, int], Union[float, int], Union[float, int], Union[float, int]]],
+]
+"""Type for :class:`LineMixIn`'s line style"""
+
+
class LineMixIn(ItemMixInBase):
"""Mix-in class for item with line"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH: float = 1.0
"""Default line width"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE: LineStyleType = "-"
"""Default line style"""
- _SUPPORTED_LINESTYLE = '', ' ', '-', '--', '-.', ':', None
+ _SUPPORTED_LINESTYLE = "", " ", "-", "--", "-.", ":", None
"""Supported line styles"""
def __init__(self):
- self._linewidth = self._DEFAULT_LINEWIDTH
- self._linestyle = self._DEFAULT_LINESTYLE
+ self._linewidth: float = self._DEFAULT_LINEWIDTH
+ self._linestyle: LineStyleType = self._DEFAULT_LINESTYLE
@classmethod
- def getSupportedLineStyles(cls):
- """Returns list of supported line styles.
-
- :rtype: List[str,None]
- """
+ def getSupportedLineStyles(cls) -> tuple[str | None]:
+ """Returns list of supported constant line styles."""
return cls._SUPPORTED_LINESTYLE
- def getLineWidth(self):
- """Return the curve line width in pixels
-
- :rtype: float
- """
+ def getLineWidth(self) -> float:
+ """Return the curve line width in pixels"""
return self._linewidth
- def setLineWidth(self, width):
+ def setLineWidth(self, width: float):
"""Set the width in pixel of the curve line
See :meth:`getLineWidth`.
-
- :param float width: Width in pixels
"""
width = float(width)
if width != self._linewidth:
self._linewidth = width
self._updated(ItemChangedType.LINE_WIDTH)
- def getLineStyle(self):
+ @classmethod
+ def isValidLineStyle(cls, style: LineStyleType | None) -> bool:
+ """Returns True for valid styles"""
+ if style is None or style in cls.getSupportedLineStyles():
+ return True
+ if not isinstance(style, tuple):
+ return False
+ if (
+ len(style) == 2
+ and isinstance(style[0], (float, int))
+ and (
+ style[1] is None
+ or style[1] == ()
+ or (
+ isinstance(style[1], tuple)
+ and len(style[1]) in (2, 4)
+ and all(map(lambda item: isinstance(item, (float, int)), style[1]))
+ )
+ )
+ ):
+ return True
+ return False
+
+ def getLineStyle(self) -> LineStyleType:
"""Return the type of the line
Type of line::
@@ -878,20 +912,19 @@ class LineMixIn(ItemMixInBase):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
-
- :rtype: str
+ - (offset, (dash pattern))
"""
return self._linestyle
- def setLineStyle(self, style):
+ def setLineStyle(self, style: LineStyleType | None):
"""Set the style of the curve line.
See :meth:`getLineStyle`.
- :param str style: Line style
+ :param style: Line style
"""
- style = str(style)
- assert style in self.getSupportedLineStyles()
+ if not self.isValidLineStyle(style):
+ raise ValueError(f"No a valid line style: {style}")
if style is None:
style = self._DEFAULT_LINESTYLE
if style != self._linestyle:
@@ -902,7 +935,7 @@ class LineMixIn(ItemMixInBase):
class ColorMixIn(ItemMixInBase):
"""Mix-in class for item with color"""
- _DEFAULT_COLOR = (0., 0., 0., 1.)
+ _DEFAULT_COLOR = (0.0, 0.0, 0.0, 1.0)
"""Default color of the item"""
def __init__(self):
@@ -940,10 +973,43 @@ class ColorMixIn(ItemMixInBase):
self._updated(ItemChangedType.COLOR)
+class LineGapColorMixIn(ItemMixInBase):
+ """Mix-in class for dashed line gap color"""
+
+ _DEFAULT_LINE_GAP_COLOR = None
+ """Default dashed line gap color of the item"""
+
+ def __init__(self):
+ self.__lineGapColor = self._DEFAULT_LINE_GAP_COLOR
+
+ def getLineGapColor(self):
+ """Returns the RGBA color of dashed line gap of the item
+
+ :rtype: 4-tuple of float in [0, 1] or None
+ """
+ return self.__lineGapColor
+
+ def setLineGapColor(self, color):
+ """Set dashed line gap color
+
+ It supports:
+ - color names: e.g., 'green'
+ - color codes: '#RRGGBB' and '#RRGGBBAA'
+ - indexed color names: e.g., 'C0'
+ - RGB(A) sequence of uint8 in [0, 255] or float in [0, 1]
+ - QColor
+
+ :param color: line background color to be used
+ :type color: Union[str, List[int], List[float], QColor, None]
+ """
+ self.__lineGapColor = None if color is None else colors.rgba(color)
+ self._updated(ItemChangedType.LINE_GAP_COLOR)
+
+
class YAxisMixIn(ItemMixInBase):
"""Mix-in class for item with yaxis"""
- _DEFAULT_YAXIS = 'left'
+ _DEFAULT_YAXIS = "left"
"""Default Y axis the item belongs to"""
def __init__(self):
@@ -964,7 +1030,7 @@ class YAxisMixIn(ItemMixInBase):
:param str yaxis: 'left' or 'right'
"""
yaxis = str(yaxis)
- assert yaxis in ('left', 'right')
+ assert yaxis in ("left", "right")
if yaxis != self._yaxis:
self._yaxis = yaxis
# Handle data extent changed for DataItem
@@ -976,11 +1042,13 @@ class YAxisMixIn(ItemMixInBase):
# Switch Y axis signal connection
plot = self.getPlot()
if plot is not None:
- previousYAxis = 'left' if self.getXAxis() == 'right' else 'right'
+ previousYAxis = "left" if self.getXAxis() == "right" else "right"
plot.getYAxis(previousYAxis).sigLimitsChanged.disconnect(
- self._visibleBoundsChanged)
+ self._visibleBoundsChanged
+ )
plot.getYAxis(self.getYAxis()).sigLimitsChanged.connect(
- self._visibleBoundsChanged)
+ self._visibleBoundsChanged
+ )
self._visibleBoundsChanged()
self._updated(ItemChangedType.YAXIS)
@@ -1014,7 +1082,7 @@ class AlphaMixIn(ItemMixInBase):
"""Mix-in class for item with opacity"""
def __init__(self):
- self._alpha = 1.
+ self._alpha = 1.0
def getAlpha(self):
"""Returns the opacity of the item
@@ -1037,7 +1105,7 @@ class AlphaMixIn(ItemMixInBase):
:type alpha: float
"""
alpha = float(alpha)
- alpha = max(0., min(alpha, 1.)) # Clip alpha to [0., 1.] range
+ alpha = max(0.0, min(alpha, 1.0)) # Clip alpha to [0., 1.] range
if alpha != self._alpha:
self._alpha = alpha
self._updated(ItemChangedType.ALPHA)
@@ -1051,14 +1119,15 @@ class ComplexMixIn(ItemMixInBase):
class ComplexMode(_Enum):
"""Identify available display mode for complex"""
- NONE = 'none'
- ABSOLUTE = 'amplitude'
- PHASE = 'phase'
- REAL = 'real'
- IMAGINARY = 'imaginary'
- AMPLITUDE_PHASE = 'amplitude_phase'
- LOG10_AMPLITUDE_PHASE = 'log10_amplitude_phase'
- SQUARE_AMPLITUDE = 'square_amplitude'
+
+ NONE = "none"
+ ABSOLUTE = "amplitude"
+ PHASE = "phase"
+ REAL = "real"
+ IMAGINARY = "imaginary"
+ AMPLITUDE_PHASE = "amplitude_phase"
+ LOG10_AMPLITUDE_PHASE = "log10_amplitude_phase"
+ SQUARE_AMPLITUDE = "square_amplitude"
def __init__(self):
self.__complex_mode = self.ComplexMode.ABSOLUTE
@@ -1114,7 +1183,7 @@ class ComplexMixIn(ItemMixInBase):
elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
return numpy.absolute(data) ** 2
else:
- raise ValueError('Unsupported conversion mode: %s', str(mode))
+ raise ValueError("Unsupported conversion mode: %s", str(mode))
@classmethod
def supportedComplexModes(cls):
@@ -1140,22 +1209,22 @@ class ScatterVisualizationMixIn(ItemMixInBase):
class Visualization(_Enum):
"""Different modes of scatter plot visualizations"""
- POINTS = 'points'
+ POINTS = "points"
"""Display scatter plot as a point cloud"""
- LINES = 'lines'
+ LINES = "lines"
"""Display scatter plot as a wireframe.
This is based on Delaunay triangulation
"""
- SOLID = 'solid'
+ SOLID = "solid"
"""Display scatter plot as a set of filled triangles.
This is based on Delaunay triangulation
"""
- REGULAR_GRID = 'regular_grid'
+ REGULAR_GRID = "regular_grid"
"""Display scatter plot as an image.
It expects the points to be the intersection of a regular grid,
@@ -1164,7 +1233,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
(either all lines from left to right or all from right to left).
"""
- IRREGULAR_GRID = 'irregular_grid'
+ IRREGULAR_GRID = "irregular_grid"
"""Display scatter plot as contiguous quadrilaterals.
It expects the points to be the intersection of an irregular grid,
@@ -1173,7 +1242,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
(either all lines from left to right or all from right to left).
"""
- BINNED_STATISTIC = 'binned_statistic'
+ BINNED_STATISTIC = "binned_statistic"
"""Display scatter plot as 2D binned statistic (i.e., generalized histogram).
"""
@@ -1181,13 +1250,13 @@ class ScatterVisualizationMixIn(ItemMixInBase):
class VisualizationParameter(_Enum):
"""Different parameter names for scatter plot visualizations"""
- GRID_MAJOR_ORDER = 'grid_major_order'
+ GRID_MAJOR_ORDER = "grid_major_order"
"""The major order of points in the regular grid.
Either 'row' (row-major, fast X) or 'column' (column-major, fast Y).
"""
- GRID_BOUNDS = 'grid_bounds'
+ GRID_BOUNDS = "grid_bounds"
"""The expected range in data coordinates of the regular grid.
A 2-tuple of 2-tuple: (begin (x, y), end (x, y)).
@@ -1196,24 +1265,24 @@ class ScatterVisualizationMixIn(ItemMixInBase):
As for `GRID_SHAPE`, this can be wider than the current data.
"""
- GRID_SHAPE = 'grid_shape'
+ GRID_SHAPE = "grid_shape"
"""The expected size of the regular grid (height, width).
The given shape can be wider than the number of points,
in which case the grid is not fully filled.
"""
- BINNED_STATISTIC_SHAPE = 'binned_statistic_shape'
+ BINNED_STATISTIC_SHAPE = "binned_statistic_shape"
"""The number of bins in each dimension (height, width).
"""
- BINNED_STATISTIC_FUNCTION = 'binned_statistic_function'
+ BINNED_STATISTIC_FUNCTION = "binned_statistic_function"
"""The reduction function to apply to each bin (str).
Available reduction functions are: 'mean' (default), 'count', 'sum'.
"""
- DATA_BOUNDS_HINT = 'data_bounds_hint'
+ DATA_BOUNDS_HINT = "data_bounds_hint"
"""The expected bounds of the data in data coordinates.
A 2-tuple of 2-tuple: ((ymin, ymax), (xmin, xmax)).
@@ -1224,8 +1293,8 @@ class ScatterVisualizationMixIn(ItemMixInBase):
"""
_SUPPORTED_VISUALIZATION_PARAMETER_VALUES = {
- VisualizationParameter.GRID_MAJOR_ORDER: ('row', 'column'),
- VisualizationParameter.BINNED_STATISTIC_FUNCTION: ('mean', 'count', 'sum'),
+ VisualizationParameter.GRID_MAJOR_ORDER: ("row", "column"),
+ VisualizationParameter.BINNED_STATISTIC_FUNCTION: ("mean", "count", "sum"),
}
"""Supported visualization parameter values.
@@ -1234,9 +1303,12 @@ class ScatterVisualizationMixIn(ItemMixInBase):
def __init__(self):
self.__visualization = self.Visualization.POINTS
- self.__parameters = dict(# Init parameters to None
- (parameter, None) for parameter in self.VisualizationParameter)
- self.__parameters[self.VisualizationParameter.BINNED_STATISTIC_FUNCTION] = 'mean'
+ self.__parameters = dict( # Init parameters to None
+ (parameter, None) for parameter in self.VisualizationParameter
+ )
+ self.__parameters[
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ] = "mean"
@classmethod
def supportedVisualizations(cls):
@@ -1262,8 +1334,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
:returns: tuple of supported of values or None if not defined.
"""
parameter = cls.VisualizationParameter(parameter)
- return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get(
- parameter, None)
+ return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get(parameter, None)
def setVisualization(self, mode):
"""Set the scatter plot visualization mode to use.
@@ -1350,6 +1421,7 @@ class ScatterVisualizationMixIn(ItemMixInBase):
class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
"""Base class for :class:`Curve` and :class:`Scatter`"""
+
# note: _logFilterData must be overloaded if you overload
# getData to change its signature
@@ -1397,22 +1469,18 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
errorClipped[mask] = valueMinusError[mask] <= 0
if numpy.any(errorClipped): # Need filtering
-
# expand errorbars to 2xN
if error.size == 1: # Scalar
- error = numpy.full(
- (2, len(value)), error, dtype=numpy.float64)
+ error = numpy.full((2, len(value)), error, dtype=numpy.float64)
elif error.ndim == 1: # N array
- newError = numpy.empty((2, len(value)),
- dtype=numpy.float64)
- newError[0,:] = error
- newError[1,:] = error
+ newError = numpy.empty((2, len(value)), dtype=numpy.float64)
+ newError[0, :] = error
+ newError[1, :] = error
error = newError
elif error.size == 2 * len(value): # 2xN array
- error = numpy.array(
- error, copy=True, dtype=numpy.float64)
+ error = numpy.array(error, copy=True, dtype=numpy.float64)
else:
_logger.error("Unhandled error array")
@@ -1436,16 +1504,17 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
if xPositive:
x = self.getXData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
+ with numpy.errstate(invalid="ignore"): # Ignore NaN warnings
xclipped = x <= 0
if yPositive:
y = self.getYData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
+ with numpy.errstate(invalid="ignore"): # Ignore NaN warnings
yclipped = y <= 0
- self._clippedCache[(xPositive, yPositive)] = \
- numpy.logical_or(xclipped, yclipped)
+ self._clippedCache[(xPositive, yPositive)] = numpy.logical_or(
+ xclipped, yclipped
+ )
return self._clippedCache[(xPositive, yPositive)]
def _logFilterData(self, xPositive, yPositive):
@@ -1479,6 +1548,31 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
return x, y, xerror, yerror
+ @staticmethod
+ def __minMaxDataWithError(
+ data: numpy.ndarray,
+ error: Optional[Union[float, numpy.ndarray]],
+ positiveOnly: bool,
+ ) -> Tuple[float]:
+ if error is None:
+ min_, max_ = min_max(data, finite=True)
+ return min_, max_
+
+ # float, 1D or 2D array
+ dataMinusError = data - numpy.atleast_2d(error)[0]
+ dataMinusError = dataMinusError[numpy.isfinite(dataMinusError)]
+ if positiveOnly:
+ dataMinusError = dataMinusError[dataMinusError > 0]
+ min_ = numpy.nan if dataMinusError.size == 0 else numpy.min(dataMinusError)
+
+ dataPlusError = data + numpy.atleast_2d(error)[-1]
+ dataPlusError = dataPlusError[numpy.isfinite(dataPlusError)]
+ if positiveOnly:
+ dataPlusError = dataPlusError[dataPlusError > 0]
+ max_ = numpy.nan if dataPlusError.size == 0 else numpy.max(dataPlusError)
+
+ return min_, max_
+
def _getBounds(self):
if self.getXData(copy=False).size == 0: # Empty data
return None
@@ -1491,7 +1585,6 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
xPositive = False
yPositive = False
- # TODO bounds do not take error bars into account
if (xPositive, yPositive) not in self._boundsCache:
# use the getData class method because instance method can be
# overloaded to return additional arrays
@@ -1500,15 +1593,19 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
# hack to avoid duplicating caching mechanism in Scatter
# (happens when cached data is used, caching done using
# Scatter._logFilterData)
- x, y, _xerror, _yerror = data[0], data[1], data[3], data[4]
+ x, y, xerror, yerror = data[0], data[1], data[3], data[4]
else:
- x, y, _xerror, _yerror = data
+ x, y, xerror, yerror = data
- xmin, xmax = min_max(x, finite=True)
- ymin, ymax = min_max(y, finite=True)
- self._boundsCache[(xPositive, yPositive)] = tuple([
- (bound if bound is not None else numpy.nan)
- for bound in (xmin, xmax, ymin, ymax)])
+ xmin, xmax = self.__minMaxDataWithError(x, xerror, xPositive)
+ ymin, ymax = self.__minMaxDataWithError(y, yerror, yPositive)
+
+ self._boundsCache[(xPositive, yPositive)] = tuple(
+ [
+ (bound if bound is not None else numpy.nan)
+ for bound in (xmin, xmax, ymin, ymax)
+ ]
+ )
return self._boundsCache[(xPositive, yPositive)]
def _getCachedData(self):
@@ -1522,8 +1619,9 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
if xPositive or yPositive:
# At least one axis has log scale, filter data
if (xPositive, yPositive) not in self._filteredCache:
- self._filteredCache[(xPositive, yPositive)] = \
- self._logFilterData(xPositive, yPositive)
+ self._filteredCache[(xPositive, yPositive)] = self._logFilterData(
+ xPositive, yPositive
+ )
return self._filteredCache[(xPositive, yPositive)]
return None
@@ -1544,10 +1642,12 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
if cached_data is not None:
return cached_data
- return (self.getXData(copy),
- self.getYData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
+ return (
+ self.getXData(copy),
+ self.getYData(copy),
+ self.getXErrorData(copy),
+ self.getYErrorData(copy),
+ )
def getXData(self, copy=True):
"""Returns the x coordinates of the data points
@@ -1600,8 +1700,8 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
: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.
+ of same length as the data: row 0 for lower errors,
+ row 1 for upper errors.
:param yerror: Values with the uncertainties on the y values.
:type yerror: A float, or a numpy.ndarray of float32. See xerror.
:param bool copy: True make a copy of the data (default),
@@ -1614,12 +1714,10 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
# Convert complex data
if numpy.iscomplexobj(x):
- _logger.warning(
- 'Converting x data to absolute value to plot it.')
+ _logger.warning("Converting x data to absolute value to plot it.")
x = numpy.absolute(x)
if numpy.iscomplexobj(y):
- _logger.warning(
- 'Converting y data to absolute value to plot it.')
+ _logger.warning("Converting y data to absolute value to plot it.")
y = numpy.absolute(y)
if xerror is not None:
@@ -1627,7 +1725,8 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
xerror = numpy.array(xerror, copy=copy)
if numpy.iscomplexobj(xerror):
_logger.warning(
- 'Converting xerror data to absolute value to plot it.')
+ "Converting xerror data to absolute value to plot it."
+ )
xerror = numpy.absolute(xerror)
else:
xerror = float(xerror)
@@ -1636,7 +1735,8 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
yerror = numpy.array(yerror, copy=copy)
if numpy.iscomplexobj(yerror):
_logger.warning(
- 'Converting yerror data to absolute value to plot it.')
+ "Converting yerror data to absolute value to plot it."
+ )
yerror = numpy.absolute(yerror)
else:
yerror = float(yerror)
@@ -1665,7 +1765,7 @@ class BaselineMixIn(object):
:param baseline: baseline value(s)
:type: Union[None,float,numpy.ndarray]
"""
- if (isinstance(baseline, abc.Iterable)):
+ if isinstance(baseline, abc.Iterable):
baseline = numpy.array(baseline)
self._baseline = baseline
@@ -1687,7 +1787,6 @@ class _Style:
class HighlightedMixIn(ItemMixInBase):
-
def __init__(self):
self._highlightStyle = self._DEFAULT_HIGHLIGHT_STYLE
self._highlighted = False
diff --git a/src/silx/gui/plot/items/curve.py b/src/silx/gui/plot/items/curve.py
index 7cbe26e..e8d0d52 100644
--- a/src/silx/gui/plot/items/curve.py
+++ b/src/silx/gui/plot/items/curve.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,6 +23,7 @@
# ###########################################################################*/
"""This module provides the :class:`Curve` item of the :class:`Plot`.
"""
+from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
@@ -34,11 +34,22 @@ import logging
import numpy
-from ....utils.deprecation import deprecated
+from ....utils.deprecation import deprecated_warning
from ... import colors
-from .core import (PointsBase, LabelsMixIn, ColorMixIn, YAxisMixIn,
- FillMixIn, LineMixIn, SymbolMixIn, ItemChangedType,
- BaselineMixIn, HighlightedMixIn, _Style)
+from .core import (
+ PointsBase,
+ LabelsMixIn,
+ ColorMixIn,
+ YAxisMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ LineStyleType,
+ SymbolMixIn,
+ BaselineMixIn,
+ HighlightedMixIn,
+ _Style,
+)
_logger = logging.getLogger(__name__)
@@ -50,14 +61,22 @@ class CurveStyle(_Style):
Set a value to None to use the default
:param color: Color
- :param Union[str,None] linestyle: Style of the line
- :param Union[float,None] linewidth: Width of the line
- :param Union[str,None] symbol: Symbol for markers
- :param Union[float,None] symbolsize: Size of the markers
+ :param linestyle: Style of the line
+ :param linewidth: Width of the line
+ :param symbol: Symbol for markers
+ :param symbolsize: Size of the markers
+ :param gapcolor: Color of gaps of dashed line
"""
- def __init__(self, color=None, linestyle=None, linewidth=None,
- symbol=None, symbolsize=None):
+ def __init__(
+ self,
+ color: colors.RGBAColorType | None = None,
+ linestyle: LineStyleType | None = None,
+ linewidth: float | None = None,
+ symbol: str | None = None,
+ symbolsize: float | None = None,
+ gapcolor: colors.RGBAColorType | None = None,
+ ):
if color is None:
self._color = None
else:
@@ -69,8 +88,8 @@ class CurveStyle(_Style):
color = colors.rgba(color)
self._color = color
- if linestyle is not None:
- assert linestyle in LineMixIn.getSupportedLineStyles()
+ if not LineMixIn.isValidLineStyle(linestyle):
+ raise ValueError(f"Not a valid line style: {linestyle}")
self._linestyle = linestyle
self._linewidth = None if linewidth is None else float(linewidth)
@@ -81,6 +100,8 @@ class CurveStyle(_Style):
self._symbolsize = None if symbolsize is None else float(symbolsize)
+ self._gapcolor = None if gapcolor is None else colors.rgba(gapcolor)
+
def getColor(self, copy=True):
"""Returns the color or None if not set.
@@ -94,7 +115,14 @@ class CurveStyle(_Style):
else:
return self._color
- def getLineStyle(self):
+ def getLineGapColor(self):
+ """Returns the color of dashed line gaps or None if not set.
+
+ :rtype: Union[List[float],None]
+ """
+ return self._gapcolor
+
+ def getLineStyle(self) -> LineStyleType | None:
"""Return the type of the line or None if not set.
Type of line::
@@ -104,8 +132,7 @@ class CurveStyle(_Style):
- '--' dashed line
- '-.' dash-dot line
- ':' dotted line
-
- :rtype: Union[str,None]
+ - (offset, (dash pattern))
"""
return self._linestyle
@@ -142,17 +169,29 @@ class CurveStyle(_Style):
def __eq__(self, other):
if isinstance(other, CurveStyle):
- return (numpy.array_equal(self.getColor(), other.getColor()) and
- self.getLineStyle() == other.getLineStyle() and
- self.getLineWidth() == other.getLineWidth() and
- self.getSymbol() == other.getSymbol() and
- self.getSymbolSize() == other.getSymbolSize())
+ return (
+ numpy.array_equal(self.getColor(), other.getColor())
+ and self.getLineStyle() == other.getLineStyle()
+ and self.getLineWidth() == other.getLineWidth()
+ and self.getSymbol() == other.getSymbol()
+ and self.getSymbolSize() == other.getSymbolSize()
+ and self.getLineGapColor() == other.getLineGapColor()
+ )
else:
return False
-class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
- LineMixIn, BaselineMixIn, HighlightedMixIn):
+class Curve(
+ PointsBase,
+ ColorMixIn,
+ YAxisMixIn,
+ FillMixIn,
+ LabelsMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ BaselineMixIn,
+ HighlightedMixIn,
+):
"""Description of a curve"""
_DEFAULT_Z_LAYER = 1
@@ -161,13 +200,13 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
_DEFAULT_SELECTABLE = True
"""Default selectable state for curves"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the curve"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the curve"""
- _DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color='black')
+ _DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color="black")
"""Default highlight style of the item"""
_DEFAULT_BASELINE = None
@@ -179,6 +218,7 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
FillMixIn.__init__(self)
LabelsMixIn.__init__(self)
LineMixIn.__init__(self)
+ LineGapColorMixIn.__init__(self)
BaselineMixIn.__init__(self)
HighlightedMixIn.__init__(self)
@@ -187,29 +227,38 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
def _addBackendRenderer(self, backend):
"""Update backend renderer"""
# Filter-out values <= 0
- xFiltered, yFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
+ xFiltered, yFiltered, xerror, yerror = self.getData(copy=False, displayed=True)
if len(xFiltered) == 0 or not numpy.any(numpy.isfinite(xFiltered)):
return None # No data to display, do not add renderer to backend
style = self.getCurrentStyle()
- return backend.addCurve(xFiltered, yFiltered,
- color=style.getColor(),
- symbol=style.getSymbol(),
- linestyle=style.getLineStyle(),
- linewidth=style.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=xerror,
- yerror=yerror,
- fill=self.isFill(),
- alpha=self.getAlpha(),
- symbolsize=style.getSymbolSize(),
- baseline=self.getBaseline(copy=False))
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=style.getColor(),
+ gapcolor=style.getLineGapColor(),
+ symbol=style.getSymbol(),
+ linestyle=style.getLineStyle(),
+ linewidth=style.getLineWidth(),
+ yaxis=self.getYAxis(),
+ xerror=xerror,
+ yerror=yerror,
+ fill=self.isFill(),
+ alpha=self.getAlpha(),
+ symbolsize=style.getSymbolSize(),
+ baseline=self.getBaseline(copy=False),
+ )
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use Curve methods",
+ )
if isinstance(item, slice):
return [self[index] for index in range(*item.indices(5))]
elif item == 0:
@@ -223,44 +272,24 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
return {} if info is None else info
elif item == 4:
params = {
- 'info': self.getInfo(),
- 'color': self.getColor(),
- 'symbol': self.getSymbol(),
- 'linewidth': self.getLineWidth(),
- 'linestyle': self.getLineStyle(),
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
- 'yaxis': self.getYAxis(),
- 'xerror': self.getXErrorData(copy=False),
- 'yerror': self.getYErrorData(copy=False),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'fill': self.isFill(),
+ "info": self.getInfo(),
+ "color": self.getColor(),
+ "symbol": self.getSymbol(),
+ "linewidth": self.getLineWidth(),
+ "linestyle": self.getLineStyle(),
+ "xlabel": self.getXLabel(),
+ "ylabel": self.getYLabel(),
+ "yaxis": self.getYAxis(),
+ "xerror": self.getXErrorData(copy=False),
+ "yerror": self.getYErrorData(copy=False),
+ "z": self.getZValue(),
+ "selectable": self.isSelectable(),
+ "fill": self.isFill(),
}
return params
else:
raise IndexError("Index out of range: %s", str(item))
- @deprecated(replacement='Curve.getHighlightedStyle().getColor()',
- since_version='0.9.0')
- def getHighlightedColor(self):
- """Returns the RGBA highlight color of the item
-
- :rtype: 4-tuple of float in [0, 1]
- """
- return self.getHighlightedStyle().getColor()
-
- @deprecated(replacement='Curve.setHighlightedStyle()',
- since_version='0.9.0')
- def setHighlightedColor(self, color):
- """Set the color to use when highlighted
-
- :param color: color(s) to be used for highlight
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in colors.py
- """
- self.setHighlightedStyle(CurveStyle(color))
-
def getCurrentStyle(self):
"""Returns the current curve style.
@@ -275,32 +304,26 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
linewidth = style.getLineWidth()
symbol = style.getSymbol()
symbolsize = style.getSymbolSize()
+ gapcolor = style.getLineGapColor()
return CurveStyle(
color=self.getColor() if color is None else color,
linestyle=self.getLineStyle() if linestyle is None else linestyle,
linewidth=self.getLineWidth() if linewidth is None else linewidth,
symbol=self.getSymbol() if symbol is None else symbol,
- symbolsize=self.getSymbolSize() if symbolsize is None else symbolsize)
+ symbolsize=self.getSymbolSize() if symbolsize is None else symbolsize,
+ gapcolor=self.getLineGapColor() if gapcolor is None else gapcolor,
+ )
else:
- return CurveStyle(color=self.getColor(),
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- symbol=self.getSymbol(),
- symbolsize=self.getSymbolSize())
-
- @deprecated(replacement='Curve.getCurrentStyle()',
- since_version='0.9.0')
- def getCurrentColor(self):
- """Returns the current color of the curve.
-
- This color is either the color of the curve or the highlighted color,
- depending on the highlight state.
-
- :rtype: 4-tuple of float in [0, 1]
- """
- return self.getCurrentStyle().getColor()
+ return CurveStyle(
+ color=self.getColor(),
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ symbol=self.getSymbol(),
+ symbolsize=self.getSymbolSize(),
+ gapcolor=self.getLineGapColor(),
+ )
def setData(self, x, y, xerror=None, yerror=None, baseline=None, copy=True):
"""Set the data of the curve.
@@ -320,6 +343,5 @@ class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
"""
- PointsBase.setData(self, x=x, y=y, xerror=xerror, yerror=yerror,
- copy=copy)
+ PointsBase.setData(self, x=x, y=y, xerror=xerror, yerror=yerror, copy=copy)
self._setBaseline(baseline=baseline)
diff --git a/src/silx/gui/plot/items/histogram.py b/src/silx/gui/plot/items/histogram.py
index 16bbefa..1dc851b 100644
--- a/src/silx/gui/plot/items/histogram.py
+++ b/src/silx/gui/plot/items/histogram.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,15 +32,20 @@ import logging
import typing
import numpy
-from collections import OrderedDict, namedtuple
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
from ....utils.proxy import docstring
-from .core import (DataItem, AlphaMixIn, BaselineMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, ItemChangedType, Item)
+from .core import (
+ DataItem,
+ AlphaMixIn,
+ BaselineMixIn,
+ ColorMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+ ItemChangedType,
+)
from ._pick import PickingResult
_logger = logging.getLogger(__name__)
@@ -63,17 +67,17 @@ def _computeEdges(x, histogramType):
"""
# for now we consider that the spaces between xs are constant
edges = x.copy()
- if histogramType == 'left':
+ if histogramType == "left":
width = 1
if len(x) > 1:
width = x[1] - x[0]
edges = numpy.append(x[0] - width, edges)
- if histogramType == 'center':
- edges = _computeEdges(edges, 'right')
+ if histogramType == "center":
+ edges = _computeEdges(edges, "right")
widths = (edges[1:] - edges[0:-1]) / 2.0
widths = numpy.append(widths, widths[-1])
edges = edges - widths
- if histogramType == 'right':
+ if histogramType == "right":
width = 1
if len(x) > 1:
width = x[-1] - x[-2]
@@ -103,8 +107,16 @@ def _getHistogramCurve(histogram, edges):
# TODO: Yerror, test log scale
-class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, BaselineMixIn):
+class Histogram(
+ DataItem,
+ AlphaMixIn,
+ ColorMixIn,
+ FillMixIn,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+ BaselineMixIn,
+):
"""Description of an histogram"""
_DEFAULT_Z_LAYER = 1
@@ -113,10 +125,10 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
_DEFAULT_SELECTABLE = False
"""Default selectable state for histograms"""
- _DEFAULT_LINEWIDTH = 1.
+ _DEFAULT_LINEWIDTH = 1.0
"""Default line width of the histogram"""
- _DEFAULT_LINESTYLE = '-'
+ _DEFAULT_LINESTYLE = "-"
"""Default line style of the histogram"""
_DEFAULT_BASELINE = None
@@ -128,6 +140,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
ColorMixIn.__init__(self)
FillMixIn.__init__(self)
LineMixIn.__init__(self)
+ LineGapColorMixIn.__init__(self)
YAxisMixIn.__init__(self)
self._histogram = ()
@@ -157,26 +170,30 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
if xPositive or yPositive:
clipped = numpy.logical_or(
- (x <= 0) if xPositive else False,
- (y <= 0) if yPositive else False)
+ (x <= 0) if xPositive else False, (y <= 0) if yPositive else False
+ )
# Make a copy and replace negative points by NaN
x = numpy.array(x, dtype=numpy.float64)
y = numpy.array(y, dtype=numpy.float64)
x[clipped] = numpy.nan
y[clipped] = numpy.nan
- return backend.addCurve(x, y,
- color=self.getColor(),
- symbol='',
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=None,
- yerror=None,
- fill=self.isFill(),
- alpha=self.getAlpha(),
- baseline=baseline,
- symbolsize=1)
+ return backend.addCurve(
+ x,
+ y,
+ color=self.getColor(),
+ gapcolor=self.getLineGapColor(),
+ symbol="",
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ yaxis=self.getYAxis(),
+ xerror=None,
+ yerror=None,
+ fill=self.isFill(),
+ alpha=self.getAlpha(),
+ baseline=baseline,
+ symbolsize=1,
+ )
def _getBounds(self):
values, edges, baseline = self.getData(copy=False)
@@ -194,11 +211,10 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
if xPositive:
# Replace edges <= 0 by NaN and corresponding values by NaN
- clipped_edges = (edges <= 0)
+ clipped_edges = edges <= 0
edges = numpy.array(edges, copy=True, dtype=numpy.float64)
edges[clipped_edges] = numpy.nan
- clipped_values = numpy.logical_or(clipped_edges[:-1],
- clipped_edges[1:])
+ clipped_values = numpy.logical_or(clipped_edges[:-1], clipped_edges[1:])
else:
clipped_values = numpy.zeros_like(values, dtype=bool)
@@ -209,20 +225,26 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
values[clipped_values] = numpy.nan
if yPositive:
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- numpy.nanmin(values),
- numpy.nanmax(values))
+ return (
+ numpy.nanmin(edges),
+ numpy.nanmax(edges),
+ numpy.nanmin(values),
+ numpy.nanmax(values),
+ )
else: # No log scale on y axis, include 0 in bounds
if numpy.all(numpy.isnan(values)):
return None
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- min(0, numpy.nanmin(values)),
- max(0, numpy.nanmax(values)))
-
- def __pickFilledHistogram(self, x: float, y: float) -> typing.Optional[PickingResult]:
+ return (
+ numpy.nanmin(edges),
+ numpy.nanmax(edges),
+ min(0, numpy.nanmin(values)),
+ max(0, numpy.nanmax(values)),
+ )
+
+ def __pickFilledHistogram(
+ self, x: float, y: float
+ ) -> typing.Optional[PickingResult]:
"""Picking implementation for filled histogram
:param x: X position in pixels
@@ -242,7 +264,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
# Check x
edges = self.getBinEdgesData(copy=False)
- index = numpy.searchsorted(edges, (xData,), side='left')[0] - 1
+ index = numpy.searchsorted(edges, (xData,), side="left")[0] - 1
# Safe indexing in histogram values
index = numpy.clip(index, 0, len(edges) - 2)
@@ -252,8 +274,9 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
baseline = 0 # Default value
value = self.getValueData(copy=False)[index]
- if ((baseline <= value and baseline <= yData <= value) or
- (value < baseline and value <= yData <= baseline)):
+ if (baseline <= value and baseline <= yData <= value) or (
+ value < baseline and value <= yData <= baseline
+ ):
return PickingResult(self, numpy.array([index]))
else:
return None
@@ -297,12 +320,13 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
:returns: (N histogram value, N+1 bin edges)
:rtype: 2-tuple of numpy.nadarray
"""
- return (self.getValueData(copy),
- self.getBinEdgesData(copy),
- self.getBaseline(copy))
+ return (
+ self.getValueData(copy),
+ self.getBinEdgesData(copy),
+ self.getBaseline(copy),
+ )
- def setData(self, histogram, edges, align='center', baseline=None,
- copy=True):
+ def setData(self, histogram, edges, align="center", baseline=None, copy=True):
"""Set the histogram values and bin edges.
:param numpy.ndarray histogram: The values of the histogram.
@@ -325,7 +349,7 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
assert histogram.ndim == 1
assert edges.ndim == 1
assert edges.size in (histogram.size, histogram.size + 1)
- assert align in ('center', 'left', 'right')
+ assert align in ("center", "left", "right")
if histogram.size == 0: # No data
self._histogram = ()
@@ -339,12 +363,12 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
edgesDiff = edgesDiff[numpy.logical_not(numpy.isnan(edgesDiff))]
assert numpy.all(edgesDiff >= 0) or numpy.all(edgesDiff <= 0)
# manage baseline
- if (isinstance(baseline, abc.Iterable)):
+ if isinstance(baseline, abc.Iterable):
baseline = numpy.array(baseline)
if baseline.size == histogram.size:
new_baseline = numpy.empty(baseline.shape[0] * 2)
for i_value, value in enumerate(baseline):
- new_baseline[i_value*2:i_value*2+2] = value
+ new_baseline[i_value * 2 : i_value * 2 + 2] = value
baseline = new_baseline
self._histogram = histogram
self._edges = edges
@@ -377,11 +401,11 @@ class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
"""
# for now we consider that the spaces between xs are constant
edges = x.copy()
- if histogramType == 'left':
+ if histogramType == "left":
return edges[1:]
- if histogramType == 'center':
+ if histogramType == "center":
edges = (edges[1:] + edges[:-1]) / 2.0
- if histogramType == 'right':
+ if histogramType == "right":
width = 1
if len(x) > 1:
width = x[-1] + x[-2]
diff --git a/src/silx/gui/plot/items/image.py b/src/silx/gui/plot/items/image.py
index 5cc719b..18310d9 100644
--- a/src/silx/gui/plot/items/image.py
+++ b/src/silx/gui/plot/items/image.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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,17 +29,21 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "08/12/2020"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import logging
import numpy
from ....utils.proxy import docstring
-from .core import (DataItem, LabelsMixIn, DraggableMixIn, ColormapMixIn,
- AlphaMixIn, ItemChangedType)
+from ....utils.deprecation import deprecated_warning
+from .core import (
+ DataItem,
+ LabelsMixIn,
+ DraggableMixIn,
+ ColormapMixIn,
+ AlphaMixIn,
+ ItemChangedType,
+)
_logger = logging.getLogger(__name__)
@@ -63,23 +66,22 @@ def _convertImageToRgba32(image, copy=True):
assert image.shape[-1] in (3, 4)
# Convert type to uint8
- if image.dtype.name != 'uint8':
- if image.dtype.kind == 'f': # Float in [0, 1]
- image = (numpy.clip(image, 0., 1.) * 255).astype(numpy.uint8)
- elif image.dtype.kind == 'b': # boolean
+ if image.dtype.name != "uint8":
+ if image.dtype.kind == "f": # Float in [0, 1]
+ image = (numpy.clip(image, 0.0, 1.0) * 255).astype(numpy.uint8)
+ elif image.dtype.kind == "b": # boolean
image = image.astype(numpy.uint8) * 255
- elif image.dtype.kind in ('i', 'u'): # int, uint
+ elif image.dtype.kind in ("i", "u"): # int, uint
image = numpy.clip(image, 0, 255).astype(numpy.uint8)
else:
- raise ValueError('Unsupported image dtype: %s', image.dtype.name)
+ raise ValueError("Unsupported image dtype: %s", image.dtype.name)
copy = False # A copy as already been done, avoid next one
# Convert RGB to RGBA
if image.shape[-1] == 3:
- new_image = numpy.empty((image.shape[0], image.shape[1], 4),
- dtype=numpy.uint8)
- new_image[:,:,:3] = image
- new_image[:,:, 3] = 255
+ new_image = numpy.empty((image.shape[0], image.shape[1], 4), dtype=numpy.uint8)
+ new_image[:, :, :3] = image
+ new_image[:, :, 3] = 255
return new_image # This is a copy anyway
else:
return numpy.array(image, copy=copy)
@@ -101,11 +103,17 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
self._data = data
self._mask = mask
self.__valueDataCache = None # Store default data
- self._origin = (0., 0.)
- self._scale = (1., 1.)
+ self._origin = (0.0, 0.0)
+ self._scale = (1.0, 1.0)
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use ImageBase methods",
+ )
if isinstance(item, slice):
return [self[index] for index in range(*item.indices(5))]
elif item == 0:
@@ -119,15 +127,15 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
return None
elif item == 4:
params = {
- 'info': self.getInfo(),
- 'origin': self.getOrigin(),
- 'scale': self.getScale(),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'draggable': self.isDraggable(),
- 'colormap': None,
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
+ "info": self.getInfo(),
+ "origin": self.getOrigin(),
+ "scale": self.getScale(),
+ "z": self.getZValue(),
+ "selectable": self.isSelectable(),
+ "draggable": self.isDraggable(),
+ "colormap": None,
+ "xlabel": self.getXLabel(),
+ "ylabel": self.getYLabel(),
}
return params
else:
@@ -168,8 +176,7 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
@docstring(DraggableMixIn)
def drag(self, from_, to):
origin = self.getOrigin()
- self.setOrigin((origin[0] + to[0] - from_[0],
- origin[1] + to[1] - from_[1]))
+ self.setOrigin((origin[0] + to[0] - from_[0], origin[1] + to[1] - from_[1]))
def getData(self, copy=True):
"""Returns the image data
@@ -191,8 +198,10 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
self._boundsChanged()
self._updated(ItemChangedType.DATA)
- if (self.getMaskData(copy=False) is not None and
- previousShape != self._data.shape):
+ if (
+ self.getMaskData(copy=False) is not None
+ and previousShape != self._data.shape
+ ):
# Data shape changed, so mask shape changes.
# Send event, mask is lazily updated in getMaskData
self._updated(ItemChangedType.MASK)
@@ -212,7 +221,9 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
if self._mask.shape != shape:
# Clip/extend mask to match data
newMask = numpy.zeros(shape, dtype=self._mask.dtype)
- newMask[:self._mask.shape[0], :self._mask.shape[1]] = self._mask[:shape[0], :shape[1]]
+ newMask[: self._mask.shape[0], : self._mask.shape[1]] = self._mask[
+ : shape[0], : shape[1]
+ ]
self._mask = newMask
return numpy.array(self._mask, copy=copy)
@@ -229,7 +240,9 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
shape = self.getData(copy=False).shape[:2]
if mask.shape != shape:
- _logger.warning("Inconsistent shape between mask and data %s, %s", mask.shape, shape)
+ _logger.warning(
+ "Inconsistent shape between mask and data %s, %s", mask.shape, shape
+ )
# Clip/extent is done lazily in getMaskData
elif self._mask is None:
return # No update
@@ -279,7 +292,7 @@ class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
False to use internal representation (do not modify!)
:returns: numpy.ndarray of uint8 of shape (height, width, 4)
"""
- raise NotImplementedError('This MUST be implemented in sub-class')
+ raise NotImplementedError("This MUST be implemented in sub-class")
def getOrigin(self):
"""Returns the offset from origin at which to display the image.
@@ -337,9 +350,11 @@ class ImageDataBase(ImageBase, ColormapMixIn):
def _getColormapForRendering(self):
colormap = self.getColormap()
if colormap.isAutoscale():
+ # NOTE: Make sure getColormapRange comes from the original object
+ vrange = colormap.getColormapRange(self)
# Avoid backend to compute autoscale: use item cache
colormap = colormap.copy()
- colormap.setVRange(*colormap.getColormapRange(self))
+ colormap.setVRange(*vrange)
return colormap
def getRgbaImageData(self, copy=True):
@@ -351,7 +366,7 @@ class ImageDataBase(ImageBase, ColormapMixIn):
return self.getColormap().applyToData(self)
def setData(self, data, copy=True):
- """"Set the image data
+ """Set the image data
:param numpy.ndarray data: Data array with 2 dimensions (h, w)
:param bool copy: True (Default) to get a copy,
@@ -359,13 +374,11 @@ class ImageDataBase(ImageBase, ColormapMixIn):
"""
data = numpy.array(data, copy=copy)
assert data.ndim == 2
- if data.dtype.kind == 'b':
- _logger.warning(
- 'Converting boolean image to int8 to plot it.')
+ if data.dtype.kind == "b":
+ _logger.warning("Converting boolean image to int8 to plot it.")
data = numpy.array(data, copy=False, dtype=numpy.int8)
elif numpy.iscomplexobj(data):
- _logger.warning(
- 'Converting complex image to absolute value to plot it.')
+ _logger.warning("Converting complex image to absolute value to plot it.")
data = numpy.absolute(data)
super().setData(data)
@@ -392,8 +405,10 @@ class ImageData(ImageDataBase):
# Do not render with non linear scales
return None
- if (self.getAlternativeImageData(copy=False) is not None or
- self.getAlphaData(copy=False) is not None):
+ if (
+ self.getAlternativeImageData(copy=False) is not None
+ or self.getAlphaData(copy=False) is not None
+ ):
dataToUse = self.getRgbaImageData(copy=False)
else:
dataToUse = self.getData(copy=False)
@@ -401,20 +416,28 @@ class ImageData(ImageDataBase):
if dataToUse.size == 0:
return None # No data to display
- return backend.addImage(dataToUse,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=self._getColormapForRendering(),
- alpha=self.getAlpha())
+ return backend.addImage(
+ dataToUse,
+ origin=self.getOrigin(),
+ scale=self.getScale(),
+ colormap=self._getColormapForRendering(),
+ alpha=self.getAlpha(),
+ )
def __getitem__(self, item):
"""Compatibility with PyMca and silx <= 0.4.0"""
+ deprecated_warning(
+ "Attributes",
+ "__getitem__",
+ since_version="2.0.0",
+ replacement="Use ImageData methods",
+ )
if item == 3:
return self.getAlternativeImageData(copy=False)
params = ImageBase.__getitem__(self, item)
if item == 4:
- params['colormap'] = self.getColormap()
+ params["colormap"] = self.getColormap()
return params
@@ -432,7 +455,7 @@ class ImageData(ImageDataBase):
alphaImage = self.getAlphaData(copy=False)
if alphaImage is not None:
# Apply transparency
- image[:,:, 3] = image[:,:, 3] * alphaImage
+ image[:, :, 3] = image[:, :, 3] * alphaImage
return image
def getAlternativeImageData(self, copy=True):
@@ -460,7 +483,7 @@ class ImageData(ImageDataBase):
return numpy.array(self.__alpha, copy=copy)
def setData(self, data, alternative=None, alpha=None, copy=True):
- """"Set the image data and optionally an alternative RGB(A) representation
+ """Set the image data and optionally an alternative RGB(A) representation
:param numpy.ndarray data: Data array with 2 dimensions (h, w)
:param alternative: RGB(A) image to display instead of data,
@@ -485,10 +508,10 @@ class ImageData(ImageDataBase):
if alpha is not None:
alpha = numpy.array(alpha, copy=copy)
assert alpha.shape == data.shape
- if alpha.dtype.kind != 'f':
+ if alpha.dtype.kind != "f":
alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
+ if numpy.any(numpy.logical_or(alpha < 0.0, alpha > 1.0)):
+ alpha = numpy.clip(alpha, 0.0, 1.0)
self.__alpha = alpha
super().setData(data)
@@ -513,11 +536,13 @@ class ImageRgba(ImageBase):
if data.size == 0:
return None # No data to display
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=None,
- alpha=self.getAlpha())
+ return backend.addImage(
+ data,
+ origin=self.getOrigin(),
+ scale=self.getScale(),
+ colormap=None,
+ alpha=self.getAlpha(),
+ )
def getRgbaImageData(self, copy=True):
"""Get the displayed RGB(A) image
@@ -534,8 +559,14 @@ class ImageRgba(ImageBase):
False to use internal representation (do not modify!)
"""
data = numpy.array(data, copy=copy)
- assert data.ndim == 3
- assert data.shape[-1] in (3, 4)
+ if data.ndim != 3:
+ raise ValueError(
+ f"RGB(A) image is expected to be a 3D dataset. Got {data.ndim} dimensions"
+ )
+ if data.shape[-1] not in (3, 4):
+ raise ValueError(
+ f"RGB(A) image is expected to have 3 or 4 elements as last dimension. Got {data.shape[-1]}"
+ )
super().setData(data)
def _getValueData(self, copy=True):
@@ -546,10 +577,10 @@ class ImageRgba(ImageBase):
:param bool copy:
"""
rgba = self.getRgbaImageData(copy=False).astype(numpy.float32)
- intensity = (rgba[:, :, 0] * 0.299 +
- rgba[:, :, 1] * 0.587 +
- rgba[:, :, 2] * 0.114)
- intensity *= rgba[:, :, 3] / 255.
+ intensity = (
+ rgba[:, :, 0] * 0.299 + rgba[:, :, 1] * 0.587 + rgba[:, :, 2] * 0.114
+ )
+ intensity *= rgba[:, :, 3] / 255.0
return intensity
@@ -559,6 +590,7 @@ class MaskImageData(ImageData):
This class is used to flag mask items. This information is used to improve
internal silx widgets.
"""
+
pass
diff --git a/src/silx/gui/plot/items/image_aggregated.py b/src/silx/gui/plot/items/image_aggregated.py
index 75fdd59..b35e00a 100644
--- a/src/silx/gui/plot/items/image_aggregated.py
+++ b/src/silx/gui/plot/items/image_aggregated.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2021 European Synchrotron Radiation Facility
+# Copyright (c) 2021-2023 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,6 +31,7 @@ __date__ = "07/07/2021"
import enum
import logging
from typing import Tuple, Union
+import warnings
import numpy
@@ -69,7 +69,7 @@ class ImageDataAggregated(ImageDataBase):
self.__currentLOD = 0, 0
self.__aggregationMode = self.Aggregation.NONE
- def setAggregationMode(self, mode: Union[str,Aggregation]):
+ def setAggregationMode(self, mode: Union[str, Aggregation]):
"""Set the aggregation method used to reduce the data to screen resolution.
:param Aggregation mode: The aggregation method
@@ -116,12 +116,14 @@ class ImageDataAggregated(ImageDataBase):
if (lodx, lody) not in self.__cacheLODData:
height, width = data.shape
- self.__cacheLODData[(lodx, lody)] = aggregator(
- data[: (height // lody) * lody, : (width // lodx) * lodx].reshape(
- height // lody, lody, width // lodx, lodx
- ),
- axis=(1, 3),
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", category=RuntimeWarning)
+ self.__cacheLODData[(lodx, lody)] = aggregator(
+ data[
+ : (height // lody) * lody, : (width // lodx) * lodx
+ ].reshape(height // lody, lody, width // lodx, lodx),
+ axis=(1, 3),
+ )
self.__currentLOD = lodx, lody
displayedData = self.__cacheLODData[self.__currentLOD]
@@ -154,10 +156,7 @@ class ImageDataAggregated(ImageDataBase):
xaxis = plot.getXAxis()
yaxis = plot.getYAxis(axis)
- if (
- xaxis.getScale() != Axis.LINEAR
- or yaxis.getScale() != Axis.LINEAR
- ):
+ if xaxis.getScale() != Axis.LINEAR or yaxis.getScale() != Axis.LINEAR:
raise RuntimeError("Only available with linear axes")
xmin, xmax = xaxis.getLimits()
@@ -201,8 +200,10 @@ class ImageDataAggregated(ImageDataBase):
def __plotLimitsChanged(self):
"""Trigger update if level of details has changed"""
- if (self.getAggregationMode() != self.Aggregation.NONE and
- self.__currentLOD != self._getLevelOfDetails()):
+ if (
+ self.getAggregationMode() != self.Aggregation.NONE
+ and self.__currentLOD != self._getLevelOfDetails()
+ ):
self._updated()
@docstring(ImageDataBase)
diff --git a/src/silx/gui/plot/items/marker.py b/src/silx/gui/plot/items/marker.py
index 50d070c..b3da451 100755
--- a/src/silx/gui/plot/items/marker.py
+++ b/src/silx/gui/plot/items/marker.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,6 +23,7 @@
# ###########################################################################*/
"""This module provides markers item of the :class:`Plot`.
"""
+from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
@@ -31,11 +31,22 @@ __date__ = "06/03/2017"
import logging
+import numpy
from ....utils.proxy import docstring
-from .core import (Item, DraggableMixIn, ColorMixIn, LineMixIn, SymbolMixIn,
- ItemChangedType, YAxisMixIn)
+from .core import (
+ Item,
+ DraggableMixIn,
+ ColorMixIn,
+ LineMixIn,
+ SymbolMixIn,
+ ItemChangedType,
+ YAxisMixIn,
+)
+from silx import config
from silx.gui import qt
+from silx.gui import colors
+
_logger = logging.getLogger(__name__)
@@ -48,7 +59,7 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
sigDragFinished = qt.Signal()
"""Signal emitted when the marker is released"""
- _DEFAULT_COLOR = (0., 0., 0., 1.)
+ _DEFAULT_COLOR = (0.0, 0.0, 0.0, 1.0)
"""Default color of the markers"""
def __init__(self):
@@ -57,14 +68,21 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
ColorMixIn.__init__(self)
YAxisMixIn.__init__(self)
- self._text = ''
+ self._text = ""
+ self._font = None
+ if config.DEFAULT_PLOT_MARKER_TEXT_FONT_SIZE is not None:
+ self._font = qt.QFont(
+ qt.QApplication.instance().font().family(),
+ config.DEFAULT_PLOT_MARKER_TEXT_FONT_SIZE,
+ )
+
self._x = None
self._y = None
+ self._bgColor: colors.RGBAColorType | None = None
self._constraint = self._defaultConstraint
self.__isBeingDragged = False
- def _addRendererCall(self, backend,
- symbol=None, linestyle='-', linewidth=1):
+ def _addRendererCall(self, backend, symbol=None, linestyle="-", linewidth=1):
"""Perform the update of the backend renderer"""
return backend.addMarker(
x=self.getXPosition(),
@@ -75,7 +93,10 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
linestyle=linestyle,
linewidth=linewidth,
constraint=self.getConstraint(),
- yaxis=self.getYAxis())
+ yaxis=self.getYAxis(),
+ font=self._font, # Do not use getFont to spare creating a new QFont
+ bgcolor=self.getBackgroundColor(),
+ )
def _addBackendRenderer(self, backend):
"""Update backend renderer"""
@@ -109,6 +130,39 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
self._text = text
self._updated(ItemChangedType.TEXT)
+ def getFont(self) -> qt.QFont | None:
+ """Returns a copy of the QFont used to render text.
+
+ To modify the text font, use :meth:`setFont`.
+ """
+ return None if self._font is None else qt.QFont(self._font)
+
+ def setFont(self, font: qt.QFont | None):
+ """Set the QFont used to render text, use None for default.
+
+ A copy is stored, so further modification of the provided font are not taken into account.
+ """
+ if font != self._font:
+ self._font = None if font is None else qt.QFont(font)
+ self._updated(ItemChangedType.FONT)
+
+ def getBackgroundColor(self) -> colors.RGBAColorType | None:
+ """Returns the RGBA background color of the item"""
+ return self._bgColor
+
+ def setBackgroundColor(self, color):
+ """Set item text background color
+
+ :param color: color(s) to be used as a str ("#RRGGBB") or (npoints, 4)
+ unsigned byte array or one of the predefined color names
+ defined in colors.py
+ """
+ if color is not None:
+ color = colors.rgba(color)
+ if self._bgColor != color:
+ self._bgColor = color
+ self._updated(ItemChangedType.BACKGROUND_COLOR)
+
def getXPosition(self):
"""Returns the X position of the marker line in data coordinates
@@ -123,14 +177,14 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
"""
return self._y
- def getPosition(self):
+ def getPosition(self) -> tuple[float | None, float | None]:
"""Returns the (x, y) position of the marker in data coordinates
:rtype: 2-tuple of float or None
"""
return self._x, self._y
- def setPosition(self, x, y):
+ def setPosition(self, x: float, y: float):
"""Set marker position in data coordinates
Constraint are applied if any.
@@ -189,15 +243,15 @@ class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
class Marker(MarkerBase, SymbolMixIn):
"""Description of a marker"""
- _DEFAULT_SYMBOL = '+'
+ _DEFAULT_SYMBOL = "+"
"""Default symbol of the marker"""
def __init__(self):
MarkerBase.__init__(self)
SymbolMixIn.__init__(self)
- self._x = 0.
- self._y = 0.
+ self._x = 0.0
+ self._y = 0.0
def _addBackendRenderer(self, backend):
return self._addRendererCall(backend, symbol=self.getSymbol())
@@ -210,9 +264,9 @@ class Marker(MarkerBase, SymbolMixIn):
:param constraint: The constraint of the dragging of this marker
:type: constraint: callable or str
"""
- if constraint == 'horizontal':
+ if constraint == "horizontal":
constraint = self._horizontalConstraint
- elif constraint == 'vertical':
+ elif constraint == "vertical":
constraint = self._verticalConstraint
super(Marker, self)._setConstraint(constraint)
@@ -232,9 +286,9 @@ class _LineMarker(MarkerBase, LineMixIn):
LineMixIn.__init__(self)
def _addBackendRenderer(self, backend):
- return self._addRendererCall(backend,
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth())
+ return self._addRendererCall(
+ backend, linestyle=self.getLineStyle(), linewidth=self.getLineWidth()
+ )
class XMarker(_LineMarker):
@@ -242,7 +296,7 @@ class XMarker(_LineMarker):
def __init__(self):
_LineMarker.__init__(self)
- self._x = 0.
+ self._x = 0.0
def setPosition(self, x, y):
"""Set marker line position in data coordinates
@@ -264,7 +318,7 @@ class YMarker(_LineMarker):
def __init__(self):
_LineMarker.__init__(self)
- self._y = 0.
+ self._y = 0.0
def setPosition(self, x, y):
"""Set marker line position in data coordinates
diff --git a/src/silx/gui/plot/items/roi.py b/src/silx/gui/plot/items/roi.py
index 38a1424..7390b88 100644
--- a/src/silx/gui/plot/items/roi.py
+++ b/src/silx/gui/plot/items/roi.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2022 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
@@ -36,6 +35,7 @@ __date__ = "28/06/2018"
import logging
import numpy
+from typing import Tuple
from ... import utils
from .. import items
@@ -50,6 +50,7 @@ from ._roi_base import _RegionOfInterestBase
from ._roi_base import RegionOfInterest
from ._roi_base import HandleBasedROI
from ._arc_roi import ArcROI # noqa
+from ._band_roi import BandROI # noqa
from ._roi_base import InteractionModeMixIn # noqa
from ._roi_base import RoiInteractionMode # noqa
@@ -60,15 +61,15 @@ logger = logging.getLogger(__name__)
class PointROI(RegionOfInterest, items.SymbolMixIn):
"""A ROI identifying a point in a 2D plot."""
- ICON = 'add-shape-point'
- NAME = 'point markers'
+ ICON = "add-shape-point"
+ NAME = "point markers"
SHORT_NAME = "point"
"""Metadata for this kind of ROI"""
_plotShape = "point"
"""Plot shape which is used for the first interaction"""
- _DEFAULT_SYMBOL = '+'
+ _DEFAULT_SYMBOL = "+"
"""Default symbol of the PointROI
It overwrite the `SymbolMixIn` class attribte.
@@ -88,30 +89,26 @@ class PointROI(RegionOfInterest, items.SymbolMixIn):
self.setPosition(points[0])
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(PointROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
+ def getPosition(self) -> Tuple[float, float]:
+ """Returns the position of this ROI"""
return self._marker.getPosition()
def setPosition(self, pos):
"""Set the position of this ROI
- :param numpy.ndarray pos: 2d-coordinate of this point
+ :param pos: 2d-coordinate of this point
"""
self._marker.setPosition(*pos)
@@ -126,16 +123,15 @@ class PointROI(RegionOfInterest, items.SymbolMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = '%f %f' % self.getPosition()
+ params = "%f %f" % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
class CrossROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a point in a 2D plot and displayed as a cross
- """
+ """A ROI identifying a point in a 2D plot and displayed as a cross"""
- ICON = 'add-shape-cross'
- NAME = 'cross marker'
+ ICON = "add-shape-cross"
+ NAME = "cross marker"
SHORT_NAME = "cross"
"""Metadata for this kind of ROI"""
@@ -177,17 +173,14 @@ class CrossROI(HandleBasedROI, items.LineMixIn):
pos = points[0]
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
+ def getPosition(self) -> Tuple[float, float]:
+ """Returns the position of this ROI"""
return self._handle.getPosition()
- def setPosition(self, pos):
+ def setPosition(self, pos: Tuple[float, float]):
"""Set the position of this ROI
- :param numpy.ndarray pos: 2d-coordinate of this point
+ :param pos: 2d-coordinate of this point
"""
self._handle.setPosition(*pos)
@@ -213,8 +206,8 @@ class LineROI(HandleBasedROI, items.LineMixIn):
in the center to translate the full ROI.
"""
- ICON = 'add-shape-diagonal'
- NAME = 'line ROI'
+ ICON = "add-shape-diagonal"
+ NAME = "line ROI"
SHORT_NAME = "line"
"""Metadata for this kind of ROI"""
@@ -244,11 +237,12 @@ class LineROI(HandleBasedROI, items.LineMixIn):
self._updateItemProperty(event, self, self.__shape)
super(LineROI, self)._updated(event, checkVisibility)
- def _updatedStyle(self, event, style):
+ def _updatedStyle(self, event, style: items.CurveStyle):
super(LineROI, self)._updatedStyle(event, style)
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -257,7 +251,7 @@ class LineROI(HandleBasedROI, items.LineMixIn):
def _updateText(self, text):
self._handleLabel.setText(text)
- def setEndPoints(self, startPoint, endPoint):
+ def setEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
"""Set this line location using the ending points
:param numpy.ndarray startPoint: Staring bounding point of the line
@@ -266,7 +260,7 @@ class LineROI(HandleBasedROI, items.LineMixIn):
if not numpy.array_equal((startPoint, endPoint), self.getEndPoints()):
self.__updateEndPoints(startPoint, endPoint)
- def __updateEndPoints(self, startPoint, endPoint):
+ def __updateEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
"""Update marker and shape to match given end points
:param numpy.ndarray startPoint: Staring bounding point of the line
@@ -328,28 +322,44 @@ class LineROI(HandleBasedROI, items.LineMixIn):
return False
return (
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=bottom_left, seg2_end_pt=bottom_right) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=bottom_right, seg2_end_pt=top_right) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=top_right, seg2_end_pt=top_left) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=top_left, seg2_end_pt=bottom_left)
+ segments_intersection(
+ seg1_start_pt=line_pt1,
+ seg1_end_pt=line_pt2,
+ seg2_start_pt=bottom_left,
+ seg2_end_pt=bottom_right,
+ )
+ or segments_intersection(
+ seg1_start_pt=line_pt1,
+ seg1_end_pt=line_pt2,
+ seg2_start_pt=bottom_right,
+ seg2_end_pt=top_right,
+ )
+ or segments_intersection(
+ seg1_start_pt=line_pt1,
+ seg1_end_pt=line_pt2,
+ seg2_start_pt=top_right,
+ seg2_end_pt=top_left,
+ )
+ or segments_intersection(
+ seg1_start_pt=line_pt1,
+ seg1_end_pt=line_pt2,
+ seg2_start_pt=top_left,
+ seg2_end_pt=bottom_left,
+ )
) is not None
def __str__(self):
start, end = self.getEndPoints()
params = start[0], start[1], end[0], end[1]
- params = 'start: %f %f; end: %f %f' % params
+ params = "start: %f %f; end: %f %f" % params
return "%s(%s)" % (self.__class__.__name__, params)
class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying an horizontal line in a 2D plot."""
- ICON = 'add-shape-horizontal'
- NAME = 'horizontal line ROI'
+ ICON = "add-shape-horizontal"
+ NAME = "horizontal line ROI"
SHORT_NAME = "hline"
"""Metadata for this kind of ROI"""
@@ -366,16 +376,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
self.addItem(self._marker)
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(HorizontalLineROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
self._marker.setLineStyle(style.getLineStyle())
@@ -387,18 +396,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
return
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
+ def getPosition(self) -> float:
+ """Returns the position of this line if the horizontal axis"""
pos = self._marker.getPosition()
return pos[1]
- def setPosition(self, pos):
+ def setPosition(self, pos: float):
"""Set the position of this ROI
- :param float pos: Horizontal position of this line
+ :param pos: Horizontal position of this line
"""
self._marker.setPosition(0, pos)
@@ -412,15 +418,15 @@ class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = 'y: %f' % self.getPosition()
+ params = "y: %f" % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
class VerticalLineROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying a vertical line in a 2D plot."""
- ICON = 'add-shape-vertical'
- NAME = 'vertical line ROI'
+ ICON = "add-shape-vertical"
+ NAME = "vertical line ROI"
SHORT_NAME = "vline"
"""Metadata for this kind of ROI"""
@@ -437,16 +443,15 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
self.addItem(self._marker)
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
self._updateItemProperty(event, self, self._marker)
super(VerticalLineROI, self)._updated(event, checkVisibility)
+ def _updateText(self, text: str):
+ self._marker.setText(text)
+
def _updatedStyle(self, event, style):
self._marker.setColor(style.getColor())
self._marker.setLineStyle(style.getLineStyle())
@@ -456,15 +461,12 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
pos = points[0, 0]
self.setPosition(pos)
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
+ def getPosition(self) -> float:
+ """Returns the position of this line if the horizontal axis"""
pos = self._marker.getPosition()
return pos[0]
- def setPosition(self, pos):
+ def setPosition(self, pos: float):
"""Set the position of this ROI
:param float pos: Horizontal position of this line
@@ -481,7 +483,7 @@ class VerticalLineROI(RegionOfInterest, items.LineMixIn):
self.sigRegionChanged.emit()
def __str__(self):
- params = 'x: %f' % self.getPosition()
+ params = "x: %f" % self.getPosition()
return "%s(%s)" % (self.__class__.__name__, params)
@@ -492,8 +494,8 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
center to translate the full ROI.
"""
- ICON = 'add-shape-rectangle'
- NAME = 'rectangle ROI'
+ ICON = "add-shape-rectangle"
+ NAME = "rectangle ROI"
SHORT_NAME = "rectangle"
"""Metadata for this kind of ROI"""
@@ -530,6 +532,7 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -598,11 +601,12 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
self.setGeometry(center=position, size=size)
def setGeometry(self, origin=None, size=None, center=None):
- """Set the geometry of the ROI
- """
- if ((origin is None or numpy.array_equal(origin, self.getOrigin())) and
- (center is None or numpy.array_equal(center, self.getCenter())) and
- numpy.array_equal(size, self.getSize())):
+ """Set the geometry of the ROI"""
+ if (
+ (origin is None or numpy.array_equal(origin, self.getOrigin()))
+ and (center is None or numpy.array_equal(center, self.getCenter()))
+ and numpy.array_equal(size, self.getSize())
+ ):
return # Nothing has changed
self._updateGeometry(origin, size, center)
@@ -661,17 +665,38 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
points = numpy.array([current, current2])
# Switch handles if they were crossed by interaction
- if self._handleBottomLeft.getXPosition() > self._handleBottomRight.getXPosition():
- self._handleBottomLeft, self._handleBottomRight = self._handleBottomRight, self._handleBottomLeft
+ if (
+ self._handleBottomLeft.getXPosition()
+ > self._handleBottomRight.getXPosition()
+ ):
+ self._handleBottomLeft, self._handleBottomRight = (
+ self._handleBottomRight,
+ self._handleBottomLeft,
+ )
if self._handleTopLeft.getXPosition() > self._handleTopRight.getXPosition():
- self._handleTopLeft, self._handleTopRight = self._handleTopRight, self._handleTopLeft
-
- if self._handleBottomLeft.getYPosition() > self._handleTopLeft.getYPosition():
- self._handleBottomLeft, self._handleTopLeft = self._handleTopLeft, self._handleBottomLeft
-
- if self._handleBottomRight.getYPosition() > self._handleTopRight.getYPosition():
- self._handleBottomRight, self._handleTopRight = self._handleTopRight, self._handleBottomRight
+ self._handleTopLeft, self._handleTopRight = (
+ self._handleTopRight,
+ self._handleTopLeft,
+ )
+
+ if (
+ self._handleBottomLeft.getYPosition()
+ > self._handleTopLeft.getYPosition()
+ ):
+ self._handleBottomLeft, self._handleTopLeft = (
+ self._handleTopLeft,
+ self._handleBottomLeft,
+ )
+
+ if (
+ self._handleBottomRight.getYPosition()
+ > self._handleTopRight.getYPosition()
+ ):
+ self._handleBottomRight, self._handleTopRight = (
+ self._handleTopRight,
+ self._handleBottomRight,
+ )
self._setBound(points)
@@ -679,7 +704,7 @@ class RectangleROI(HandleBasedROI, items.LineMixIn):
origin = self.getOrigin()
w, h = self.getSize()
params = origin[0], origin[1], w, h
- params = 'origin: %f %f; width: %f; height: %f' % params
+ params = "origin: %f %f; width: %f; height: %f" % params
return "%s(%s)" % (self.__class__.__name__, params)
@@ -690,8 +715,8 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
and one anchor on the perimeter to change the radius.
"""
- ICON = 'add-shape-circle'
- NAME = 'circle ROI'
+ ICON = "add-shape-circle"
+ NAME = "circle ROI"
SHORT_NAME = "circle"
"""Metadata for this kind of ROI"""
@@ -731,6 +756,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -779,8 +805,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
self._updateGeometry()
def setGeometry(self, center, radius):
- """Set the geometry of the ROI
- """
+ """Set the geometry of the ROI"""
if numpy.array_equal(center, self.getCenter()):
self.setRadius(radius)
else:
@@ -797,8 +822,9 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
nbpoints = 27
angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
- circleShape = numpy.array((numpy.cos(angles) * self.__radius,
- numpy.sin(angles) * self.__radius)).T
+ circleShape = numpy.array(
+ (numpy.cos(angles) * self.__radius, numpy.sin(angles) * self.__radius)
+ ).T
circleShape += center
self.__shape.setPoints(circleShape)
self.sigRegionChanged.emit()
@@ -821,7 +847,7 @@ class CircleROI(HandleBasedROI, items.LineMixIn):
center = self.getCenter()
radius = self.getRadius()
params = center[0], center[1], radius
- params = 'center: %f %f; radius: %f;' % params
+ params = "center: %f %f; radius: %f;" % params
return "%s(%s)" % (self.__class__.__name__, params)
@@ -833,8 +859,8 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
minor-radius. These two anchors also allow to change the orientation.
"""
- ICON = 'add-shape-ellipse'
- NAME = 'ellipse ROI'
+ ICON = "add-shape-ellipse"
+ NAME = "ellipse ROI"
SHORT_NAME = "ellipse"
"""Metadata for this kind of ROI"""
@@ -860,8 +886,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
self.__shape = shape
self.addItem(shape)
- self._radius = 0., 0.
- self._orientation = 0. # angle in radians between the X-axis and the _handleAxis0
+ self._radius = 0.0, 0.0
+ self._orientation = (
+ 0.0 # angle in radians between the X-axis and the _handleAxis0
+ )
def _updated(self, event=None, checkVisibility=True):
if event == items.ItemChangedType.VISIBLE:
@@ -873,6 +901,7 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
def setFirstShapePoints(self, points):
assert len(points) == 2
@@ -905,9 +934,9 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
center = points[0]
radius = numpy.linalg.norm(points[0] - points[1])
orientation = self._calculateOrientation(points[0], points[1])
- self.setGeometry(center=center,
- radius=(radius, radius),
- orientation=orientation)
+ self.setGeometry(
+ center=center, radius=(radius, radius), orientation=orientation
+ )
def _updateText(self, text):
self._handleLabel.setText(text)
@@ -1007,10 +1036,11 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
# ensure that we store the orientation in range [0, 2*pi
orientation = numpy.mod(orientation, 2 * numpy.pi)
- if (numpy.array_equal(center, self.getCenter()) or
- radius != self._radius or
- orientation != self._orientation):
-
+ if (
+ numpy.array_equal(center, self.getCenter())
+ or radius != self._radius
+ or orientation != self._orientation
+ ):
# Update parameters directly
self._radius = radius
self._orientation = orientation
@@ -1030,10 +1060,18 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
# _handleAxis1 is the major axis
orientation -= numpy.pi / 2
- point0 = numpy.array([center[0] + self._radius[0] * numpy.cos(orientation),
- center[1] + self._radius[0] * numpy.sin(orientation)])
- point1 = numpy.array([center[0] - self._radius[1] * numpy.sin(orientation),
- center[1] + self._radius[1] * numpy.cos(orientation)])
+ point0 = numpy.array(
+ [
+ center[0] + self._radius[0] * numpy.cos(orientation),
+ center[1] + self._radius[0] * numpy.sin(orientation),
+ ]
+ )
+ point1 = numpy.array(
+ [
+ center[0] - self._radius[1] * numpy.sin(orientation),
+ center[1] + self._radius[1] * numpy.cos(orientation),
+ ]
+ )
with utils.blockSignals(self._handleAxis0):
self._handleAxis0.setPosition(*point0)
with utils.blockSignals(self._handleAxis1):
@@ -1043,10 +1081,12 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
nbpoints = 27
angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
- X = (self._radius[0] * numpy.cos(angles) * numpy.cos(orientation)
- - self._radius[1] * numpy.sin(angles) * numpy.sin(orientation))
- Y = (self._radius[0] * numpy.cos(angles) * numpy.sin(orientation)
- + self._radius[1] * numpy.sin(angles) * numpy.cos(orientation))
+ X = self._radius[0] * numpy.cos(angles) * numpy.cos(orientation) - self._radius[
+ 1
+ ] * numpy.sin(angles) * numpy.sin(orientation)
+ Y = self._radius[0] * numpy.cos(angles) * numpy.sin(orientation) + self._radius[
+ 1
+ ] * numpy.sin(angles) * numpy.cos(orientation)
ellipseShape = numpy.array((X, Y)).T
ellipseShape += center
@@ -1083,8 +1123,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
major, minor = self.getMajorRadius(), self.getMinorRadius()
delta = self.getOrientation()
x, y = position - self.getCenter()
- return ((x*numpy.cos(delta) + y*numpy.sin(delta))**2/major**2 +
- (x*numpy.sin(delta) - y*numpy.cos(delta))**2/minor**2) <= 1
+ return (
+ (x * numpy.cos(delta) + y * numpy.sin(delta)) ** 2 / major**2
+ + (x * numpy.sin(delta) - y * numpy.cos(delta)) ** 2 / minor**2
+ ) <= 1
def __str__(self):
center = self.getCenter()
@@ -1092,7 +1134,10 @@ class EllipseROI(HandleBasedROI, items.LineMixIn):
minor = self.getMinorRadius()
orientation = self.getOrientation()
params = center[0], center[1], major, minor, orientation
- params = 'center: %f %f; major radius: %f: minor radius: %f; orientation: %f' % params
+ params = (
+ "center: %f %f; major radius: %f: minor radius: %f; orientation: %f"
+ % params
+ )
return "%s(%s)" % (self.__class__.__name__, params)
@@ -1102,8 +1147,8 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
This ROI provides 1 anchor for each point of the polygon.
"""
- ICON = 'add-shape-polygon'
- NAME = 'polygon ROI'
+ ICON = "add-shape-polygon"
+ NAME = "polygon ROI"
SHORT_NAME = "polygon"
"""Metadata for this kind of ROI"""
@@ -1134,6 +1179,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
self.__shape.setColor(style.getColor())
self.__shape.setLineStyle(style.getLineStyle())
self.__shape.setLineWidth(style.getLineWidth())
+ self.__shape.setLineGapColor(style.getLineGapColor())
if self._handleClose is not None:
color = self._computeHandleColor(style.getColor())
self._handleClose.setColor(color)
@@ -1156,8 +1202,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
self.setPoints(points)
def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
+ """Called when the ROI creation interaction was started."""
# Handle to see where to close the polygon
self._handleClose = self.addUserHandle()
self._handleClose.setSymbol("o")
@@ -1178,8 +1223,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
return self._handleClose is not None
def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
+ """Called when the ROI creation interaction was finalized."""
self.removeHandle(self._handleClose)
self._handleClose = None
self.removeItem(self.__shape)
@@ -1206,7 +1250,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
:param numpy.ndarray pos: 2d-coordinate of this point
"""
- assert(len(points.shape) == 2 and points.shape[1] == 2)
+ assert len(points.shape) == 2 and points.shape[1] == 2
if numpy.array_equal(points, self._points):
return # Nothing has changed
@@ -1277,7 +1321,7 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
def __str__(self):
points = self._points
- params = '; '.join('%f %f' % (pt[0], pt[1]) for pt in points)
+ params = "; ".join("%f %f" % (pt[0], pt[1]) for pt in points)
return "%s(%s)" % (self.__class__.__name__, params)
@docstring(HandleBasedROI)
@@ -1300,8 +1344,8 @@ class PolygonROI(HandleBasedROI, items.LineMixIn):
class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
"""A ROI identifying an horizontal range in a 1D plot."""
- ICON = 'add-range-horizontal'
- NAME = 'horizontal range ROI'
+ ICON = "add-range-horizontal"
+ NAME = "horizontal range ROI"
SHORT_NAME = "hrange"
_plotShape = "line"
@@ -1333,16 +1377,13 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
self._updatePos(vmin, vmax)
def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- self._updateText()
- elif event == items.ItemChangedType.EDITABLE:
+ if event == items.ItemChangedType.EDITABLE:
self._updateEditable()
- self._updateText()
+ self._updateText(self.getText())
elif event == items.ItemChangedType.LINE_STYLE:
markers = [self._markerMin, self._markerMax]
self._updateItemProperty(event, self, markers)
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
+ elif event in [items.ItemChangedType.VISIBLE, items.ItemChangedType.SELECTABLE]:
markers = [self._markerMin, self._markerMax, self._markerCen]
self._updateItemProperty(event, self, markers)
super(HorizontalRangeROI, self)._updated(event, checkVisibility)
@@ -1353,8 +1394,7 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
m.setColor(style.getColor())
m.setLineWidth(style.getLineWidth())
- def _updateText(self):
- text = self.getName()
+ def _updateText(self, text: str):
if self.isEditable():
self._markerMin.setText("")
self._markerCen.setText(text)
@@ -1409,8 +1449,10 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
err = "Can't set vmin or vmax to None"
raise ValueError(err)
if vmin > vmax:
- err = "Can't set vmin and vmax because vmin >= vmax " \
- "vmin = %s, vmax = %s" % (vmin, vmax)
+ err = (
+ "Can't set vmin and vmax because vmin >= vmax "
+ "vmin = %s, vmax = %s" % (vmin, vmax)
+ )
raise ValueError(err)
self._updatePos(vmin, vmax)
@@ -1515,5 +1557,5 @@ class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
def __str__(self):
vrange = self.getRange()
- params = 'min: %f; max: %f' % vrange
+ params = "min: %f; max: %f" % vrange
return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/src/silx/gui/plot/items/scatter.py b/src/silx/gui/plot/items/scatter.py
index fdc66f7..c46b60c 100644
--- a/src/silx/gui/plot/items/scatter.py
+++ b/src/silx/gui/plot/items/scatter.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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,9 +24,6 @@
"""This module provides the :class:`Scatter` item of the :class:`Plot`.
"""
-from __future__ import division
-
-
__authors__ = ["T. Vincent", "P. Knobel"]
__license__ = "MIT"
__date__ = "29/03/2017"
@@ -37,6 +33,7 @@ from collections import namedtuple
import logging
import threading
import numpy
+from matplotlib.tri import LinearTriInterpolator, Triangulation
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, CancelledError
@@ -45,7 +42,6 @@ from ....utils.proxy import docstring
from ....math.combo import min_max
from ....math.histogram import Histogramnd
from ....utils.weakref import WeakList
-from .._utils.delaunay import delaunay
from .core import PointsBase, ColormapMixIn, ScatterVisualizationMixIn
from .axis import Axis
from ._pick import PickingResult
@@ -55,8 +51,7 @@ _logger = logging.getLogger(__name__)
class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
- """:class:`ThreadPoolExecutor` with an extra :meth:`submit_greedy` method.
- """
+ """:class:`ThreadPoolExecutor` with an extra :meth:`submit_greedy` method."""
def __init__(self, *args, **kwargs):
super(_GreedyThreadPoolExecutor, self).__init__(*args, **kwargs)
@@ -80,8 +75,7 @@ class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
if not future.done():
future.cancel()
- future = super(_GreedyThreadPoolExecutor, self).submit(
- fn, *args, **kwargs)
+ future = super(_GreedyThreadPoolExecutor, self).submit(fn, *args, **kwargs)
self.__futures[queue].append(future)
return future
@@ -89,6 +83,7 @@ class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
# Functions to guess grid shape from coordinates
+
def _get_z_line_length(array):
"""Return length of line if array is a Z-like 2D regular grid.
@@ -101,7 +96,7 @@ def _get_z_line_length(array):
if len(sign) == 0 or sign[0] == 0: # We don't handle that
return 0
# Check this way to account for 0 sign (i.e., diff == 0)
- beginnings = numpy.where(sign == - sign[0])[0] + 1
+ beginnings = numpy.where(sign == -sign[0])[0] + 1
if len(beginnings) == 0:
return 0
length = beginnings[0]
@@ -125,11 +120,11 @@ def _guess_z_grid_shape(x, y):
"""
width = _get_z_line_length(x)
if width != 0:
- return 'row', (int(numpy.ceil(len(x) / width)), width)
+ return "row", (int(numpy.ceil(len(x) / width)), width)
else:
height = _get_z_line_length(y)
if height != 0:
- return 'column', (height, int(numpy.ceil(len(y) / height)))
+ return "column", (height, int(numpy.ceil(len(y) / height)))
return None
@@ -143,7 +138,7 @@ def is_monotonic(array):
:rtype: int
"""
diff = numpy.diff(numpy.ravel(array))
- with numpy.errstate(invalid='ignore'):
+ with numpy.errstate(invalid="ignore"):
if numpy.all(diff >= 0):
return 1
elif numpy.all(diff <= 0):
@@ -172,7 +167,7 @@ def _guess_grid(x, y):
else:
# Cannot guess a regular grid
# Let's assume it's a single line
- order = 'row' # or 'column' doesn't matter for a single line
+ order = "row" # or 'column' doesn't matter for a single line
y_monotonic = is_monotonic(y)
if is_monotonic(x) or y_monotonic: # we can guess a line
x_min, x_max = min_max(x)
@@ -215,18 +210,24 @@ def _quadrilateral_grid_coords(points):
neighbour_view = numpy.lib.stride_tricks.as_strided(
points,
shape=(dim0 - 1, dim1 - 1, 2, 2, points.shape[2]),
- strides=points.strides[:2] + points.strides[:2] + points.strides[-1:], writeable=False)
+ strides=points.strides[:2] + points.strides[:2] + points.strides[-1:],
+ writeable=False,
+ )
inner_points = numpy.mean(neighbour_view, axis=(2, 3))
grid_points[1:-1, 1:-1] = inner_points
# Compute 'vertical' sides
# Alternative: grid_points[1:-1, [0, -1]] = points[:-1, [0, -1]] + points[1:, [0, -1]] - inner_points[:, [0, -1]]
- grid_points[1:-1, [0, -1], 0] = points[:-1, [0, -1], 0] + points[1:, [0, -1], 0] - inner_points[:, [0, -1], 0]
+ grid_points[1:-1, [0, -1], 0] = (
+ points[:-1, [0, -1], 0] + points[1:, [0, -1], 0] - inner_points[:, [0, -1], 0]
+ )
grid_points[1:-1, [0, -1], 1] = inner_points[:, [0, -1], 1]
# Compute 'horizontal' sides
grid_points[[0, -1], 1:-1, 0] = inner_points[[0, -1], :, 0]
- grid_points[[0, -1], 1:-1, 1] = points[[0, -1], :-1, 1] + points[[0, -1], 1:, 1] - inner_points[[0, -1], :, 1]
+ grid_points[[0, -1], 1:-1, 1] = (
+ points[[0, -1], :-1, 1] + points[[0, -1], 1:, 1] - inner_points[[0, -1], :, 1]
+ )
# Compute corners
d0, d1 = [0, 0, -1, -1], [0, -1, -1, 0]
@@ -263,11 +264,13 @@ def _quadrilateral_grid_as_triangles(points):
_RegularGridInfo = namedtuple(
- '_RegularGridInfo', ['bounds', 'origin', 'scale', 'shape', 'order'])
+ "_RegularGridInfo", ["bounds", "origin", "scale", "shape", "order"]
+)
_HistogramInfo = namedtuple(
- '_HistogramInfo', ['mean', 'count', 'sum', 'origin', 'scale', 'shape'])
+ "_HistogramInfo", ["mean", "count", "sum", "origin", "scale", "shape"]
+)
class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
@@ -282,7 +285,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
ScatterVisualizationMixIn.Visualization.REGULAR_GRID,
ScatterVisualizationMixIn.Visualization.IRREGULAR_GRID,
ScatterVisualizationMixIn.Visualization.BINNED_STATISTIC,
- )
+ )
"""Overrides supported Visualizations"""
def __init__(self):
@@ -292,7 +295,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
self._value = ()
self.__alpha = None
# Cache Delaunay triangulation future object
- self.__delaunayFuture = None
+ self.__triangulationFuture = None
# Cache interpolator future object
self.__interpolatorFuture = None
self.__executor = None
@@ -314,7 +317,9 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
data = getattr(
histoInfo,
self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ )
else:
data = self.getValueData(copy=False)
self._setColormappedData(data, copy=False)
@@ -323,8 +328,9 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
def setVisualization(self, mode):
previous = self.getVisualization()
if super().setVisualization(mode):
- if (bool(mode is self.Visualization.BINNED_STATISTIC) ^
- bool(previous is self.Visualization.BINNED_STATISTIC)):
+ if bool(mode is self.Visualization.BINNED_STATISTIC) ^ bool(
+ previous is self.Visualization.BINNED_STATISTIC
+ ):
self._updateColormappedData()
return True
else:
@@ -335,16 +341,22 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
parameter = self.VisualizationParameter.from_value(parameter)
if super(Scatter, self).setVisualizationParameter(parameter, value):
- if parameter in (self.VisualizationParameter.GRID_BOUNDS,
- self.VisualizationParameter.GRID_MAJOR_ORDER,
- self.VisualizationParameter.GRID_SHAPE):
+ if parameter in (
+ self.VisualizationParameter.GRID_BOUNDS,
+ self.VisualizationParameter.GRID_MAJOR_ORDER,
+ self.VisualizationParameter.GRID_SHAPE,
+ ):
self.__cacheRegularGridInfo = None
- if parameter in (self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
- self.VisualizationParameter.DATA_BOUNDS_HINT):
- if parameter in (self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- self.VisualizationParameter.DATA_BOUNDS_HINT):
+ if parameter in (
+ self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
+ self.VisualizationParameter.DATA_BOUNDS_HINT,
+ ):
+ if parameter in (
+ self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
+ self.VisualizationParameter.DATA_BOUNDS_HINT,
+ ):
self.__cacheHistogramInfo = None # Clean-up cache
if self.getVisualization() is self.Visualization.BINNED_STATISTIC:
self._updateColormappedData()
@@ -355,14 +367,16 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
@docstring(ScatterVisualizationMixIn)
def getCurrentVisualizationParameter(self, parameter):
value = self.getVisualizationParameter(parameter)
- if (parameter is self.VisualizationParameter.DATA_BOUNDS_HINT or
- value is not None):
+ if (
+ parameter is self.VisualizationParameter.DATA_BOUNDS_HINT
+ or value is not None
+ ):
return value # Value has been set, return it
elif parameter is self.VisualizationParameter.GRID_BOUNDS:
grid = self.__getRegularGridInfo()
return None if grid is None else grid.bounds
-
+
elif parameter is self.VisualizationParameter.GRID_MAJOR_ORDER:
grid = self.__getRegularGridInfo()
return None if grid is None else grid.order
@@ -382,15 +396,19 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Get grid info"""
if self.__cacheRegularGridInfo is None:
shape = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_SHAPE)
+ self.VisualizationParameter.GRID_SHAPE
+ )
order = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_MAJOR_ORDER)
+ self.VisualizationParameter.GRID_MAJOR_ORDER
+ )
if shape is None or order is None:
- guess = _guess_grid(self.getXData(copy=False),
- self.getYData(copy=False))
+ guess = _guess_grid(
+ self.getXData(copy=False), self.getYData(copy=False)
+ )
if guess is None:
_logger.warning(
- 'Cannot guess a grid: Cannot display as regular grid image')
+ "Cannot guess a grid: Cannot display as regular grid image"
+ )
return None
if shape is None:
shape = guess[1]
@@ -401,16 +419,18 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if nbpoints > shape[0] * shape[1]:
# More data points that provided grid shape: enlarge grid
_logger.warning(
- "More data points than provided grid shape size: extends grid")
+ "More data points than provided grid shape size: extends grid"
+ )
dim0, dim1 = shape
- if order == 'row': # keep dim1, enlarge dim0
+ if order == "row": # keep dim1, enlarge dim0
dim0 = nbpoints // dim1 + (1 if nbpoints % dim1 else 0)
else: # keep dim0, enlarge dim1
dim1 = nbpoints // dim0 + (1 if nbpoints % dim0 else 0)
shape = dim0, dim1
bounds = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_BOUNDS)
+ self.VisualizationParameter.GRID_BOUNDS
+ )
if bounds is None:
x, y = self.getXData(copy=False), self.getYData(copy=False)
min_, max_ = min_max(x)
@@ -420,10 +440,12 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
bounds = (xRange[0], yRange[0]), (xRange[1], yRange[1])
begin, end = bounds
- scale = ((end[0] - begin[0]) / max(1, shape[1] - 1),
- (end[1] - begin[1]) / max(1, shape[0] - 1))
+ scale = (
+ (end[0] - begin[0]) / max(1, shape[1] - 1),
+ (end[1] - begin[1]) / max(1, shape[0] - 1),
+ )
if scale[0] == 0 and scale[1] == 0:
- scale = 1., 1.
+ scale = 1.0, 1.0
elif scale[0] == 0:
scale = scale[1], scale[1]
elif scale[1] == 0:
@@ -432,7 +454,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
origin = begin[0] - 0.5 * scale[0], begin[1] - 0.5 * scale[1]
self.__cacheRegularGridInfo = _RegularGridInfo(
- bounds=bounds, origin=origin, scale=scale, shape=shape, order=order)
+ bounds=bounds, origin=origin, scale=scale, shape=shape, order=order
+ )
return self.__cacheRegularGridInfo
@@ -440,9 +463,10 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Get histogram info"""
if self.__cacheHistogramInfo is None:
shape = self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_SHAPE)
+ self.VisualizationParameter.BINNED_STATISTIC_SHAPE
+ )
if shape is None:
- shape = 100, 100 # TODO compute auto shape
+ shape = 100, 100 # TODO compute auto shape
x, y, values = self.getData(copy=False)[:3]
if len(x) == 0: # No histogram
@@ -455,31 +479,40 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if not numpy.issubdtype(values.dtype, numpy.floating):
values = values.astype(numpy.float64)
- ranges = (tuple(min_max(y, finite=True)),
- tuple(min_max(x, finite=True)))
+ ranges = (tuple(min_max(y, finite=True)), tuple(min_max(x, finite=True)))
rangesHint = self.getVisualizationParameter(
- self.VisualizationParameter.DATA_BOUNDS_HINT)
+ self.VisualizationParameter.DATA_BOUNDS_HINT
+ )
if rangesHint is not None:
- ranges = tuple((min(dataMin, hintMin), max(dataMax, hintMax))
- for (dataMin, dataMax), (hintMin, hintMax) in zip(ranges, rangesHint))
+ ranges = tuple(
+ (min(dataMin, hintMin), max(dataMax, hintMax))
+ for (dataMin, dataMax), (hintMin, hintMax) in zip(
+ ranges, rangesHint
+ )
+ )
points = numpy.transpose(numpy.array((y, x)))
counts, sums, bin_edges = Histogramnd(
- points,
- histo_range=ranges,
- n_bins=shape,
- weights=values)
+ points, histo_range=ranges, n_bins=shape, weights=values
+ )
yEdges, xEdges = bin_edges
origin = xEdges[0], yEdges[0]
- scale = ((xEdges[-1] - xEdges[0]) / (len(xEdges) - 1),
- (yEdges[-1] - yEdges[0]) / (len(yEdges) - 1))
+ scale = (
+ (xEdges[-1] - xEdges[0]) / (len(xEdges) - 1),
+ (yEdges[-1] - yEdges[0]) / (len(yEdges) - 1),
+ )
- with numpy.errstate(divide='ignore', invalid='ignore'):
+ with numpy.errstate(divide="ignore", invalid="ignore"):
histo = sums / counts
self.__cacheHistogramInfo = _HistogramInfo(
- mean=histo, count=counts, sum=sums,
- origin=origin, scale=scale, shape=shape)
+ mean=histo,
+ count=counts,
+ sum=sums,
+ origin=origin,
+ scale=scale,
+ shape=shape,
+ )
return self.__cacheHistogramInfo
@@ -499,7 +532,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
"""Update backend renderer"""
# Filter-out values <= 0
xFiltered, yFiltered, valueFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
+ copy=False, displayed=True
+ )
# Remove not finite numbers (this includes filtered out x, y <= 0)
mask = numpy.logical_and(numpy.isfinite(xFiltered), numpy.isfinite(yFiltered))
@@ -513,62 +547,79 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if visualization is self.Visualization.BINNED_STATISTIC:
plot = self.getPlot()
- if (plot is None or
- plot.getXAxis().getScale() != Axis.LINEAR or
- plot.getYAxis().getScale() != Axis.LINEAR):
+ if (
+ plot is None
+ or plot.getXAxis().getScale() != Axis.LINEAR
+ or plot.getYAxis().getScale() != Axis.LINEAR
+ ):
# Those visualizations are not available with log scaled axes
return None
histoInfo = self.__getHistogramInfo()
if histoInfo is None:
return None
- data = getattr(histoInfo, self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
+ data = getattr(
+ histoInfo,
+ self.getVisualizationParameter(
+ self.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ )
return backend.addImage(
data=data,
origin=histoInfo.origin,
scale=histoInfo.scale,
colormap=self.getColormap(),
- alpha=self.getAlpha())
+ alpha=self.getAlpha(),
+ )
elif visualization is self.Visualization.POINTS:
rgbacolors = self.__applyColormapToData()
- return backend.addCurve(xFiltered, yFiltered,
- color=rgbacolors[mask],
- symbol=self.getSymbol(),
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=xerror,
- yerror=yerror,
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=self.getSymbolSize(),
- baseline=None)
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=rgbacolors[mask],
+ gapcolor=None,
+ symbol=self.getSymbol(),
+ linewidth=0,
+ linestyle="",
+ yaxis="left",
+ xerror=xerror,
+ yerror=yerror,
+ fill=False,
+ alpha=self.getAlpha(),
+ symbolsize=self.getSymbolSize(),
+ baseline=None,
+ )
else:
plot = self.getPlot()
- if (plot is None or
- plot.getXAxis().getScale() != Axis.LINEAR or
- plot.getYAxis().getScale() != Axis.LINEAR):
+ if (
+ plot is None
+ or plot.getXAxis().getScale() != Axis.LINEAR
+ or plot.getYAxis().getScale() != Axis.LINEAR
+ ):
# Those visualizations are not available with log scaled axes
return None
if visualization is self.Visualization.SOLID:
- triangulation = self._getDelaunay().result()
- if triangulation is None:
+ try:
+ triangulation = self._getTriangulationFuture().result()
+ except (RuntimeError, ValueError):
_logger.warning(
- 'Cannot get a triangulation: Cannot display as solid surface')
+ "Cannot get a triangulation: Cannot display as solid surface"
+ )
return None
else:
rgbacolors = self.__applyColormapToData()
- triangles = triangulation.simplices.astype(numpy.int32)
- return backend.addTriangles(xFiltered,
- yFiltered,
- triangles,
- color=rgbacolors[mask],
- alpha=self.getAlpha())
+ triangles = triangulation.triangles.astype(numpy.int32)
+ return backend.addTriangles(
+ xFiltered,
+ yFiltered,
+ triangles,
+ color=rgbacolors[mask],
+ alpha=self.getAlpha(),
+ )
elif visualization is self.Visualization.REGULAR_GRID:
gridInfo = self.__getRegularGridInfo()
@@ -576,7 +627,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
return None
dim0, dim1 = gridInfo.shape
- if gridInfo.order == 'column': # transposition needed
+ if gridInfo.order == "column": # transposition needed
dim0, dim1 = dim1, dim0
values = self.getValueData(copy=False)
@@ -584,20 +635,21 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
image = values.reshape(dim0, dim1)
else:
# The points do not fill the whole image
- if (self.__alpha is None and
- numpy.issubdtype(values.dtype, numpy.floating)):
+ if self.__alpha is None and numpy.issubdtype(
+ values.dtype, numpy.floating
+ ):
image = numpy.empty(dim0 * dim1, dtype=values.dtype)
- image[:len(values)] = values
- image[len(values):] = float('nan') # Transparent pixels
+ image[: len(values)] = values
+ image[len(values) :] = float("nan") # Transparent pixels
image.shape = dim0, dim1
else: # Per value alpha or no NaN, so convert to RGBA
rgbacolors = self.__applyColormapToData()
image = numpy.empty((dim0 * dim1, 4), dtype=numpy.uint8)
- image[:len(rgbacolors)] = rgbacolors
- image[len(rgbacolors):] = (0, 0, 0, 0) # Transparent pixels
+ image[: len(rgbacolors)] = rgbacolors
+ image[len(rgbacolors) :] = (0, 0, 0, 0) # Transparent pixels
image.shape = dim0, dim1, 4
- if gridInfo.order == 'column':
+ if gridInfo.order == "column":
if image.ndim == 2:
image = numpy.transpose(image)
else:
@@ -617,7 +669,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
origin=gridInfo.origin,
scale=gridInfo.scale,
colormap=colormap,
- alpha=self.getAlpha())
+ alpha=self.getAlpha(),
+ )
elif visualization is self.Visualization.IRREGULAR_GRID:
gridInfo = self.__getRegularGridInfo()
@@ -633,33 +686,37 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
nbpoints = len(xFiltered)
if nbpoints == 1:
# single point, render as a square points
- return backend.addCurve(xFiltered, yFiltered,
- color=rgbacolors[mask],
- symbol='s',
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=None,
- yerror=None,
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=7,
- baseline=None)
+ return backend.addCurve(
+ xFiltered,
+ yFiltered,
+ color=rgbacolors[mask],
+ gapcolor=None,
+ symbol="s",
+ linewidth=0,
+ linestyle="",
+ yaxis="left",
+ xerror=None,
+ yerror=None,
+ fill=False,
+ alpha=self.getAlpha(),
+ symbolsize=7,
+ baseline=None,
+ )
# Make shape include all points
gridOrder = gridInfo.order
if nbpoints != numpy.prod(shape):
- if gridOrder == 'row':
+ if gridOrder == "row":
shape = int(numpy.ceil(nbpoints / shape[1])), shape[1]
- else: # column-major order
+ else: # column-major order
shape = shape[0], int(numpy.ceil(nbpoints / shape[0]))
if shape[0] < 2 or shape[1] < 2: # Single line, at least 2 points
points = numpy.ones((2, nbpoints, 2), dtype=numpy.float64)
# Use row/column major depending on shape, not on info value
- gridOrder = 'row' if shape[0] == 1 else 'column'
+ gridOrder = "row" if shape[0] == 1 else "column"
- if gridOrder == 'row':
+ if gridOrder == "row":
points[0, :, 0] = xFiltered
points[0, :, 1] = yFiltered
else: # column-major order
@@ -667,35 +724,51 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
points[0, :, 1] = xFiltered
# Add a second line that will be clipped in the end
- points[1, :-1] = points[0, :-1] + numpy.cross(
- points[0, 1:] - points[0, :-1], (0., 0., 1.))[:, :2]
- points[1, -1] = points[0, -1] + numpy.cross(
- points[0, -1] - points[0, -2], (0., 0., 1.))[:2]
+ points[1, :-1] = (
+ points[0, :-1]
+ + numpy.cross(points[0, 1:] - points[0, :-1], (0.0, 0.0, 1.0))[
+ :, :2
+ ]
+ )
+ points[1, -1] = (
+ points[0, -1]
+ + numpy.cross(points[0, -1] - points[0, -2], (0.0, 0.0, 1.0))[
+ :2
+ ]
+ )
points.shape = 2, nbpoints, 2 # Use same shape for both orders
coords, indices = _quadrilateral_grid_as_triangles(points)
- elif gridOrder == 'row': # row-major order
+ elif gridOrder == "row": # row-major order
if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
+ points = numpy.empty(
+ (numpy.prod(shape), 2), dtype=numpy.float64
+ )
points[:nbpoints, 0] = xFiltered
points[:nbpoints, 1] = yFiltered
# Index of last element of last fully filled row
index = (nbpoints // shape[1]) * shape[1]
- points[nbpoints:, 0] = xFiltered[index - (numpy.prod(shape) - nbpoints):index]
+ points[nbpoints:, 0] = xFiltered[
+ index - (numpy.prod(shape) - nbpoints) : index
+ ]
points[nbpoints:, 1] = yFiltered[-1]
else:
points = numpy.transpose((xFiltered, yFiltered))
points.shape = shape[0], shape[1], 2
- else: # column-major order
+ else: # column-major order
if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
+ points = numpy.empty(
+ (numpy.prod(shape), 2), dtype=numpy.float64
+ )
points[:nbpoints, 0] = yFiltered
points[:nbpoints, 1] = xFiltered
# Index of last element of last fully filled column
index = (nbpoints // shape[0]) * shape[0]
- points[nbpoints:, 0] = yFiltered[index - (numpy.prod(shape) - nbpoints):index]
+ points[nbpoints:, 0] = yFiltered[
+ index - (numpy.prod(shape) - nbpoints) : index
+ ]
points[nbpoints:, 1] = xFiltered[-1]
else:
points = numpy.transpose((yFiltered, xFiltered))
@@ -704,25 +777,24 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
coords, indices = _quadrilateral_grid_as_triangles(points)
# Remove unused extra triangles
- coords = coords[:4*nbpoints]
- indices = indices[:2*nbpoints]
+ coords = coords[: 4 * nbpoints]
+ indices = indices[: 2 * nbpoints]
- if gridOrder == 'row':
+ if gridOrder == "row":
x, y = coords[:, 0], coords[:, 1]
else: # column-major order
y, x = coords[:, 0], coords[:, 1]
rgbacolors = rgbacolors[mask] # Filter-out not finite points
gridcolors = numpy.empty(
- (4 * nbpoints, rgbacolors.shape[-1]), dtype=rgbacolors.dtype)
+ (4 * nbpoints, rgbacolors.shape[-1]), dtype=rgbacolors.dtype
+ )
for first in range(4):
gridcolors[first::4] = rgbacolors[:nbpoints]
- return backend.addTriangles(x,
- y,
- indices,
- color=gridcolors,
- alpha=self.getAlpha())
+ return backend.addTriangles(
+ x, y, indices, color=gridcolors, alpha=self.getAlpha()
+ )
else:
_logger.error("Unhandled visualization %s", visualization)
@@ -751,11 +823,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
if gridInfo is None:
return None
- if gridInfo.order == 'row':
+ if gridInfo.order == "row":
index = row * gridInfo.shape[1] + column
else:
index = row + column * gridInfo.shape[0]
- if index >= len(self.getXData(copy=False)): # OK as long as not log scale
+ if index >= len(
+ self.getXData(copy=False)
+ ): # OK as long as not log scale
return None # Image can be larger than scatter
result = PickingResult(self, (index,))
@@ -772,9 +846,16 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
ox, oy = histoInfo.origin
xdata = self.getXData(copy=False)
ydata = self.getYData(copy=False)
- indices = numpy.nonzero(numpy.logical_and(
- numpy.logical_and(xdata >= ox + sx * col, xdata < ox + sx * (col + 1)),
- numpy.logical_and(ydata >= oy + sy * row, ydata < oy + sy * (row + 1))))[0]
+ indices = numpy.nonzero(
+ numpy.logical_and(
+ numpy.logical_and(
+ xdata >= ox + sx * col, xdata < ox + sx * (col + 1)
+ ),
+ numpy.logical_and(
+ ydata >= oy + sy * row, ydata < oy + sy * (row + 1)
+ ),
+ )
+ )[0]
result = None if len(indices) == 0 else PickingResult(self, indices)
return result
@@ -788,69 +869,43 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
self.__executor = _GreedyThreadPoolExecutor(max_workers=2)
return self.__executor
- def _getDelaunay(self):
- """Returns a :class:`Future` which result is the Delaunay object.
+ def _getTriangulationFuture(self):
+ """Returns a :class:`Future` which result is the Triangulation object.
:rtype: concurrent.futures.Future
"""
- if self.__delaunayFuture is None or self.__delaunayFuture.cancelled():
+ if self.__triangulationFuture is None or self.__triangulationFuture.cancelled():
# Need to init a new delaunay
x, y = self.getData(copy=False)[:2]
# Remove not finite points
mask = numpy.logical_and(numpy.isfinite(x), numpy.isfinite(y))
- self.__delaunayFuture = self.__getExecutor().submit_greedy(
- 'delaunay', delaunay, x[mask], y[mask])
+ self.__triangulationFuture = self.__getExecutor().submit_greedy(
+ "Triangulation", Triangulation, x[mask], y[mask]
+ )
- return self.__delaunayFuture
+ return self.__triangulationFuture
@staticmethod
- def __initInterpolator(delaunayFuture, values):
+ def __initInterpolator(triangulationFuture, values):
"""Returns an interpolator for the given data points
- :param concurrent.futures.Future delaunayFuture:
- Future object which result is a Delaunay object
+ :param concurrent.futures.Future triangulationFuture:
+ Future object which result is a Triangulation object
:param numpy.ndarray values: The data value of valid points.
:rtype: Union[callable,None]
"""
- # Wait for Delaunay to complete
+ # Wait for Triangulation to complete
try:
- triangulation = delaunayFuture.result()
+ triangulation = triangulationFuture.result()
+ except (RuntimeError, ValueError):
+ return None # triangulation failed
except CancelledError:
- triangulation = None
-
- if triangulation is None:
- interpolator = None # Error case
- else:
- # Lazy-loading of interpolator
- try:
- from scipy.interpolate import LinearNDInterpolator
- except ImportError:
- LinearNDInterpolator = None
-
- if LinearNDInterpolator is not None:
- interpolator = LinearNDInterpolator(triangulation, values)
-
- # First call takes a while, do it here
- interpolator([(0., 0.)])
-
- else:
- # Fallback using matplotlib interpolator
- import matplotlib.tri
-
- x, y = triangulation.points.T
- tri = matplotlib.tri.Triangulation(
- x, y, triangles=triangulation.simplices)
- mplInterpolator = matplotlib.tri.LinearTriInterpolator(
- tri, values)
-
- # Wrap interpolator to have same API as scipy's one
- def interpolator(points):
- return mplInterpolator(*points.T)
+ return None
- return interpolator
+ return LinearTriInterpolator(triangulation, values)
- def _getInterpolator(self):
+ def _getInterpolatorFuture(self):
"""Returns a :class:`Future` which result is the interpolator.
The interpolator is a callable taking an array Nx2 of points
@@ -860,8 +915,7 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
:rtype: concurrent.futures.Future
"""
- if (self.__interpolatorFuture is None or
- self.__interpolatorFuture.cancelled()):
+ if self.__interpolatorFuture is None or self.__interpolatorFuture.cancelled():
# Need to init a new interpolator
x, y, values = self.getData(copy=False)[:3]
# Remove not finite points
@@ -869,8 +923,11 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
x, y, values = x[mask], y[mask], values[mask]
self.__interpolatorFuture = self.__getExecutor().submit_greedy(
- 'interpolator',
- self.__initInterpolator, self._getDelaunay(), values)
+ "interpolator",
+ self.__initInterpolator,
+ self._getTriangulationFuture(),
+ values,
+ )
return self.__interpolatorFuture
def _logFilterData(self, xPositive, yPositive):
@@ -932,11 +989,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
assert len(data) == 5
return data
- return (self.getXData(copy),
- self.getYData(copy),
- self.getValueData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
+ return (
+ self.getXData(copy),
+ self.getYData(copy),
+ self.getValueData(copy),
+ self.getXErrorData(copy),
+ self.getYErrorData(copy),
+ )
# reimplemented from PointsBase to handle `value`
def setData(self, x, y, value, xerror=None, yerror=None, alpha=None, copy=True):
@@ -950,12 +1009,12 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
: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.
+ of same length as the data: row 0 for lower errors,
+ row 1 for upper errors.
:param yerror: Values with the uncertainties on the y values
:type yerror: A float, or a numpy.ndarray of float32. See xerror.
:param alpha: Values with the transparency (between 0 and 1)
- :type alpha: A float, or a numpy.ndarray of float32
+ :type alpha: A float, or a numpy.ndarray of float32
:param bool copy: True make a copy of the data (default),
False to use provided arrays.
"""
@@ -965,14 +1024,13 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
# Convert complex data
if numpy.iscomplexobj(value):
- _logger.warning(
- 'Converting value data to absolute value to plot it.')
+ _logger.warning("Converting value data to absolute value to plot it.")
value = numpy.absolute(value)
# Reset triangulation and interpolator
- if self.__delaunayFuture is not None:
- self.__delaunayFuture.cancel()
- self.__delaunayFuture = None
+ if self.__triangulationFuture is not None:
+ self.__triangulationFuture.cancel()
+ self.__triangulationFuture = None
if self.__interpolatorFuture is not None:
self.__interpolatorFuture.cancel()
self.__interpolatorFuture = None
@@ -988,10 +1046,10 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
alpha = numpy.array(alpha, copy=copy)
assert alpha.ndim == 1
assert len(x) == len(alpha)
- if alpha.dtype.kind != 'f':
+ if alpha.dtype.kind != "f":
alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
+ if numpy.any(numpy.logical_or(alpha < 0.0, alpha > 1.0)):
+ alpha = numpy.clip(alpha, 0.0, 1.0)
self.__alpha = alpha
# set x, y, xerror, yerror
diff --git a/src/silx/gui/plot/items/shape.py b/src/silx/gui/plot/items/shape.py
index 00ac5f5..c911924 100644
--- a/src/silx/gui/plot/items/shape.py
+++ b/src/silx/gui/plot/items/shape.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,18 +33,65 @@ import logging
import numpy
-from ... import colors
from .core import (
- Item, DataItem,
- ColorMixIn, FillMixIn, ItemChangedType, LineMixIn, YAxisMixIn)
+ Item,
+ DataItem,
+ AlphaMixIn,
+ ColorMixIn,
+ FillMixIn,
+ ItemChangedType,
+ LineMixIn,
+ LineGapColorMixIn,
+ YAxisMixIn,
+)
+from ....utils.deprecation import deprecated
_logger = logging.getLogger(__name__)
+class _OverlayItem(Item):
+ """Item with settable overlay"""
+
+ def __init__(self):
+ self.__overlay = False
+ Item.__init__(self)
+
+ def isOverlay(self) -> bool:
+ """Return true if shape is drawn as an overlay"""
+ return self.__overlay
+
+ def setOverlay(self, overlay: bool):
+ """Set the overlay state of the shape
+
+ :param overlay: True to make it an overlay
+ """
+ overlay = bool(overlay)
+ if overlay != self.__overlay:
+ self.__overlay = overlay
+ self._updated(ItemChangedType.OVERLAY)
+
+
+class _TwoColorsLineMixIn(LineMixIn, LineGapColorMixIn):
+ """Mix-in class for items with a background color for dashes"""
+
+ def __init__(self):
+ LineMixIn.__init__(self)
+ LineGapColorMixIn.__init__(self)
+
+ @deprecated(replacement="getLineGapColor", since_version="2.0.0")
+ def getLineBgColor(self):
+ return self.getLineGapColor()
+
+ @deprecated(replacement="setLineGapColor", since_version="2.0.0")
+ def setLineBgColor(self, color, copy: bool = True):
+ self.setLineGapColor(color)
+ self._updated(ItemChangedType.LINE_BG_COLOR)
+
+
# TODO probably make one class for each kind of shape
# TODO check fill:polygon/polyline + fill = duplicated
-class Shape(Item, ColorMixIn, FillMixIn, LineMixIn):
+class Shape(_OverlayItem, ColorMixIn, FillMixIn, _TwoColorsLineMixIn):
"""Description of a shape item
:param str type_: The type of shape in:
@@ -53,48 +99,30 @@ class Shape(Item, ColorMixIn, FillMixIn, LineMixIn):
"""
def __init__(self, type_):
- Item.__init__(self)
+ _OverlayItem.__init__(self)
ColorMixIn.__init__(self)
FillMixIn.__init__(self)
- LineMixIn.__init__(self)
- self._overlay = False
- assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polylines')
+ _TwoColorsLineMixIn.__init__(self)
+ assert type_ in ("hline", "polygon", "rectangle", "vline", "polylines")
self._type = type_
self._points = ()
- self._lineBgColor = None
-
self._handle = None
def _addBackendRenderer(self, backend):
"""Update backend renderer"""
points = self.getPoints(copy=False)
x, y = points.T[0], points.T[1]
- return backend.addShape(x,
- y,
- shape=self.getType(),
- color=self.getColor(),
- fill=self.isFill(),
- overlay=self.isOverlay(),
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- linebgcolor=self.getLineBgColor())
-
- def isOverlay(self):
- """Return true if shape is drawn as an overlay
-
- :rtype: bool
- """
- return self._overlay
-
- def setOverlay(self, overlay):
- """Set the overlay state of the shape
-
- :param bool overlay: True to make it an overlay
- """
- overlay = bool(overlay)
- if overlay != self._overlay:
- self._overlay = overlay
- self._updated(ItemChangedType.OVERLAY)
+ return backend.addShape(
+ x,
+ y,
+ shape=self.getType(),
+ color=self.getColor(),
+ fill=self.isFill(),
+ overlay=self.isOverlay(),
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ gapcolor=self.getLineGapColor(),
+ )
def getType(self):
"""Returns the type of shape to draw.
@@ -126,34 +154,6 @@ class Shape(Item, ColorMixIn, FillMixIn, LineMixIn):
self._points = numpy.array(points, copy=copy)
self._updated(ItemChangedType.DATA)
- def getLineBgColor(self):
- """Returns the RGBA color of the item
- :rtype: 4-tuple of float in [0, 1] or array of colors
- """
- return self._lineBgColor
-
- def setLineBgColor(self, color, copy=True):
- """Set item color
- :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 bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if color is not None:
- if isinstance(color, str):
- color = colors.rgba(color)
- else:
- color = numpy.array(color, copy=copy)
- # TODO more checks + improve color array support
- if color.ndim == 1: # Single RGBA color
- color = colors.rgba(color)
- else: # Array of colors
- assert color.ndim == 2
-
- self._lineBgColor = color
- self._updated(ItemChangedType.LINE_BG_COLOR)
-
class BoundingRect(DataItem, YAxisMixIn):
"""An invisible shape which enforce the plot view to display the defined
@@ -214,11 +214,11 @@ class _BaseExtent(DataItem):
:param str axis: Either 'x' or 'y'.
"""
- def __init__(self, axis='x'):
- assert axis in ('x', 'y')
+ def __init__(self, axis="x"):
+ assert axis in ("x", "y")
DataItem.__init__(self)
self.__axis = axis
- self.__range = 1., 100.
+ self.__range = 1.0, 100.0
def setRange(self, min_, max_):
"""Set the range of the extent of this item in data coordinates.
@@ -250,17 +250,17 @@ class _BaseExtent(DataItem):
plot = self.getPlot()
if plot is not None:
- axis = plot.getXAxis() if self.__axis == 'x' else plot.getYAxis()
+ axis = plot.getXAxis() if self.__axis == "x" else plot.getYAxis()
if axis._isLogarithmic():
if max_ <= 0:
return None
if min_ <= 0:
min_ = max_
- if self.__axis == 'x':
- return min_, max_, float('nan'), float('nan')
+ if self.__axis == "x":
+ return min_, max_, float("nan"), float("nan")
else:
- return float('nan'), float('nan'), min_, max_
+ return float("nan"), float("nan"), min_, max_
class XAxisExtent(_BaseExtent):
@@ -270,8 +270,9 @@ class XAxisExtent(_BaseExtent):
item with a horizontal extent regarding plot data bounds, i.e.,
:meth:`PlotWidget.resetZoom` will take this horizontal extent into account.
"""
+
def __init__(self):
- _BaseExtent.__init__(self, axis='x')
+ _BaseExtent.__init__(self, axis="x")
class YAxisExtent(_BaseExtent, YAxisMixIn):
@@ -283,5 +284,110 @@ class YAxisExtent(_BaseExtent, YAxisMixIn):
"""
def __init__(self):
- _BaseExtent.__init__(self, axis='y')
+ _BaseExtent.__init__(self, axis="y")
YAxisMixIn.__init__(self)
+
+
+class Line(_OverlayItem, AlphaMixIn, ColorMixIn, _TwoColorsLineMixIn):
+ """Description of a infinite line item as y = slope * x + interecpt
+
+ Warning: If slope is not finite, then the line is x = intercept.
+ """
+
+ def __init__(self, slope: float = 0, intercept: float = 0):
+ assert numpy.isfinite(intercept)
+
+ _OverlayItem.__init__(self)
+ AlphaMixIn.__init__(self)
+ ColorMixIn.__init__(self)
+ _TwoColorsLineMixIn.__init__(self)
+ self.__slope = float(slope)
+ self.__intercept = float(intercept)
+ self.__coordinates = None
+ self._setVisibleBoundsTracking(True)
+
+ def __updatePoints(self):
+ if not self.isVisible():
+ return
+
+ plot = self.getPlot()
+ if plot is None or not plot.isVisible():
+ return
+
+ xmin, xmax = plot.getXAxis().getLimits()
+ ymin, ymax = plot.getYAxis().getLimits()
+
+ slope = self.getSlope()
+ intercept = self.getIntercept()
+
+ if not numpy.isfinite(slope):
+ if not xmin <= intercept <= xmax:
+ coordinates = None
+ else:
+ coordinates = (intercept, intercept), (ymin, ymax)
+ else:
+ ycoords = slope * xmin + intercept, slope * xmax + intercept
+
+ if min(ycoords) < ymax and max(ycoords) > ymin:
+ coordinates = (xmin, xmax), ycoords
+ else:
+ coordinates = None
+
+ if coordinates != self.__coordinates:
+ self.__coordinates = coordinates
+ self._updated()
+
+ def _visibleBoundsChanged(self, *args) -> None:
+ """Override method to benefit from bounds tracking"""
+ self.__updatePoints()
+ return super()._visibleBoundsChanged(*args)
+
+ def setSlope(self, slope: float):
+ slope = float(slope)
+ if slope != self.__slope:
+ self.__slope = slope
+ self.__updatePoints()
+ self._updated(ItemChangedType.DATA)
+
+ def getSlope(self) -> float:
+ return self.__slope
+
+ def setIntercept(self, intercept: float):
+ intercept = float(intercept)
+ assert numpy.isfinite(intercept)
+ if intercept != self.__intercept:
+ self.__intercept = intercept
+ self.__updatePoints()
+ self._updated(ItemChangedType.DATA)
+
+ def getIntercept(self) -> float:
+ return self.__intercept
+
+ def setSlopeInterceptFromPoints(self, point0, point1):
+ """Set slope and intercept from 2 (x, y) points"""
+ x0, y0 = point0
+ x1, y1 = point1
+ if x0 == x1: # Special case: vertical line
+ self.setSlope(float("inf"))
+ self.setIntercept(x0)
+ return
+
+ slope = (y1 - y0) / (x1 - x0)
+ self.setSlope(slope)
+ self.setIntercept(y0 - x0 * slope)
+
+ def _addBackendRenderer(self, backend):
+ """Update backend renderer"""
+ if self.__coordinates is None:
+ return None
+
+ return backend.addShape(
+ *self.__coordinates,
+ shape="polylines",
+ color=self.getColor(),
+ fill=False,
+ overlay=self.isOverlay(),
+ linestyle=self.getLineStyle(),
+ linewidth=self.getLineWidth(),
+ gapcolor=self.getLineGapColor(),
+ )
diff --git a/src/silx/gui/plot/matplotlib/Colormap.py b/src/silx/gui/plot/matplotlib/Colormap.py
deleted file mode 100644
index dc432b2..0000000
--- a/src/silx/gui/plot/matplotlib/Colormap.py
+++ /dev/null
@@ -1,249 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2017-2020 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.
-#
-# ############################################################################*/
-"""Matplotlib's new colormaps"""
-
-import numpy
-import logging
-from matplotlib.colors import ListedColormap
-import matplotlib.colors
-import matplotlib.cm
-import silx.resources
-from silx.utils.deprecation import deprecated, deprecated_warning
-
-
-deprecated_warning(type_='module',
- name=__file__,
- replacement='silx.gui.colors.Colormap',
- since_version='0.10.0')
-
-
-_logger = logging.getLogger(__name__)
-
-_AVAILABLE_AS_RESOURCE = ('magma', 'inferno', 'plasma', 'viridis')
-"""List available colormap name as resources"""
-
-_AVAILABLE_AS_BUILTINS = ('gray', 'reversed gray',
- 'temperature', 'red', 'green', 'blue')
-"""List of colormaps available through built-in declarations"""
-
-_CMAPS = {}
-"""Cache colormaps"""
-
-
-@property
-@deprecated(since_version='0.10.0')
-def magma():
- return getColormap('magma')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def inferno():
- return getColormap('inferno')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def plasma():
- return getColormap('plasma')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def viridis():
- return getColormap('viridis')
-
-
-@deprecated(since_version='0.10.0')
-def getColormap(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 name in _AVAILABLE_AS_RESOURCE:
- filename = silx.resources.resource_filename("gui/colormaps/%s.npy" % name)
- data = numpy.load(filename)
- lut = ListedColormap(data, name=name)
- _CMAPS[name] = lut
- return lut
- else:
- # matplotlib built-in
- return matplotlib.cm.get_cmap(name)
-
-
-@deprecated(since_version='0.10.0')
-def getScalarMappable(colormap, data=None):
- """Returns matplotlib ScalarMappable corresponding to colormap
-
- :param :class:`.Colormap` 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.getName() is not None:
- cmap = getColormap(colormap.getName())
-
- else: # No name, use custom colors
- if colormap.getColormapLUT() is None:
- raise ValueError(
- 'addImage: colormap no name nor list of colors.')
- colors = colormap.getColormapLUT()
- 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)
-
- vmin, vmax = colormap.getColormapRange(data)
- normalization = colormap.getNormalization()
- if normalization == colormap.LOGARITHM:
- norm = matplotlib.colors.LogNorm(vmin, vmax)
- elif normalization == colormap.LINEAR:
- norm = matplotlib.colors.Normalize(vmin, vmax)
- else:
- raise RuntimeError("Unsupported normalization: %s" % normalization)
-
- return matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
-
-
-@deprecated(replacement='silx.colors.Colormap.applyToData',
- since_version='0.8.0')
-def applyColormapToData(data, colormap):
- """Apply a colormap to the data and returns the RGBA image
-
- This supports data of any dimensions (not only of dimension 2).
- The returned array will have one more dimension (with 4 entries)
- than the input data to store the RGBA channels
- corresponding to each bin in the array.
-
- :param numpy.ndarray data: The data to convert.
- :param :class:`.Colormap`: The colormap to apply
- """
- # 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 (colormap.getName() is None and
- colormap.getColormapLUT() is not None):
- colors = colormap.getColormapLUT()
- 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
- colormap.getNormalization() == 'linear' and
- not colormap.isAutoscale() and
- colormap.getVMin() == 0 and
- colormap.getVMax() == 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__)
-
- scalarMappable = getScalarMappable(colormap, data)
- rgbaImage = scalarMappable.to_rgba(data, bytes=True)
-
- return rgbaImage
-
-
-@deprecated(replacement='silx.colors.Colormap.getSupportedColormaps',
- since_version='0.10.0')
-def getSupportedColormaps():
- """Get the supported colormap names as a tuple of str.
- """
- colormaps = set(matplotlib.cm.datad.keys())
- colormaps.update(_AVAILABLE_AS_BUILTINS)
- colormaps.update(_AVAILABLE_AS_RESOURCE)
- return tuple(sorted(colormaps))
diff --git a/src/silx/gui/plot/setup.py b/src/silx/gui/plot/setup.py
deleted file mode 100644
index e0b2c91..0000000
--- a/src/silx/gui/plot/setup.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "29/06/2017"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('plot', parent_package, top_path)
- config.add_subpackage('_utils')
- config.add_subpackage('utils')
- config.add_subpackage('matplotlib')
- config.add_subpackage('stats')
- config.add_subpackage('backends')
- config.add_subpackage('backends.glutils')
- config.add_subpackage('items')
- config.add_subpackage('test')
- config.add_subpackage('tools')
- config.add_subpackage('tools.profile')
- config.add_subpackage('tools.test')
- config.add_subpackage('actions')
-
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
-
- setup(configuration=configuration)
diff --git a/src/silx/gui/plot/stats/__init__.py b/src/silx/gui/plot/stats/__init__.py
index 04a5327..dfaa865 100644
--- a/src/silx/gui/plot/stats/__init__.py
+++ b/src/silx/gui/plot/stats/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/stats/stats.py b/src/silx/gui/plot/stats/stats.py
index a81f7bb..d575e3f 100644
--- a/src/silx/gui/plot/stats/stats.py
+++ b/src/silx/gui/plot/stats/stats.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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 +31,6 @@ __license__ = "MIT"
__date__ = "06/06/2018"
-from collections import OrderedDict
from functools import lru_cache
import logging
@@ -45,12 +43,11 @@ from ..items.roi import RegionOfInterest
from ....math.combo import min_max
from silx.utils.proxy import docstring
-from ....utils.deprecation import deprecated
logger = logging.getLogger(__name__)
-class Stats(OrderedDict):
+class Stats(dict):
"""Class to define a set of statistic relative to a dataset
(image, curve...).
@@ -61,15 +58,17 @@ class Stats(OrderedDict):
:param List statslist: List of the :class:`Stat` object to be computed.
"""
+
def __init__(self, statslist=None):
- OrderedDict.__init__(self)
+ super().__init__()
_statslist = statslist if not None else []
if statslist is not None:
for stat in _statslist:
self.add(stat)
- def calculate(self, item, plot, onlimits, roi, data_changed=False,
- roi_changed=False):
+ def calculate(
+ self, item, plot, onlimits, roi, data_changed=False, roi_changed=False
+ ):
"""
Call all :class:`Stat` object registered and return the result of the
computation.
@@ -88,27 +87,26 @@ class Stats(OrderedDict):
of the calculation as value
"""
res = {}
- context = self._getContext(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ context = self._getContext(item=item, plot=plot, onlimits=onlimits, roi=roi)
for statName, stat in list(self.items()):
if context.kind not in stat.compatibleKinds:
- logger.debug('kind %s not managed by statistic %s'
- % (context.kind, stat.name))
+ logger.debug(
+ "kind %s not managed by statistic %s" % (context.kind, stat.name)
+ )
res[statName] = None
else:
if roi_changed is True:
context.clear_mask()
if data_changed is True or roi_changed is True:
# if data changed or mask changed
- context.clipData(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ context.clipData(item=item, plot=plot, onlimits=onlimits, roi=roi)
# init roi and data
res[statName] = stat.calculate(context)
return res
def __setitem__(self, key, value):
assert isinstance(value, StatBase)
- OrderedDict.__setitem__(self, key, value)
+ super().__setitem__(key, value)
def add(self, stat):
"""Add a :class:`Stat` to the set
@@ -135,14 +133,11 @@ class Stats(OrderedDict):
from ...plot3d import items as items3d # Lazy import
if isinstance(item, (items3d.Scatter2D, items3d.Scatter3D)):
- context = _plot3DScatterContext(item, plot, onlimits,
- roi=roi)
- elif isinstance(item,
- (items3d.ImageData, items3d.ScalarField3D)):
- context = _plot3DArrayContext(item, plot, onlimits,
- roi=roi)
+ context = _plot3DScatterContext(item, plot, onlimits, roi=roi)
+ elif isinstance(item, (items3d.ImageData, items3d.ScalarField3D)):
+ context = _plot3DArrayContext(item, plot, onlimits, roi=roi)
if context is None:
- raise ValueError('Item type not managed')
+ raise ValueError("Item type not managed")
return context
@@ -165,6 +160,7 @@ class _StatsContext(object):
For now, incompatible with `onlimits` calculation
:type roi: Union[None,:class:`_RegionOfInterestBase`]
"""
+
def __init__(self, item, kind, plot, onlimits, roi):
assert item
assert plot
@@ -235,13 +231,6 @@ class _StatsContext(object):
"""
raise NotImplementedError("Base class")
- @deprecated(reason="context are now stored and keep during stats life."
- "So this function will be called only once",
- replacement="clipData", since_version="0.13.0")
- def createContext(self, item, plot, onlimits, roi):
- return self.clipData(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
-
def isStructuredData(self):
"""Returns True if data as an array-like structure.
@@ -272,15 +261,18 @@ class _StatsContext(object):
def _checkContextInputs(self, item, plot, onlimits, roi):
if roi is not None and onlimits is True:
- raise ValueError('Stats context is unable to manage both a ROI'
- 'and the `onlimits` option')
+ raise ValueError(
+ "Stats context is unable to manage both a ROI"
+ "and the `onlimits` option"
+ )
class _ScatterCurveHistoMixInContext(_StatsContext):
def __init__(self, kind, item, plot, onlimits, roi):
self.clear_mask()
- _StatsContext.__init__(self, item=item, kind=kind,
- plot=plot, onlimits=onlimits, roi=roi)
+ _StatsContext.__init__(
+ self, item=item, kind=kind, plot=plot, onlimits=onlimits, roi=roi
+ )
def _set_mask_validity(self, onlimits, from_, to_):
self._onlimits = onlimits
@@ -293,8 +285,7 @@ class _ScatterCurveHistoMixInContext(_StatsContext):
self._to_ = None
def is_mask_valid(self, onlimits, from_, to_):
- return (onlimits == self.onlimits and from_ == self._from_ and
- to_ == self._to_)
+ return onlimits == self.onlimits and from_ == self._from_ and to_ == self._to_
class _CurveContext(_ScatterCurveHistoMixInContext):
@@ -309,15 +300,15 @@ class _CurveContext(_ScatterCurveHistoMixInContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='curve', item=item,
- plot=plot, onlimits=onlimits,
- roi=roi)
+ _ScatterCurveHistoMixInContext.__init__(
+ self, kind="curve", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
self.roi = roi
self.onlimits = onlimits
xData, yData = item.getData(copy=True)[0:2]
@@ -354,10 +345,11 @@ class _CurveContext(_ScatterCurveHistoMixInContext):
self.axes = (xData,)
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
+ raise TypeError("curve `context` can ony manage 1D roi")
class _HistogramContext(_ScatterCurveHistoMixInContext):
@@ -372,15 +364,15 @@ class _HistogramContext(_ScatterCurveHistoMixInContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='histogram',
- item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _ScatterCurveHistoMixInContext.__init__(
+ self, kind="histogram", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
yData, edges = item.getData(copy=True)[0:2]
xData = item._revertComputeEdges(x=edges, histogramType=item.getAlignment())
@@ -393,13 +385,16 @@ class _HistogramContext(_ScatterCurveHistoMixInContext):
mask = mask == 0
self._set_mask_validity(onlimits=onlimits, from_=minX, to_=maxX)
elif roi:
- if self.is_mask_valid(onlimits=onlimits, from_=roi._fromdata, to_=roi._todata):
+ if self.is_mask_valid(
+ onlimits=onlimits, from_=roi._fromdata, to_=roi._todata
+ ):
mask = self.mask
else:
mask = (roi._fromdata <= xData) & (xData <= roi._todata)
mask = mask == 0
- self._set_mask_validity(onlimits=onlimits, from_=roi._fromdata,
- to_=roi._todata)
+ self._set_mask_validity(
+ onlimits=onlimits, from_=roi._fromdata, to_=roi._todata
+ )
else:
mask = numpy.zeros_like(yData)
mask = mask.astype(numpy.uint32)
@@ -415,11 +410,12 @@ class _HistogramContext(_ScatterCurveHistoMixInContext):
self.axes = (self.xData,)
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
+ raise TypeError("curve `context` can ony manage 1D roi")
class _ScatterContext(_ScatterCurveHistoMixInContext):
@@ -435,15 +431,15 @@ class _ScatterContext(_ScatterCurveHistoMixInContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='scatter',
- item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _ScatterCurveHistoMixInContext.__init__(
+ self, kind="scatter", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_ScatterCurveHistoMixInContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
valueData = item.getValueData(copy=True)
xData = item.getXData(copy=True)
yData = item.getYData(copy=True)
@@ -462,8 +458,9 @@ class _ScatterContext(_ScatterCurveHistoMixInContext):
yData = yData[(minY <= yData) & (yData <= maxY)]
if roi:
- if self.is_mask_valid(onlimits=onlimits, from_=roi.getFrom(),
- to_=roi.getTo()):
+ if self.is_mask_valid(
+ onlimits=onlimits, from_=roi.getFrom(), to_=roi.getTo()
+ ):
mask = self.mask
else:
mask = (xData < roi.getFrom()) | (xData > roi.getTo())
@@ -481,11 +478,12 @@ class _ScatterContext(_ScatterCurveHistoMixInContext):
self.min, self.max = None, None
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
+ raise TypeError("curve `context` can ony manage 1D roi")
class _ImageContext(_StatsContext):
@@ -512,13 +510,14 @@ class _ImageContext(_StatsContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
self.clear_mask()
- _StatsContext.__init__(self, kind='image', item=item,
- plot=plot, onlimits=onlimits, roi=roi)
+ _StatsContext.__init__(
+ self, kind="image", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
- def _set_mask_validity(self, xmin: float, xmax: float, ymin: float, ymax
- : float):
+ def _set_mask_validity(self, xmin: float, xmax: float, ymin: float, ymax: float):
self._mask_x_min = xmin
self._mask_x_max = xmax
self._mask_y_min = ymin
@@ -531,13 +530,16 @@ class _ImageContext(_StatsContext):
self._mask_y_max = None
def is_mask_valid(self, xmin, xmax, ymin, ymax):
- return (xmin == self._mask_x_min and xmax == self._mask_x_max and
- ymin == self._mask_y_min and ymax == self._mask_y_max)
+ return (
+ xmin == self._mask_x_min
+ and xmax == self._mask_x_max
+ and ymin == self._mask_y_min
+ and ymax == self._mask_y_max
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
self.origin = item.getOrigin()
self.scale = item.getScale()
@@ -561,8 +563,9 @@ class _ImageContext(_StatsContext):
if XMaxBound <= XMinBound or YMaxBound <= YMinBound:
self.data = None
else:
- self.data = self.data[YMinBound:YMaxBound + 1,
- XMinBound:XMaxBound + 1]
+ self.data = self.data[
+ YMinBound : YMaxBound + 1, XMinBound : XMaxBound + 1
+ ]
mask = numpy.zeros_like(self.data)
elif roi:
minX, maxX = 0, self.data.shape[1]
@@ -573,8 +576,9 @@ class _ImageContext(_StatsContext):
XMaxBound = min(maxX, self.data.shape[1])
YMaxBound = min(maxY, self.data.shape[0])
- if self.is_mask_valid(xmin=XMinBound, xmax=XMaxBound,
- ymin=YMinBound, ymax=YMaxBound):
+ if self.is_mask_valid(
+ xmin=XMinBound, xmax=XMaxBound, ymin=YMinBound, ymax=YMaxBound
+ ):
mask = self.mask
else:
for x in range(XMinBound, XMaxBound):
@@ -582,8 +586,9 @@ class _ImageContext(_StatsContext):
_x = (x * self.scale[0]) + self.origin[0]
_y = (y * self.scale[1]) + self.origin[1]
mask[y, x] = not roi.contains((_x, _y))
- self._set_mask_validity(xmin=XMinBound, xmax=XMaxBound,
- ymin=YMinBound, ymax=YMaxBound)
+ self._set_mask_validity(
+ xmin=XMinBound, xmax=XMaxBound, ymin=YMinBound, ymax=YMaxBound
+ )
self.values = numpy.ma.array(self.data, mask=mask)
if self.values.compressed().size > 0:
self.min, self.max = min_max(self.values.compressed())
@@ -591,15 +596,18 @@ class _ImageContext(_StatsContext):
self.min, self.max = None, None
if self.values is not None:
- self.axes = (self.origin[1] + self.scale[1] * numpy.arange(self.data.shape[0]),
- self.origin[0] + self.scale[0] * numpy.arange(self.data.shape[1]))
+ self.axes = (
+ self.origin[1] + self.scale[1] * numpy.arange(self.data.shape[0]),
+ self.origin[0] + self.scale[0] * numpy.arange(self.data.shape[1]),
+ )
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
+ raise TypeError("curve `context` can ony manage 2D roi")
class _plot3DScatterContext(_StatsContext):
@@ -616,14 +624,15 @@ class _plot3DScatterContext(_StatsContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _StatsContext.__init__(self, kind='scatter', item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext.__init__(
+ self, kind="scatter", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
if onlimits:
raise RuntimeError("Unsupported plot %s" % str(plot))
values = item.getValueData(copy=False)
@@ -647,11 +656,12 @@ class _plot3DScatterContext(_StatsContext):
self.min, self.max = None, None
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
+ raise TypeError("curve `context` can ony manage 2D roi")
class _plot3DArrayContext(_StatsContext):
@@ -668,14 +678,15 @@ class _plot3DArrayContext(_StatsContext):
For now, incompatible with `onlinits` calculation
:type roi: Union[None, :class:`ROI`]
"""
+
def __init__(self, item, plot, onlimits, roi):
- _StatsContext.__init__(self, kind='image', item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext.__init__(
+ self, kind="image", item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
@docstring(_StatsContext)
def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
+ self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, roi=roi)
if onlimits:
raise RuntimeError("Unsupported plot %s" % str(plot))
@@ -697,14 +708,15 @@ class _plot3DArrayContext(_StatsContext):
self.min, self.max = None, None
def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
+ _StatsContext._checkContextInputs(
+ self, item=item, plot=plot, onlimits=onlimits, roi=roi
+ )
if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
+ raise TypeError("curve `context` can ony manage 2D roi")
-BASIC_COMPATIBLE_KINDS = 'curve', 'image', 'scatter', 'histogram'
+BASIC_COMPATIBLE_KINDS = "curve", "image", "scatter", "histogram"
class StatBase(object):
@@ -715,6 +727,7 @@ class StatBase(object):
:param List[str] compatibleKinds:
The kind of items (curve, scatter...) for which the statistic apply.
"""
+
def __init__(self, name, compatibleKinds=BASIC_COMPATIBLE_KINDS, description=None):
self.name = name
self.compatibleKinds = compatibleKinds
@@ -727,7 +740,7 @@ class StatBase(object):
:param _StatsContext context:
:return dict: key is stat name, statistic computed is the dict value
"""
- raise NotImplementedError('Base class')
+ raise NotImplementedError("Base class")
def getToolTip(self, kind):
"""
@@ -750,6 +763,7 @@ class Stat(StatBase):
:param tuple kinds: the compatible item kinds of the function (curve,
image...)
"""
+
def __init__(self, name, fct, kinds=BASIC_COMPATIBLE_KINDS):
StatBase.__init__(self, name, kinds)
self._fct = fct
@@ -760,16 +774,18 @@ class Stat(StatBase):
if context.kind in self.compatibleKinds:
return self._fct(context.values)
else:
- raise ValueError('Kind %s not managed by %s'
- '' % (context.kind, self.name))
+ raise ValueError(
+ "Kind %s not managed by %s" "" % (context.kind, self.name)
+ )
else:
return None
class StatMin(StatBase):
"""Compute the minimal value on data"""
+
def __init__(self):
- StatBase.__init__(self, name='min')
+ StatBase.__init__(self, name="min")
@docstring(StatBase)
def calculate(self, context):
@@ -778,8 +794,9 @@ class StatMin(StatBase):
class StatMax(StatBase):
"""Compute the maximal value on data"""
+
def __init__(self):
- StatBase.__init__(self, name='max')
+ StatBase.__init__(self, name="max")
@docstring(StatBase)
def calculate(self, context):
@@ -788,8 +805,9 @@ class StatMax(StatBase):
class StatDelta(StatBase):
"""Compute the delta between minimal and maximal on data"""
+
def __init__(self):
- StatBase.__init__(self, name='delta')
+ StatBase.__init__(self, name="delta")
@docstring(StatBase)
def calculate(self, context):
@@ -823,8 +841,9 @@ class _StatCoord(StatBase):
class StatCoordMin(_StatCoord):
"""Compute the coordinates of the first minimum value of the data"""
+
def __init__(self):
- _StatCoord.__init__(self, name='coords min')
+ _StatCoord.__init__(self, name="coords min")
@docstring(StatBase)
def calculate(self, context):
@@ -841,8 +860,9 @@ class StatCoordMin(_StatCoord):
class StatCoordMax(_StatCoord):
"""Compute the coordinates of the first maximum value of the data"""
+
def __init__(self):
- _StatCoord.__init__(self, name='coords max')
+ _StatCoord.__init__(self, name="coords max")
@docstring(StatBase)
def calculate(self, context):
@@ -861,8 +881,9 @@ class StatCoordMax(_StatCoord):
class StatCOM(StatBase):
"""Compute data center of mass"""
+
def __init__(self):
- StatBase.__init__(self, name='COM', description='Center of mass')
+ StatBase.__init__(self, name="COM", description="Center of mass")
@docstring(StatBase)
def calculate(self, context):
@@ -871,7 +892,7 @@ class StatCOM(StatBase):
values = numpy.ma.array(context.values, mask=context.mask, dtype=numpy.float64)
sum_ = numpy.sum(values)
- if sum_ == 0.:
+ if sum_ == 0.0 or numpy.ma.is_masked(sum_):
return (numpy.nan,) * len(context.axes)
if context.isStructuredData():
@@ -879,11 +900,11 @@ class StatCOM(StatBase):
for index, axis in enumerate(context.axes):
axes = tuple([i for i in range(len(context.axes)) if i != index])
centerofmass.append(
- numpy.sum(axis * numpy.sum(values, axis=axes)) / sum_)
+ numpy.sum(axis * numpy.sum(values, axis=axes)) / sum_
+ )
return tuple(reversed(centerofmass))
else:
- return tuple(
- numpy.sum(axis * values) / sum_ for axis in context.axes)
+ return tuple(numpy.sum(axis * values) / sum_ for axis in context.axes)
@docstring(StatBase)
def getToolTip(self, kind):
diff --git a/src/silx/gui/plot/stats/statshandler.py b/src/silx/gui/plot/stats/statshandler.py
index 17578d8..8e7e08b 100644
--- a/src/silx/gui/plot/stats/statshandler.py
+++ b/src/silx/gui/plot/stats/statshandler.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2022 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,6 +31,9 @@ __date__ = "05/06/2018"
import logging
+import numbers
+
+import numpy
from silx.gui import qt
from silx.gui.plot import stats as statsmdl
@@ -46,8 +48,8 @@ class _FloatItem(qt.QTableWidgetItem):
qt.QTableWidgetItem.__init__(self, type=type)
def __lt__(self, other):
- self_values = self.text().lstrip('(').rstrip(')').split(',')
- other_values = other.text().lstrip('(').rstrip(')').split(',')
+ self_values = self.text().lstrip("(").rstrip(")").split(",")
+ other_values = other.text().lstrip("(").rstrip(")").split(",")
for self_value, other_value in zip(self_values, other_values):
f_self_value = float(self_value)
f_other_value = float(other_value)
@@ -65,18 +67,22 @@ class StatFormatter(object):
which will be used to display the result of the
statistic computation.
"""
- DEFAULT_FORMATTER = '{0:.3f}'
+
+ DEFAULT_FORMATTER = "{0:.3f}"
def __init__(self, formatter=DEFAULT_FORMATTER, qItemClass=_FloatItem):
self.formatter = formatter
self.tabWidgetItemClass = qItemClass
def format(self, val):
- if self.formatter is None or val is None:
- return str(val)
- else:
+ if val is None or numpy.ma.is_masked(val):
+ return "--"
+
+ if self.formatter is not None and isinstance(val, numbers.Number):
return self.formatter.format(val)
+ return str(val)
+
class StatsHandler(object):
"""
@@ -116,9 +122,11 @@ class StatsHandler(object):
if isinstance(arg[0], statsmdl.StatBase):
stat = arg[0]
if len(arg) > 2:
- raise ValueError('To many argument with %s. At most one '
- 'argument can be associated with the '
- 'BaseStat (the `StatFormatter`')
+ raise ValueError(
+ "To many argument with %s. At most one "
+ "argument can be associated with the "
+ "BaseStat (the `StatFormatter`"
+ )
if len(arg) == 2:
assert arg[1] is None or isinstance(arg[1], (StatFormatter, str))
formatter = arg[1]
@@ -129,15 +137,20 @@ class StatsHandler(object):
arg = arg[0]
if type(arg[0]) is not str:
- raise ValueError('first element of the tuple should be a string'
- ' or a StatBase instance')
+ raise ValueError(
+ "first element of the tuple should be a string"
+ " or a StatBase instance"
+ )
if len(arg) == 1:
- raise ValueError('A function should be associated with the'
- 'stat name')
+ raise ValueError(
+ "A function should be associated with the" "stat name"
+ )
if len(arg) > 3:
- raise ValueError('Two much argument given for defining statistic.'
- 'Take at most three arguments (name, function, '
- 'kinds)')
+ raise ValueError(
+ "Two much argument given for defining statistic."
+ "Take at most three arguments (name, function, "
+ "kinds)"
+ )
if len(arg) == 2:
stat = statsmdl.Stat(name=arg[0], fct=arg[1])
else:
@@ -175,12 +188,13 @@ class StatsHandler(object):
if isinstance(val, (tuple, list)):
res = []
[res.append(self.formatters[name].format(_val)) for _val in val]
- return ', '.join(res)
+ return ", ".join(res)
else:
return self.formatters[name].format(val)
- def calculate(self, item, plot, onlimits, roi=None, data_changed=False,
- roi_changed=False):
+ def calculate(
+ self, item, plot, onlimits, roi=None, data_changed=False, roi_changed=False
+ ):
"""
compute all statistic registered and return the list of formatted
statistics result.
@@ -195,8 +209,14 @@ class StatsHandler(object):
:return: list of formatted statistics (as str)
:rtype: dict
"""
- res = self.stats.calculate(item, plot, onlimits, roi,
- data_changed=data_changed, roi_changed=roi_changed)
+ res = self.stats.calculate(
+ item,
+ plot,
+ onlimits,
+ roi,
+ data_changed=data_changed,
+ roi_changed=roi_changed,
+ )
for resName, resValue in list(res.items()):
res[resName] = self.format(resName, res[resName])
return res
diff --git a/src/silx/gui/plot/test/__init__.py b/src/silx/gui/plot/test/__init__.py
index 3ad225d..78821ec 100644
--- a/src/silx/gui/plot/test/__init__.py
+++ b/src/silx/gui/plot/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/_utils/setup.py b/src/silx/gui/plot/test/conftest.py
index 0271745..78475fb 100644
--- a/src/silx/gui/plot/_utils/setup.py
+++ b/src/silx/gui/plot/test/conftest.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,21 +21,23 @@
# THE SOFTWARE.
#
# ###########################################################################*/
+"""Test PlotWidget active item"""
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "21/03/2017"
-
-
-from numpy.distutils.misc_util import Configuration
-
+__date__ = "13/12/2023"
-def configuration(parent_package='', top_path=None):
- config = Configuration('_utils', parent_package, top_path)
- config.add_subpackage('test')
- return config
+import pytest
+from silx.gui.plot import PlotWidget
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
+@pytest.fixture
+def plotWidget(qWidgetFactory, request):
+ try:
+ backend = request.param
+ except AttributeError:
+ backend = "mpl" # Backend was not defined
+ if backend == "gl":
+ request.getfixturevalue("use_opengl") # Skip test if OpenGL test disabled
+ yield qWidgetFactory(PlotWidget, backend=backend)
diff --git a/src/silx/gui/plot/test/testAlphaSlider.py b/src/silx/gui/plot/test/testAlphaSlider.py
index ca57bf5..e9ccb45 100644
--- a/src/silx/gui/plot/test/testAlphaSlider.py
+++ b/src/silx/gui/plot/test/testAlphaSlider.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
@@ -30,7 +29,6 @@ __license__ = "MIT"
__date__ = "28/03/2017"
import numpy
-import unittest
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt
@@ -77,19 +75,16 @@ class TestActiveImageAlphaSlider(TestCaseQt):
def testGetImage(self):
self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]))
- self.assertEqual(self.plot.getActiveImage(),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getActiveImage(), self.aslider.getItem())
self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2")
self.plot.setActiveImage("2")
- self.assertEqual(self.plot.getImage("2"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getImage("2"), self.aslider.getItem())
def testGetAlpha(self):
self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
self.aslider.setValue(137)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 137. / 255)
+ self.assertAlmostEqual(self.aslider.getAlpha(), 137.0 / 255)
class TestNamedImageAlphaSlider(TestCaseQt):
@@ -131,19 +126,16 @@ class TestNamedImageAlphaSlider(TestCaseQt):
self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2")
self.aslider.setLegend("1")
- self.assertEqual(self.plot.getImage("1"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getImage("1"), self.aslider.getItem())
self.aslider.setLegend("2")
- self.assertEqual(self.plot.getImage("2"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getImage("2"), self.aslider.getItem())
def testGetAlpha(self):
self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
self.aslider.setLegend("1")
self.aslider.setValue(128)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 128. / 255)
+ self.assertAlmostEqual(self.aslider.getAlpha(), 128.0 / 255)
class TestNamedScatterAlphaSlider(TestCaseQt):
@@ -176,29 +168,22 @@ class TestNamedScatterAlphaSlider(TestCaseQt):
# no Scatter set initially, slider must be deactivate
self.assertFalse(self.aslider.isEnabled())
- self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7],
- legend="1")
+ self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7], legend="1")
self.aslider.setLegend("1")
# now we have an image set
self.assertTrue(self.aslider.isEnabled())
def testGetScatter(self):
- self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7],
- legend="1")
- self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70],
- legend="2")
+ self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7], legend="1")
+ self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70], legend="2")
self.aslider.setLegend("1")
- self.assertEqual(self.plot.getScatter("1"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getScatter("1"), self.aslider.getItem())
self.aslider.setLegend("2")
- self.assertEqual(self.plot.getScatter("2"),
- self.aslider.getItem())
+ self.assertEqual(self.plot.getScatter("2"), self.aslider.getItem())
def testGetAlpha(self):
- self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70],
- legend="1")
+ self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70], legend="1")
self.aslider.setLegend("1")
self.aslider.setValue(128)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 128. / 255)
+ self.assertAlmostEqual(self.aslider.getAlpha(), 128.0 / 255)
diff --git a/src/silx/gui/plot/test/testAxis.py b/src/silx/gui/plot/test/testAxis.py
new file mode 100644
index 0000000..dcf2f06
--- /dev/null
+++ b/src/silx/gui/plot/test/testAxis.py
@@ -0,0 +1,147 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Tests of PlotWidget Axis items"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "15/06/2023"
+
+
+from silx.gui.plot import PlotWidget
+
+
+def testAxisIsVisible(qapp, qWidgetFactory):
+ """Test Axis.isVisible method"""
+ plotWidget = qWidgetFactory(PlotWidget)
+
+ assert plotWidget.getXAxis().isVisible()
+ assert plotWidget.getYAxis().isVisible()
+ assert not plotWidget.getYAxis("right").isVisible()
+
+ # Add curve on right axis
+ plotWidget.addCurve((0, 1, 2), (1, 2, 3), yaxis="right")
+ qapp.processEvents()
+
+ assert plotWidget.getYAxis("right").isVisible()
+
+ # hide curve on right axis
+ curve = plotWidget.getItems()[0]
+ curve.setVisible(False)
+ qapp.processEvents()
+
+ assert not plotWidget.getYAxis("right").isVisible()
+
+ # show curve on right axis
+ curve.setVisible(True)
+ qapp.processEvents()
+
+ assert plotWidget.getYAxis("right").isVisible()
+
+ # Move curve to left axis
+ curve.setYAxis("left")
+ qapp.processEvents()
+
+ assert not plotWidget.getYAxis("right").isVisible()
+
+
+def testAxisSetScaleLogNoData(qapp, qWidgetFactory):
+ """Test Axis.setScale('log') method with an empty plot
+
+ Limits are reset only when negative
+ """
+ plotWidget = qWidgetFactory(PlotWidget)
+ xaxis = plotWidget.getXAxis()
+ yaxis = plotWidget.getYAxis()
+ y2axis = plotWidget.getYAxis("right")
+
+ xaxis.setLimits(-1.0, 1.0)
+ yaxis.setLimits(2.0, 3.0)
+ y2axis.setLimits(-2.0, -1.0)
+
+ xaxis.setScale("log")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (1.0, 100.0)
+ assert yaxis.getLimits() == (2.0, 3.0)
+ assert y2axis.getLimits() == (-2.0, -1.0)
+
+ xaxis.setLimits(10.0, 20.0)
+
+ yaxis.setScale("log")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (10.0, 20.0)
+ assert yaxis.getLimits() == (2.0, 3.0) # Positive range is preserved
+ assert y2axis.getLimits() == (1.0, 100.0) # Negative min is reset
+
+
+def testAxisSetScaleLogWithData(qapp, qWidgetFactory):
+ """Test Axis.setScale('log') method with data
+
+ Limits are reset only when negative and takes the data range into account
+ """
+ plotWidget = qWidgetFactory(PlotWidget)
+ xaxis = plotWidget.getXAxis()
+ yaxis = plotWidget.getYAxis()
+ plotWidget.addCurve((-1, 1, 2, 3), (-1, 1, 2, 3))
+
+ xaxis.setLimits(-1.0, 0.5) # Limits contains no positive data
+ yaxis.setLimits(-1.0, 2.0) # Limits contains positive data
+
+ xaxis.setScale("log")
+ yaxis.setScale("log")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (1.0, 3.0) # Reset to positive data range
+ assert yaxis.getLimits() == (1.0, 2.0) # Keep max limit
+
+
+def testAxisSetScaleLinear(qapp, qWidgetFactory):
+ """Test Axis.setScale('linear') method: Limits are not changed"""
+ plotWidget = qWidgetFactory(PlotWidget)
+ xaxis = plotWidget.getXAxis()
+ yaxis = plotWidget.getYAxis()
+ y2axis = plotWidget.getYAxis("right")
+ xaxis.setScale("log")
+ yaxis.setScale("log")
+ plotWidget.resetZoom()
+ qapp.processEvents()
+
+ xaxis.setLimits(10.0, 1000.0)
+ yaxis.setLimits(20.0, 2000.0)
+ y2axis.setLimits(30.0, 3000.0)
+
+ xaxis.setScale("linear")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (10.0, 1000.0)
+ assert yaxis.getLimits() == (20.0, 2000.0)
+ assert y2axis.getLimits() == (30.0, 3000.0)
+
+ yaxis.setScale("linear")
+ qapp.processEvents()
+
+ assert xaxis.getLimits() == (10.0, 1000.0)
+ assert yaxis.getLimits() == (20.0, 2000.0)
+ assert y2axis.getLimits() == (30.0, 3000.0)
diff --git a/src/silx/gui/plot/test/testColorBar.py b/src/silx/gui/plot/test/testColorBar.py
index 3dc8ff1..7202bc2 100644
--- a/src/silx/gui/plot/test/testColorBar.py
+++ b/src/silx/gui/plot/test/testColorBar.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -28,7 +27,6 @@ __authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "24/04/2018"
-import unittest
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.plot.ColorBar import _ColorScale
from silx.gui.plot.ColorBar import ColorBarWidget
@@ -41,6 +39,7 @@ import numpy
class TestColorScale(TestCaseQt):
"""Test that interaction with the colorScale is correct"""
+
def setUp(self):
super(TestColorScale, self).setUp()
self.colorScaleWidget = _ColorScale(colormap=None, parent=None)
@@ -60,37 +59,32 @@ class TestColorScale(TestCaseQt):
self.assertIsNone(colormap)
def testRelativePositionLinear(self):
- self.colorMapLin1 = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=0.0,
- vmax=1.0)
+ self.colorMapLin1 = Colormap(
+ name="gray", normalization=Colormap.LINEAR, vmin=0.0, vmax=1.0
+ )
self.colorScaleWidget.setColormap(self.colorMapLin1)
self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.25) == 0.25)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.5) == 0.5)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(1.0) == 1.0)
-
- self.colorMapLin2 = Colormap(name='viridis',
- normalization=Colormap.LINEAR,
- vmin=-10,
- vmax=0)
+ self.colorScaleWidget.getValueFromRelativePosition(0.25) == 0.25
+ )
+ self.assertTrue(self.colorScaleWidget.getValueFromRelativePosition(0.5) == 0.5)
+ self.assertTrue(self.colorScaleWidget.getValueFromRelativePosition(1.0) == 1.0)
+
+ self.colorMapLin2 = Colormap(
+ name="viridis", normalization=Colormap.LINEAR, vmin=-10, vmax=0
+ )
self.colorScaleWidget.setColormap(self.colorMapLin2)
self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.25) == -7.5)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.5) == -5.0)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(1.0) == 0.0)
+ self.colorScaleWidget.getValueFromRelativePosition(0.25) == -7.5
+ )
+ self.assertTrue(self.colorScaleWidget.getValueFromRelativePosition(0.5) == -5.0)
+ self.assertTrue(self.colorScaleWidget.getValueFromRelativePosition(1.0) == 0.0)
def testRelativePositionLog(self):
- self.colorMapLog1 = Colormap(name='temperature',
- normalization=Colormap.LOGARITHM,
- vmin=1.0,
- vmax=100.0)
+ self.colorMapLog1 = Colormap(
+ name="temperature", normalization=Colormap.LOGARITHM, vmin=1.0, vmax=100.0
+ )
self.colorScaleWidget.setColormap(self.colorMapLog1)
@@ -131,14 +125,13 @@ class TestNoAutoscale(TestCaseQt):
super(TestNoAutoscale, self).tearDown()
def testLogNormNoAutoscale(self):
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=1.0,
- vmax=100.0)
+ colormapLog = Colormap(
+ name="gray", normalization=Colormap.LOGARITHM, vmin=1.0, vmax=100.0
+ )
data = numpy.linspace(10, 1e10, 9).reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormapLog, legend="toto")
+ self.plot.setActiveImage("toto")
# test Ticks
self.tickBar.setTicksNumber(10)
@@ -156,14 +149,13 @@ class TestNoAutoscale(TestCaseQt):
self.assertTrue(val == 1.0)
def testLinearNormNoAutoscale(self):
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=-4,
- vmax=5)
+ colormapLog = Colormap(
+ name="gray", normalization=Colormap.LINEAR, vmin=-4, vmax=5
+ )
data = numpy.linspace(1, 9, 9).reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormapLog, legend="toto")
+ self.plot.setActiveImage("toto")
# test Ticks
self.tickBar.setTicksNumber(10)
@@ -210,15 +202,14 @@ class TestColorBarWidget(TestCaseQt):
Note : colorbar is modified by the Plot directly not ColorBarWidget
"""
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
+ colormapLog = Colormap(
+ name="gray", normalization=Colormap.LOGARITHM, vmin=None, vmax=None
+ )
data = numpy.array([-5, -4, 0, 2, 3, 5, 10, 20, 30])
data = data.reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormapLog, legend="toto")
+ self.plot.setActiveImage("toto")
# default behavior when with log and negative values: should set vmin
# to 1 and vmax to 10
@@ -227,52 +218,43 @@ class TestColorBarWidget(TestCaseQt):
# if data is positive
data[data < 1] = data.max()
- self.plot.addImage(data=data,
- colormap=colormapLog,
- legend='toto',
- replace=True)
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormapLog, legend="toto", replace=True)
+ self.plot.setActiveImage("toto")
self.assertTrue(self.colorBar.getColorScaleBar().minVal == data.min())
self.assertTrue(self.colorBar.getColorScaleBar().maxVal == data.max())
def testPlotAssocation(self):
"""Make sure the ColorBarWidget is properly connected with the plot"""
- colormap = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None)
+ colormap = Colormap(
+ name="gray", normalization=Colormap.LINEAR, vmin=None, vmax=None
+ )
# make sure that default settings are the same (but a copy of the
self.colorBar.setPlot(self.plot)
- self.assertTrue(
- self.colorBar.getColormap() is self.plot.getDefaultColormap())
+ self.assertTrue(self.colorBar.getColormap() is self.plot.getDefaultColormap())
data = numpy.linspace(0, 10, 100).reshape(10, 10)
- self.plot.addImage(data=data, colormap=colormap, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=data, colormap=colormap, legend="toto")
+ self.plot.setActiveImage("toto")
# make sure the modification of the colormap has been done
- self.assertFalse(
- self.colorBar.getColormap() is self.plot.getDefaultColormap())
- self.assertTrue(
- self.colorBar.getColormap() is colormap)
+ self.assertFalse(self.colorBar.getColormap() is self.plot.getDefaultColormap())
+ self.assertTrue(self.colorBar.getColormap() is colormap)
# test that colorbar is updated when default plot colormap changes
self.plot.clear()
- plotColormap = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
+ plotColormap = Colormap(
+ name="gray", normalization=Colormap.LOGARITHM, vmin=None, vmax=None
+ )
self.plot.setDefaultColormap(plotColormap)
self.assertTrue(self.colorBar.getColormap() is plotColormap)
def testColormapWithoutRange(self):
"""Test with a colormap with vmin==vmax"""
- colormap = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=1.0,
- vmax=1.0)
+ colormap = Colormap(
+ name="gray", normalization=Colormap.LINEAR, vmin=1.0, vmax=1.0
+ )
self.colorBar.setColormap(colormap)
@@ -301,40 +283,35 @@ class TestColorBarUpdate(TestCaseQt):
super(TestColorBarUpdate, self).tearDown()
def testUpdateColorMap(self):
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=0,
- vmax=1)
+ colormap = Colormap(name="gray", normalization="linear", vmin=0, vmax=1)
# check inital state
- self.plot.addImage(data=self.data, colormap=colormap, legend='toto')
- self.plot.setActiveImage('toto')
+ self.plot.addImage(data=self.data, colormap=colormap, legend="toto")
+ self.plot.setActiveImage("toto")
self.assertTrue(self.colorBar.getColorScaleBar().minVal == 0)
self.assertTrue(self.colorBar.getColorScaleBar().maxVal == 1)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmin == 0)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmax == 1)
+ self.assertTrue(self.colorBar.getColorScaleBar().getTickBar()._vmin == 0)
+ self.assertTrue(self.colorBar.getColorScaleBar().getTickBar()._vmax == 1)
self.assertIsInstance(
self.colorBar.getColorScaleBar().getTickBar()._normalizer,
- LinearNormalization)
+ LinearNormalization,
+ )
# update colormap
colormap.setVMin(0.5)
self.assertTrue(self.colorBar.getColorScaleBar().minVal == 0.5)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmin == 0.5)
+ self.assertTrue(self.colorBar.getColorScaleBar().getTickBar()._vmin == 0.5)
colormap.setVMax(0.8)
self.assertTrue(self.colorBar.getColorScaleBar().maxVal == 0.8)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmax == 0.8)
+ self.assertTrue(self.colorBar.getColorScaleBar().getTickBar()._vmax == 0.8)
- colormap.setNormalization('log')
+ colormap.setNormalization("log")
self.assertIsInstance(
self.colorBar.getColorScaleBar().getTickBar()._normalizer,
- LogarithmicNormalization)
+ LogarithmicNormalization,
+ )
# TODO : should also check that if the colormap is changing then values (especially in log scale)
# should be coherent if in autoscale
diff --git a/src/silx/gui/plot/test/testCompareImages.py b/src/silx/gui/plot/test/testCompareImages.py
index cf54b99..4bc52b4 100644
--- a/src/silx/gui/plot/test/testCompareImages.py
+++ b/src/silx/gui/plot/test/testCompareImages.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
@@ -28,79 +27,210 @@ __authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "23/07/2018"
-import unittest
+import pytest
import numpy
import weakref
-from silx.gui.utils.testutils import TestCaseQt
+from silx.gui import qt
from silx.gui.plot.CompareImages import CompareImages
-class TestCompareImages(TestCaseQt):
- """Test that CompareImages widget is working in some cases"""
-
- def setUp(self):
- super(TestCompareImages, self).setUp()
- self.widget = CompareImages()
-
- def tearDown(self):
- ref = weakref.ref(self.widget)
- self.widget = None
- self.qWaitForDestroy(ref)
- super(TestCompareImages, self).tearDown()
-
- def testIntensityImage(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(10, 10)
- self.widget.setData(image1, image2)
-
- def testRgbImage(self):
- image1 = numpy.random.randint(0, 255, size=(10, 10, 3))
- image2 = numpy.random.randint(0, 255, size=(10, 10, 3))
- self.widget.setData(image1, image2)
-
- def testRgbaImage(self):
- image1 = numpy.random.randint(0, 255, size=(10, 10, 4))
- image2 = numpy.random.randint(0, 255, size=(10, 10, 4))
- self.widget.setData(image1, image2)
-
- def testVizualisations(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(10, 10)
- self.widget.setData(image1, image2)
- for mode in CompareImages.VisualizationMode:
- self.widget.setVisualizationMode(mode)
-
- def testAlignemnt(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(5, 5)
- self.widget.setData(image1, image2)
- for mode in CompareImages.AlignmentMode:
- self.widget.setAlignmentMode(mode)
-
- def testGetPixel(self):
- image1 = numpy.random.rand(11, 11)
- image2 = numpy.random.rand(5, 5)
- image1[5, 5] = 111.111
- image2[2, 2] = 222.222
- self.widget.setData(image1, image2)
- expectedValue = {}
- expectedValue[CompareImages.AlignmentMode.CENTER] = 222.222
- expectedValue[CompareImages.AlignmentMode.STRETCH] = 222.222
- expectedValue[CompareImages.AlignmentMode.ORIGIN] = None
- for mode in expectedValue.keys():
- self.widget.setAlignmentMode(mode)
- data = self.widget.getRawPixelData(11 / 2.0, 11 / 2.0)
- data1, data2 = data
- self.assertEqual(data1, 111.111)
- self.assertEqual(data2, expectedValue[mode])
-
- def testImageEmpty(self):
- self.widget.setData(image1=None, image2=None)
- self.assertTrue(self.widget.getRawPixelData(11 / 2.0, 11 / 2.0) == (None, None))
-
- def testSetImageSeparately(self):
- self.widget.setImage1(numpy.random.rand(10, 10))
- self.widget.setImage2(numpy.random.rand(10, 10))
- for mode in CompareImages.VisualizationMode:
- self.widget.setVisualizationMode(mode)
+@pytest.fixture
+def compareImages(qapp, qapp_utils):
+ widget = CompareImages()
+ widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ yield widget
+ widget.close()
+ ref = weakref.ref(widget)
+ widget = None
+ qapp_utils.qWaitForDestroy(ref)
+
+
+def testIntensityImage(compareImages):
+ image1 = numpy.random.rand(10, 10)
+ image2 = numpy.random.rand(10, 10)
+ compareImages.setData(image1, image2)
+
+
+def testRgbImage(compareImages):
+ image1 = numpy.random.randint(0, 255, size=(10, 10, 3))
+ image2 = numpy.random.randint(0, 255, size=(10, 10, 3))
+ compareImages.setData(image1, image2)
+
+
+def testRgbaImage(compareImages):
+ image1 = numpy.random.randint(0, 255, size=(10, 10, 4))
+ image2 = numpy.random.randint(0, 255, size=(10, 10, 4))
+ compareImages.setData(image1, image2)
+
+
+def testAlignemnt(compareImages):
+ image1 = numpy.random.rand(10, 10)
+ image2 = numpy.random.rand(5, 5)
+ compareImages.setData(image1, image2)
+ for mode in CompareImages.AlignmentMode:
+ compareImages.setAlignmentMode(mode)
+
+
+def testGetPixel(compareImages):
+ image1 = numpy.random.rand(11, 11)
+ image2 = numpy.random.rand(5, 5)
+ image1[5, 5] = 111.111
+ image2[2, 2] = 222.222
+ compareImages.setData(image1, image2)
+ expectedValue = {}
+ expectedValue[CompareImages.AlignmentMode.CENTER] = 222.222
+ expectedValue[CompareImages.AlignmentMode.STRETCH] = 222.222
+ expectedValue[CompareImages.AlignmentMode.ORIGIN] = None
+ for mode in expectedValue.keys():
+ compareImages.setAlignmentMode(mode)
+ data = compareImages.getRawPixelData(11 / 2.0, 11 / 2.0)
+ data1, data2 = data
+ assert data1 == 111.111
+ assert data2 == expectedValue[mode]
+
+
+def testImageEmpty(compareImages):
+ compareImages.setData(image1=None, image2=None)
+
+
+def testSetImageSeparately(compareImages):
+ compareImages.setImage1(numpy.random.rand(10, 10))
+ compareImages.setImage2(numpy.random.rand(10, 10))
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationMode(compareImages, data):
+ (visualizationMode,) = data
+ compareImages.setImage1(numpy.random.rand(10, 10))
+ compareImages.setImage2(numpy.random.rand(10, 10))
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationModeWithoutImage(compareImages, data):
+ (visualizationMode,) = data
+ compareImages.setImage1(None)
+ compareImages.setImage2(None)
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationModeWithOnlyImage1(compareImages, data):
+ (visualizationMode,) = data
+ compareImages.setImage1(numpy.random.rand(10, 10))
+ compareImages.setImage2(None)
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationModeWithOnlyImage2(compareImages, data):
+ (visualizationMode,) = data
+ compareImages.setImage1(None)
+ compareImages.setImage2(numpy.random.rand(10, 10))
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.VisualizationMode.COMPOSITE_A_MINUS_B,),
+ (CompareImages.VisualizationMode.COMPOSITE_RED_BLUE_GRAY,),
+ (CompareImages.VisualizationMode.HORIZONTAL_LINE,),
+ (CompareImages.VisualizationMode.VERTICAL_LINE,),
+ (CompareImages.VisualizationMode.ONLY_A,),
+ (CompareImages.VisualizationMode.ONLY_B,),
+ ],
+)
+def testVisualizationModeWithRGBImage(compareImages, data):
+ (visualizationMode,) = data
+ image1 = numpy.random.rand(10, 10)
+ image2 = numpy.random.randint(0, 255, size=(10, 10, 3))
+ compareImages.setData(image1, image2)
+ compareImages.setVisualizationMode(visualizationMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.AlignmentMode.STRETCH,),
+ (CompareImages.AlignmentMode.AUTO,),
+ (CompareImages.AlignmentMode.CENTER,),
+ (CompareImages.AlignmentMode.ORIGIN,),
+ ],
+)
+def testAlignemntModeWithoutImages(compareImages, data):
+ (alignmentMode,) = data
+ compareImages.setAlignmentMode(alignmentMode)
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ (CompareImages.AlignmentMode.STRETCH,),
+ (CompareImages.AlignmentMode.AUTO,),
+ (CompareImages.AlignmentMode.CENTER,),
+ (CompareImages.AlignmentMode.ORIGIN,),
+ ],
+)
+def testAlignemntModeWithSingleImage(compareImages, data):
+ (alignmentMode,) = data
+ compareImages.setImage1(numpy.arange(9).reshape(3, 3))
+ compareImages.setAlignmentMode(alignmentMode)
+
+
+def testTooltip(compareImages):
+ compareImages.setImage1(numpy.arange(9).reshape(3, 3))
+ compareImages.setImage2(numpy.arange(9).reshape(3, 3))
+ compareImages.getRawPixelData(1.5, 1.5)
+
+
+def testTooltipWithoutImage(compareImages):
+ compareImages.setImage1(numpy.arange(9).reshape(3, 3))
+ compareImages.setImage2(numpy.arange(9).reshape(3, 3))
+ compareImages.getRawPixelData(1.5, 1.5)
+
+
+def testTooltipWithSingleImage(compareImages):
+ compareImages.setImage1(numpy.arange(9).reshape(3, 3))
+ compareImages.getRawPixelData(1.5, 1.5)
diff --git a/src/silx/gui/plot/test/testComplexImageView.py b/src/silx/gui/plot/test/testComplexImageView.py
index 46025b9..f8b331b 100644
--- a/src/silx/gui/plot/test/testComplexImageView.py
+++ b/src/silx/gui/plot/test/testComplexImageView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "17/01/2018"
-import unittest
import logging
import numpy
@@ -58,7 +56,7 @@ class TestComplexImageView(PlotWidgetTestCase, ParametricTestCase):
# Test colormap API
colormap = self.plot.getColormap().copy()
- colormap.setName('magma')
+ colormap.setName("magma")
self.plot.setColormap(colormap)
self.qWait(100)
diff --git a/src/silx/gui/plot/test/testCurvesROIWidget.py b/src/silx/gui/plot/test/testCurvesROIWidget.py
index d7dfafd..05acd36 100644
--- a/src/silx/gui/plot/test/testCurvesROIWidget.py
+++ b/src/silx/gui/plot/test/testCurvesROIWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,8 +30,6 @@ __date__ = "16/11/2017"
import logging
import os.path
-import pytest
-from collections import OrderedDict
import numpy
from silx.gui import qt
@@ -41,9 +38,7 @@ from silx.gui.plot import Plot1D
from silx.test.utils import temp_dir
from silx.gui.utils.testutils import TestCaseQt, SignalListener
from silx.gui.plot import PlotWindow, CurvesROIWidget
-from silx.gui.plot.CurvesROIWidget import ROITable
from silx.gui.utils.testutils import getQToolButtonFromAction
-from silx.gui.plot.PlotInteraction import ItemsInteraction
_logger = logging.getLogger(__name__)
@@ -75,10 +70,12 @@ class TestCurvesROIWidget(TestCaseQt):
def testDummyAPI(self):
"""Simple test of the getRois and setRois API"""
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
+ roi_neg = CurvesROIWidget.ROI(
+ name="negative", fromdata=-20, todata=-10, type_="X"
+ )
+ roi_pos = CurvesROIWidget.ROI(
+ name="positive", fromdata=10, todata=20, type_="X"
+ )
self.widget.roiWidget.setRois((roi_pos, roi_neg))
@@ -88,9 +85,11 @@ class TestCurvesROIWidget(TestCaseQt):
def testWithCurves(self):
"""Plot with curves: test all ROI widget buttons"""
for offset in range(2):
- self.plot.addCurve(numpy.arange(1000),
- offset + numpy.random.random(1000),
- legend=str(offset))
+ self.plot.addCurve(
+ numpy.arange(1000),
+ offset + numpy.random.random(1000),
+ legend=str(offset),
+ )
# Add two ROI
self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
@@ -106,7 +105,7 @@ class TestCurvesROIWidget(TestCaseQt):
self.qWait(200)
with temp_dir() as tmpDir:
- self.tmpFile = os.path.join(tmpDir, 'test.ini')
+ self.tmpFile = os.path.join(tmpDir, "test.ini")
# Save ROIs
self.widget.roiWidget.save(self.tmpFile)
@@ -114,13 +113,12 @@ class TestCurvesROIWidget(TestCaseQt):
self.assertEqual(len(self.widget.getRois()), 2)
# Reset ROIs
- self.mouseClick(self.widget.roiWidget.resetButton,
- qt.Qt.LeftButton)
+ self.mouseClick(self.widget.roiWidget.resetButton, qt.Qt.LeftButton)
self.qWait(200)
rois = self.widget.getRois()
self.assertEqual(len(rois), 1)
roiID = list(rois.keys())[0]
- self.assertEqual(rois[roiID].getName(), 'ICR')
+ self.assertEqual(rois[roiID].getName(), "ICR")
# Load ROIs
self.widget.roiWidget.load(self.tmpFile)
@@ -136,18 +134,20 @@ class TestCurvesROIWidget(TestCaseQt):
self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
for roiID in self.widget.roiWidget.roiTable._markersHandler._roiMarkerHandlers:
- handler = self.widget.roiWidget.roiTable._markersHandler._roiMarkerHandlers[roiID]
- assert handler.getMarker('min')
- xleftMarker = handler.getMarker('min').getXPosition()
- xMiddleMarker = handler.getMarker('middle').getXPosition()
- xRightMarker = handler.getMarker('max').getXPosition()
- thValue = xleftMarker + (xRightMarker - xleftMarker) / 2.
+ handler = self.widget.roiWidget.roiTable._markersHandler._roiMarkerHandlers[
+ roiID
+ ]
+ assert handler.getMarker("min")
+ xleftMarker = handler.getMarker("min").getXPosition()
+ xMiddleMarker = handler.getMarker("middle").getXPosition()
+ xRightMarker = handler.getMarker("max").getXPosition()
+ thValue = xleftMarker + (xRightMarker - xleftMarker) / 2.0
self.assertAlmostEqual(xMiddleMarker, thValue)
def testAreaCalculation(self):
"""Test result of area calculation"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
+ x = numpy.arange(100.0)
+ y = numpy.arange(100.0)
# Add two curves
self.plot.addCurve(x, y, legend="positive")
@@ -157,30 +157,30 @@ class TestCurvesROIWidget(TestCaseQt):
self.plot.setActiveCurve("positive")
# Add two ROIs
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
+ roi_neg = CurvesROIWidget.ROI(
+ name="negative", fromdata=-20, todata=-10, type_="X"
+ )
+ roi_pos = CurvesROIWidget.ROI(
+ name="positive", fromdata=10, todata=20, type_="X"
+ )
self.widget.roiWidget.setRois((roi_pos, roi_neg))
- posCurve = self.plot.getCurve('positive')
- negCurve = self.plot.getCurve('negative')
+ posCurve = self.plot.getCurve("positive")
+ negCurve = self.plot.getCurve("negative")
- self.assertEqual(roi_pos.computeRawAndNetArea(posCurve),
- (numpy.trapz(y=[10, 20], x=[10, 20]),
- 0.0))
- self.assertEqual(roi_pos.computeRawAndNetArea(negCurve),
- (0.0, 0.0))
- self.assertEqual(roi_neg.computeRawAndNetArea(posCurve),
- ((0.0), 0.0))
- self.assertEqual(roi_neg.computeRawAndNetArea(negCurve),
- ((-150.0), 0.0))
+ self.assertEqual(
+ roi_pos.computeRawAndNetArea(posCurve),
+ (numpy.trapz(y=[10, 20], x=[10, 20]), 0.0),
+ )
+ self.assertEqual(roi_pos.computeRawAndNetArea(negCurve), (0.0, 0.0))
+ self.assertEqual(roi_neg.computeRawAndNetArea(posCurve), ((0.0), 0.0))
+ self.assertEqual(roi_neg.computeRawAndNetArea(negCurve), ((-150.0), 0.0))
def testCountsCalculation(self):
"""Test result of count calculation"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
+ x = numpy.arange(100.0)
+ y = numpy.arange(100.0)
# Add two curves
self.plot.addCurve(x, y, legend="positive")
@@ -190,36 +190,38 @@ class TestCurvesROIWidget(TestCaseQt):
self.plot.setActiveCurve("positive")
# Add two ROIs
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
+ roi_neg = CurvesROIWidget.ROI(
+ name="negative", fromdata=-20, todata=-10, type_="X"
+ )
+ roi_pos = CurvesROIWidget.ROI(
+ name="positive", fromdata=10, todata=20, type_="X"
+ )
self.widget.roiWidget.setRois((roi_pos, roi_neg))
- posCurve = self.plot.getCurve('positive')
- negCurve = self.plot.getCurve('negative')
+ posCurve = self.plot.getCurve("positive")
+ negCurve = self.plot.getCurve("negative")
- self.assertEqual(roi_pos.computeRawAndNetCounts(posCurve),
- (y[10:21].sum(), 0.0))
- self.assertEqual(roi_pos.computeRawAndNetCounts(negCurve),
- (0.0, 0.0))
- self.assertEqual(roi_neg.computeRawAndNetCounts(posCurve),
- ((0.0), 0.0))
- self.assertEqual(roi_neg.computeRawAndNetCounts(negCurve),
- (y[10:21].sum(), 0.0))
+ self.assertEqual(
+ roi_pos.computeRawAndNetCounts(posCurve), (y[10:21].sum(), 0.0)
+ )
+ self.assertEqual(roi_pos.computeRawAndNetCounts(negCurve), (0.0, 0.0))
+ self.assertEqual(roi_neg.computeRawAndNetCounts(posCurve), ((0.0), 0.0))
+ self.assertEqual(
+ roi_neg.computeRawAndNetCounts(negCurve), (y[10:21].sum(), 0.0)
+ )
def testDeferedInit(self):
"""Test behavior of the deferedInit"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
+ x = numpy.arange(100.0)
+ y = numpy.arange(100.0)
self.plot.addCurve(x=x, y=y, legend="name", replace="True")
- roisDefs = OrderedDict([
- ["range1",
- OrderedDict([["from", 20], ["to", 200], ["type", "energy"]])],
- ["range2",
- OrderedDict([["from", 300], ["to", 500], ["type", "energy"]])]
- ])
+ roisDefs = dict(
+ [
+ ["range1", dict([["from", 20], ["to", 200], ["type", "energy"]])],
+ ["range2", dict([["from", 300], ["to", 500], ["type", "energy"]])],
+ ]
+ )
roiWidget = self.plot.getCurvesRoiDockWidget().roiWidget
self.plot.getCurvesRoiDockWidget().setRois(roisDefs)
@@ -229,34 +231,41 @@ class TestCurvesROIWidget(TestCaseQt):
def testDictCompatibility(self):
"""Test that ROI api is valid with dict and not information is lost"""
- roiDict = {'from': 20, 'to': 200, 'type': 'energy', 'comment': 'no',
- 'name': 'myROI', 'calibration': [1, 2, 3]}
+ roiDict = {
+ "from": 20,
+ "to": 200,
+ "type": "energy",
+ "comment": "no",
+ "name": "myROI",
+ "calibration": [1, 2, 3],
+ }
roi = CurvesROIWidget.ROI._fromDict(roiDict)
self.assertEqual(roi.toDict(), roiDict)
def testShowAllROI(self):
"""Test the show allROI action"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
+ x = numpy.arange(100.0)
+ y = numpy.arange(100.0)
self.plot.addCurve(x=x, y=y, legend="name", replace="True")
roisDefsDict = {
- "range1": {"from": 20, "to": 200,"type": "energy"},
- "range2": {"from": 300, "to": 500, "type": "energy"}
+ "range1": {"from": 20, "to": 200, "type": "energy"},
+ "range2": {"from": 300, "to": 500, "type": "energy"},
}
roisDefsObj = (
- CurvesROIWidget.ROI(name='range3', fromdata=20, todata=200,
- type_='energy'),
- CurvesROIWidget.ROI(name='range4', fromdata=300, todata=500,
- type_='energy')
+ CurvesROIWidget.ROI(name="range3", fromdata=20, todata=200, type_="energy"),
+ CurvesROIWidget.ROI(
+ name="range4", fromdata=300, todata=500, type_="energy"
+ ),
)
self.widget.roiWidget.showAllMarkers(True)
roiWidget = self.plot.getCurvesRoiDockWidget().roiWidget
roiWidget.setRois(roisDefsDict)
- markers = [item for item in self.plot.getItems()
- if isinstance(item, items.MarkerBase)]
- self.assertEqual(len(markers), 2*3)
+ markers = [
+ item for item in self.plot.getItems() if isinstance(item, items.MarkerBase)
+ ]
+ self.assertEqual(len(markers), 2 * 3)
markersHandler = self.widget.roiWidget.roiTable._markersHandler
roiWidget.showAllMarkers(True)
@@ -269,9 +278,10 @@ class TestCurvesROIWidget(TestCaseQt):
roiWidget.setRois(roisDefsObj)
self.qapp.processEvents()
- markers = [item for item in self.plot.getItems()
- if isinstance(item, items.MarkerBase)]
- self.assertEqual(len(markers), 2*3)
+ markers = [
+ item for item in self.plot.getItems() if isinstance(item, items.MarkerBase)
+ ]
+ self.assertEqual(len(markers), 2 * 3)
markersHandler = self.widget.roiWidget.roiTable._markersHandler
roiWidget.showAllMarkers(True)
@@ -283,52 +293,51 @@ class TestCurvesROIWidget(TestCaseQt):
self.assertEqual(len(ICRROI), 1)
def testRoiEdition(self):
- """Make sure if the ROI object is edited the ROITable will be updated
- """
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
- self.widget.roiWidget.setRois((roi, ))
+ """Make sure if the ROI object is edited the ROITable will be updated"""
+ roi = CurvesROIWidget.ROI(name="linear", fromdata=0, todata=5)
+ self.widget.roiWidget.setRois((roi,))
x = (0, 1, 1, 2, 2, 3)
y = (1, 1, 2, 2, 1, 1)
- self.plot.addCurve(x=x, y=y, legend='linearCurve')
- self.plot.setActiveCurve(legend='linearCurve')
+ self.plot.addCurve(x=x, y=y, legend="linearCurve")
+ self.plot.setActiveCurve(legend="linearCurve")
self.widget.calculateROIs()
roiTable = self.widget.roiWidget.roiTable
indexesColumns = CurvesROIWidget.ROITable.COLUMNS_INDEX
- itemRawCounts = roiTable.item(0, indexesColumns['Raw Counts'])
- itemNetCounts = roiTable.item(0, indexesColumns['Net Counts'])
+ itemRawCounts = roiTable.item(0, indexesColumns["Raw Counts"])
+ itemNetCounts = roiTable.item(0, indexesColumns["Net Counts"])
- self.assertTrue(itemRawCounts.text() == '8.0')
- self.assertTrue(itemNetCounts.text() == '2.0')
+ self.assertTrue(itemRawCounts.text() == "8.0")
+ self.assertTrue(itemNetCounts.text() == "2.0")
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- itemNetArea = roiTable.item(0, indexesColumns['Net Area'])
+ itemRawArea = roiTable.item(0, indexesColumns["Raw Area"])
+ itemNetArea = roiTable.item(0, indexesColumns["Net Area"])
- self.assertTrue(itemRawArea.text() == '4.0')
- self.assertTrue(itemNetArea.text() == '1.0')
+ self.assertTrue(itemRawArea.text() == "4.0")
+ self.assertTrue(itemNetArea.text() == "1.0")
roi.setTo(2)
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- self.assertTrue(itemRawArea.text() == '3.0')
+ itemRawArea = roiTable.item(0, indexesColumns["Raw Area"])
+ self.assertTrue(itemRawArea.text() == "3.0")
roi.setFrom(1)
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- self.assertTrue(itemRawArea.text() == '2.0')
+ itemRawArea = roiTable.item(0, indexesColumns["Raw Area"])
+ self.assertTrue(itemRawArea.text() == "2.0")
def testRemoveActiveROI(self):
"""Test widget behavior when removing the active ROI"""
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
+ roi = CurvesROIWidget.ROI(name="linear", fromdata=0, todata=5)
self.widget.roiWidget.setRois((roi,))
self.widget.roiWidget.roiTable.setActiveRoi(None)
self.assertEqual(len(self.widget.roiWidget.roiTable.selectedItems()), 0)
self.widget.roiWidget.setRois((roi,))
- self.plot.setActiveCurve(legend='linearCurve')
+ self.plot.setActiveCurve(legend="linearCurve")
self.widget.calculateROIs()
def testEmitCurrentROI(self):
"""Test behavior of the CurvesROIWidget.sigROISignal"""
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
+ roi = CurvesROIWidget.ROI(name="linear", fromdata=0, todata=5)
self.widget.roiWidget.setRois((roi,))
signalListener = SignalListener()
self.widget.roiWidget.sigROISignal.connect(signalListener.partial())
@@ -348,10 +357,12 @@ class TestRoiWidgetSignals(TestCaseQt):
"""Test Signals emitted by the RoiWidgetSignals"""
def setUp(self):
+ super().setUp()
+
self.plot = Plot1D()
x = range(20)
y = range(20)
- self.plot.addCurve(x, y, legend='curve0')
+ self.plot.addCurve(x, y, legend="curve0")
self.listener = SignalListener()
self.curves_roi_widget = self.plot.getCurvesRoiWidget()
self.curves_roi_widget.sigROISignal.connect(self.listener)
@@ -368,40 +379,47 @@ class TestRoiWidgetSignals(TestCaseQt):
self.qWaitForWindowExposed(self.curves_roi_widget)
def tearDown(self):
- self.plot = None
- self.curves_roi_widget = None
+ self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.plot.close()
+ del self.plot
+
+ self.curves_roi_widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.curves_roi_widget.close()
+ del self.curves_roi_widget
+
+ super().tearDown()
def testSigROISignalAddRmRois(self):
"""Test SigROISignal when adding and removing ROIS"""
self.listener.clear()
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
+ roi1 = CurvesROIWidget.ROI(name="linear", fromdata=0, todata=5)
self.curves_roi_widget.roiTable.addRoi(roi1)
self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "linear")
self.listener.clear()
- roi2 = CurvesROIWidget.ROI(name='linear2', fromdata=0, todata=5)
+ roi2 = CurvesROIWidget.ROI(name="linear2", fromdata=0, todata=5)
self.curves_roi_widget.roiTable.addRoi(roi2)
self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear2')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "linear2")
self.listener.clear()
self.curves_roi_widget.roiTable.removeROI(roi2)
self.assertEqual(self.listener.callCount(), 1)
self.assertTrue(self.curves_roi_widget.roiTable.activeRoi == roi1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "linear")
self.listener.clear()
self.curves_roi_widget.roiTable.deleteActiveRoi()
self.assertEqual(self.listener.callCount(), 1)
self.assertTrue(self.curves_roi_widget.roiTable.activeRoi is None)
- self.assertTrue(self.listener.arguments()[0][0]['current'] is None)
+ self.assertTrue(self.listener.arguments()[0][0]["current"] is None)
self.listener.clear()
self.curves_roi_widget.roiTable.addRoi(roi1)
self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "linear")
self.assertTrue(self.curves_roi_widget.roiTable.activeRoi == roi1)
self.listener.clear()
self.qapp.processEvents()
@@ -409,13 +427,13 @@ class TestRoiWidgetSignals(TestCaseQt):
self.curves_roi_widget.roiTable.removeROI(roi1)
self.qapp.processEvents()
self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'ICR')
+ self.assertTrue(self.listener.arguments()[0][0]["current"] == "ICR")
self.listener.clear()
def testSigROISignalModifyROI(self):
"""Test SigROISignal when modifying it"""
self.curves_roi_widget.roiTable.setMiddleROIMarkerFlag(True)
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=2, todata=5)
+ roi1 = CurvesROIWidget.ROI(name="linear", fromdata=2, todata=5)
self.curves_roi_widget.roiTable.addRoi(roi1)
self.curves_roi_widget.roiTable.setActiveRoi(roi1)
@@ -427,10 +445,10 @@ class TestRoiWidgetSignals(TestCaseQt):
roi1.setTo(2.56)
self.assertEqual(self.listener.callCount(), 1)
self.listener.clear()
- roi1.setName('linear2')
+ roi1.setName("linear2")
self.assertEqual(self.listener.callCount(), 1)
self.listener.clear()
- roi1.setType('new type')
+ roi1.setType("new type")
self.assertEqual(self.listener.callCount(), 1)
widget = self.plot.getWidgetHandle()
@@ -439,18 +457,24 @@ class TestRoiWidgetSignals(TestCaseQt):
self.qapp.processEvents()
# modify roi limits (from the gui)
- roi_marker_handler = self.curves_roi_widget.roiTable._markersHandler.getMarkerHandler(roi1.getID())
- for marker_type in ('min', 'max', 'middle'):
+ roi_marker_handler = (
+ self.curves_roi_widget.roiTable._markersHandler.getMarkerHandler(
+ roi1.getID()
+ )
+ )
+ for marker_type in ("min", "max", "middle"):
with self.subTest(marker_type=marker_type):
self.listener.clear()
marker = roi_marker_handler.getMarker(marker_type)
- x_pix, y_pix = self.plot.dataToPixel(marker.getXPosition(), marker.getYPosition())
+ x_pix, y_pix = self.plot.dataToPixel(
+ marker.getXPosition(), marker.getYPosition()
+ )
self.mouseMove(widget, pos=(x_pix, y_pix))
self.qWait(100)
self.mousePress(widget, qt.Qt.LeftButton, pos=(x_pix, y_pix))
- self.mouseMove(widget, pos=(x_pix+20, y_pix))
+ self.mouseMove(widget, pos=(x_pix + 20, y_pix))
self.qWait(100)
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=(x_pix+20, y_pix))
+ self.mouseRelease(widget, qt.Qt.LeftButton, pos=(x_pix + 20, y_pix))
self.qWait(100)
self.mouseMove(widget, pos=(x_pix, y_pix))
self.qapp.processEvents()
@@ -458,8 +482,8 @@ class TestRoiWidgetSignals(TestCaseQt):
def testSetActiveCurve(self):
"""Test sigRoiSignal when set an active curve"""
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=2, todata=5)
+ roi1 = CurvesROIWidget.ROI(name="linear", fromdata=2, todata=5)
self.curves_roi_widget.roiTable.setActiveRoi(roi1)
self.listener.clear()
- self.plot.setActiveCurve('curve0')
+ self.plot.setActiveCurve("curve0")
self.assertEqual(self.listener.callCount(), 0)
diff --git a/src/silx/gui/plot/test/testImageStack.py b/src/silx/gui/plot/test/testImageStack.py
index 5c44691..482cdfd 100644
--- a/src/silx/gui/plot/test/testImageStack.py
+++ b/src/silx/gui/plot/test/testImageStack.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2020 European Synchrotron Radiation Facility
+# Copyright (c) 2020-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "15/01/2020"
-import unittest
import tempfile
import numpy
import h5py
@@ -39,7 +37,6 @@ from silx.gui.utils.testutils import TestCaseQt
from silx.io.url import DataUrl
from silx.gui.plot.ImageStack import ImageStack
from silx.gui.utils.testutils import SignalListener
-from collections import OrderedDict
import os
import time
import shutil
@@ -50,21 +47,21 @@ class TestImageStack(TestCaseQt):
def setUp(self):
TestCaseQt.setUp(self)
- self.urls = OrderedDict()
+ self.urls = {}
self._raw_data = {}
self._folder = tempfile.mkdtemp()
self._n_urls = 10
- file_name = os.path.join(self._folder, 'test_inage_stack_file.h5')
- with h5py.File(file_name, 'w') as h5f:
+ file_name = os.path.join(self._folder, "test_inage_stack_file.h5")
+ with h5py.File(file_name, "w") as h5f:
for i in range(self._n_urls):
width = numpy.random.randint(10, 40)
height = numpy.random.randint(10, 40)
raw_data = numpy.random.random((width, height))
self._raw_data[i] = raw_data
h5f[str(i)] = raw_data
- self.urls[i] = DataUrl(file_path=file_name,
- data_path=str(i),
- scheme='silx')
+ self.urls[i] = DataUrl(
+ file_path=file_name, data_path=str(i), scheme="silx"
+ )
self.widget = ImageStack()
self.urlLoadedListener = SignalListener()
@@ -80,8 +77,7 @@ class TestImageStack(TestCaseQt):
TestCaseQt.setUp(self)
def testControls(self):
- """Test that selection using the url table and the slider are working
- """
+ """Test that selection using the url table and the slider are working"""
self.widget.show()
self.assertEqual(self.widget.getCurrentUrl(), None)
self.assertEqual(self.widget.getCurrentUrlIndex(), None)
@@ -96,13 +92,15 @@ class TestImageStack(TestCaseQt):
self.assertEqual(self.urlLoadedListener.callCount(), self._n_urls)
numpy.testing.assert_array_equal(
self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[0])
+ self._raw_data[0],
+ )
self.assertEqual(self.widget._slider.value(), 0)
self.widget._urlsTable.setUrl(self.urls[4])
numpy.testing.assert_array_equal(
self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[4])
+ self._raw_data[4],
+ )
self.assertEqual(self.widget._slider.value(), 4)
self.assertEqual(self.widget.getCurrentUrl(), self.urls[4])
self.assertEqual(self.widget.getCurrentUrlIndex(), 4)
@@ -110,9 +108,11 @@ class TestImageStack(TestCaseQt):
self.widget._slider.setUrlIndex(6)
numpy.testing.assert_array_equal(
self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[6])
- self.assertEqual(self.widget._urlsTable.currentItem().text(),
- self.urls[6].path())
+ self._raw_data[6],
+ )
+ self.assertEqual(
+ self.widget._urlsTable.currentItem().text(), self.urls[6].path()
+ )
def testCurrentUrlSignals(self):
"""Test emission of 'currentUrlChangedListener'"""
@@ -152,26 +152,72 @@ class TestImageStack(TestCaseQt):
self.assertEqual(urls_values[0], self.urls[0])
self.assertEqual(urls_values[7], self.urls[7])
- self.assertEqual(self.widget._getNextUrl(urls_values[2]).path(),
- urls_values[3].path())
+ self.assertEqual(
+ self.widget._getNextUrl(urls_values[2]).path(), urls_values[3].path()
+ )
self.assertEqual(self.widget._getPreviousUrl(urls_values[0]), None)
- self.assertEqual(self.widget._getPreviousUrl(urls_values[6]).path(),
- urls_values[5].path())
-
- self.assertEqual(self.widget._getNNextUrls(2, urls_values[0]),
- urls_values[1:3])
- self.assertEqual(self.widget._getNNextUrls(5, urls_values[7]),
- urls_values[8:])
- self.assertEqual(self.widget._getNPreviousUrls(3, urls_values[2]),
- urls_values[:2])
- self.assertEqual(self.widget._getNPreviousUrls(5, urls_values[8]),
- urls_values[3:8])
+ self.assertEqual(
+ self.widget._getPreviousUrl(urls_values[6]).path(), urls_values[5].path()
+ )
+
+ self.assertEqual(self.widget._getNNextUrls(2, urls_values[0]), urls_values[1:3])
+ self.assertEqual(self.widget._getNNextUrls(5, urls_values[7]), urls_values[8:])
+ self.assertEqual(
+ self.widget._getNPreviousUrls(3, urls_values[2]), urls_values[:2]
+ )
+ self.assertEqual(
+ self.widget._getNPreviousUrls(5, urls_values[8]), urls_values[3:8]
+ )
+
+ def testRemoveUrlFromList(self):
+ """
+ Test behavior when some item (url) are removed from the list
+ """
+ self.widget.setUrlsEditable(True)
+ self.widget.show()
+ self.widget.setUrls(list(self.urls.values()))
+ self.assertEqual(len(self.widget.getUrls()), len(self.urls))
+
+ # wait for image to be loaded
+ self._waitUntilUrlLoaded()
+ ll_slider = self.widget._slider._slider
+ assert ll_slider.maximum() - ll_slider.minimum() + 1 == len(self.urls)
+
+ # remove some urls from the list (~ simulating behavior with a right click)
+ urlsTable = self.widget._urlsTable._urlsTable
+ urlsTable.clearSelection()
+ urlsTable.item(1).setSelected(True)
+ urlsTable.item(2).setSelected(True)
+ urlsTable._removeSelectedItems()
+ self.qapp.processEvents()
+
+ # make sure slider has been updated
+ assert ll_slider.maximum() - ll_slider.minimum() + 1 == len(self.urls) - 2
+ # as the ImageStack widget
+ assert len(self.widget._urls) == len(self.urls) - 2
+ removed_urls = list(self.urls.values())[1:3]
+
+ existing_urls_as_str = [url.path() for url in self.widget._urls.values()]
+ for removed_url in removed_urls:
+ assert type(removed_url) == type(tuple(self.widget._urls.values())[0])
+ assert removed_url.path() not in existing_urls_as_str
+ # make sure we have some data plot
+ self.widget.getPlotWidget().getActiveImage() is not None
+
+ # test removing remaining urls
+ urlsTable.selectAll()
+ urlsTable._removeSelectedItems()
+ self.qapp.processEvents()
+ assert len(self.widget._urls) == 0
+ assert ll_slider.maximum() - ll_slider.minimum() == 0
+ # make sure if all urls are removed nothing is plot anymore
+ self.widget.getPlotWidget().getActiveImage() is None
def _waitUntilUrlLoaded(self, timeout=2.0):
"""Wait until all image urls are loaded"""
loop_duration = 0.2
remaining_duration = timeout
- while(len(self.widget._loadingThreads) > 0 and remaining_duration > 0):
+ while len(self.widget._loadingThreads) > 0 and remaining_duration > 0:
remaining_duration -= loop_duration
time.sleep(loop_duration)
self.qapp.processEvents()
@@ -180,7 +226,9 @@ class TestImageStack(TestCaseQt):
remaining_urls = []
for thread_ in self.widget._loadingThreads:
remaining_urls.append(thread_.url.path())
- mess = 'All images are not loaded after the time out. ' \
- 'Remaining urls are: ' + str(remaining_urls)
+ mess = (
+ "All images are not loaded after the time out. "
+ "Remaining urls are: " + str(remaining_urls)
+ )
raise TimeoutError(mess)
return True
diff --git a/src/silx/gui/plot/test/testImageView.py b/src/silx/gui/plot/test/testImageView.py
index 7c1355f..df19ab7 100644
--- a/src/silx/gui/plot/test/testImageView.py
+++ b/src/silx/gui/plot/test/testImageView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -93,31 +92,33 @@ class TestImageView(TestCaseQt):
self.plot.setImage(image)
# Colormap as dict
- self.plot.setColormap({'name': 'viridis',
- 'normalization': 'log',
- 'autoscale': False,
- 'vmin': 0,
- 'vmax': 1})
+ self.plot.setColormap(
+ {
+ "name": "viridis",
+ "normalization": "log",
+ "autoscale": False,
+ "vmin": 0,
+ "vmax": 1,
+ }
+ )
colormap = self.plot.getColormap()
- self.assertEqual(colormap.getName(), 'viridis')
- self.assertEqual(colormap.getNormalization(), 'log')
+ self.assertEqual(colormap.getName(), "viridis")
+ self.assertEqual(colormap.getNormalization(), "log")
self.assertEqual(colormap.getVMin(), 0)
self.assertEqual(colormap.getVMax(), 1)
# Colormap as keyword arguments
- self.plot.setColormap(colormap='magma',
- normalization='linear',
- autoscale=True,
- vmin=1,
- vmax=2)
- self.assertEqual(colormap.getName(), 'magma')
- self.assertEqual(colormap.getNormalization(), 'linear')
+ self.plot.setColormap(
+ colormap="magma", normalization="linear", autoscale=True, vmin=1, vmax=2
+ )
+ self.assertEqual(colormap.getName(), "magma")
+ self.assertEqual(colormap.getNormalization(), "linear")
self.assertEqual(colormap.getVMin(), None)
self.assertEqual(colormap.getVMax(), None)
# Update colormap with keyword argument
- self.plot.setColormap(normalization='log')
- self.assertEqual(colormap.getNormalization(), 'log')
+ self.plot.setColormap(normalization="log")
+ self.assertEqual(colormap.getNormalization(), "log")
# Colormap as Colormap object
cmap = Colormap()
@@ -131,7 +132,7 @@ class TestImageView(TestCaseQt):
ImageView.ProfileWindowBehavior.POPUP,
)
- self.plot.setProfileWindowBehavior('embedded')
+ self.plot.setProfileWindowBehavior("embedded")
self.assertIs(
self.plot.getProfileWindowBehavior(),
ImageView.ProfileWindowBehavior.EMBEDDED,
@@ -140,9 +141,7 @@ class TestImageView(TestCaseQt):
image = numpy.arange(100).reshape(10, 10)
self.plot.setImage(image)
- self.plot.setProfileWindowBehavior(
- ImageView.ProfileWindowBehavior.POPUP
- )
+ self.plot.setProfileWindowBehavior(ImageView.ProfileWindowBehavior.POPUP)
self.assertIs(
self.plot.getProfileWindowBehavior(),
ImageView.ProfileWindowBehavior.POPUP,
@@ -171,7 +170,9 @@ class TestImageView(TestCaseQt):
image = numpy.arange(100).reshape(10, 10)
self.plot.setImage(image, reset=True)
self.qWait(100)
- self.plot.getAggregationModeAction().setAggregationMode(items.ImageDataAggregated.Aggregation.MAX)
+ self.plot.getAggregationModeAction().setAggregationMode(
+ items.ImageDataAggregated.Aggregation.MAX
+ )
self.qWait(100)
def testImageAggregationModeBackToNormalMode(self):
@@ -179,9 +180,13 @@ class TestImageView(TestCaseQt):
image = numpy.arange(100).reshape(10, 10)
self.plot.setImage(image, reset=True)
self.qWait(100)
- self.plot.getAggregationModeAction().setAggregationMode(items.ImageDataAggregated.Aggregation.MAX)
+ self.plot.getAggregationModeAction().setAggregationMode(
+ items.ImageDataAggregated.Aggregation.MAX
+ )
self.qWait(100)
- self.plot.getAggregationModeAction().setAggregationMode(items.ImageDataAggregated.Aggregation.NONE)
+ self.plot.getAggregationModeAction().setAggregationMode(
+ items.ImageDataAggregated.Aggregation.NONE
+ )
self.qWait(100)
def testRGBAInAggregationMode(self):
@@ -190,5 +195,7 @@ class TestImageView(TestCaseQt):
self.plot.setImage(image, reset=True)
self.qWait(100)
- self.plot.getAggregationModeAction().setAggregationMode(items.ImageDataAggregated.Aggregation.MAX)
+ self.plot.getAggregationModeAction().setAggregationMode(
+ items.ImageDataAggregated.Aggregation.MAX
+ )
self.qWait(100)
diff --git a/src/silx/gui/plot/test/testInteraction.py b/src/silx/gui/plot/test/testInteraction.py
index d136b21..b031454 100644
--- a/src/silx/gui/plot/test/testInteraction.py
+++ b/src/silx/gui/plot/test/testInteraction.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
@@ -41,38 +40,40 @@ class TestInteraction(unittest.TestCase):
class TestClickOrDrag(Interaction.ClickOrDrag):
def click(self, x, y, btn):
- events.append(('click', x, y, btn))
+ events.append(("click", x, y, btn))
def beginDrag(self, x, y, btn):
- events.append(('beginDrag', x, y, btn))
+ events.append(("beginDrag", x, y, btn))
def drag(self, x, y, btn):
- events.append(('drag', x, y, btn))
+ events.append(("drag", x, y, btn))
def endDrag(self, start, end, btn):
- events.append(('endDrag', start, end, btn))
+ events.append(("endDrag", start, end, btn))
clickOrDrag = TestClickOrDrag()
# click
- clickOrDrag.handleEvent('press', 10, 10, Interaction.LEFT_BTN)
+ clickOrDrag.handleEvent("press", 10, 10, Interaction.LEFT_BTN)
self.assertEqual(len(events), 0)
- clickOrDrag.handleEvent('release', 10, 10, Interaction.LEFT_BTN)
+ clickOrDrag.handleEvent("release", 10, 10, Interaction.LEFT_BTN)
self.assertEqual(len(events), 1)
- self.assertEqual(events[0], ('click', 10, 10, Interaction.LEFT_BTN))
+ self.assertEqual(events[0], ("click", 10, 10, Interaction.LEFT_BTN))
# drag
events = []
- clickOrDrag.handleEvent('press', 10, 10, Interaction.LEFT_BTN)
+ clickOrDrag.handleEvent("press", 10, 10, Interaction.LEFT_BTN)
self.assertEqual(len(events), 0)
- clickOrDrag.handleEvent('move', 15, 10)
+ clickOrDrag.handleEvent("move", 15, 10)
self.assertEqual(len(events), 2) # Received beginDrag and drag
- self.assertEqual(events[0], ('beginDrag', 10, 10, Interaction.LEFT_BTN))
- self.assertEqual(events[1], ('drag', 15, 10, Interaction.LEFT_BTN))
- clickOrDrag.handleEvent('move', 20, 10)
+ self.assertEqual(events[0], ("beginDrag", 10, 10, Interaction.LEFT_BTN))
+ self.assertEqual(events[1], ("drag", 15, 10, Interaction.LEFT_BTN))
+ clickOrDrag.handleEvent("move", 20, 10)
self.assertEqual(len(events), 3)
- self.assertEqual(events[-1], ('drag', 20, 10, Interaction.LEFT_BTN))
- clickOrDrag.handleEvent('release', 20, 10, Interaction.LEFT_BTN)
+ self.assertEqual(events[-1], ("drag", 20, 10, Interaction.LEFT_BTN))
+ clickOrDrag.handleEvent("release", 20, 10, Interaction.LEFT_BTN)
self.assertEqual(len(events), 4)
- self.assertEqual(events[-1], ('endDrag', (10, 10), (20, 10), Interaction.LEFT_BTN))
+ self.assertEqual(
+ events[-1], ("endDrag", (10, 10), (20, 10), Interaction.LEFT_BTN)
+ )
diff --git a/src/silx/gui/plot/test/testItem.py b/src/silx/gui/plot/test/testItem.py
index 0b15dc3..8a6db40 100644
--- a/src/silx/gui/plot/test/testItem.py
+++ b/src/silx/gui/plot/test/testItem.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,11 +28,11 @@ __license__ = "MIT"
__date__ = "01/09/2017"
-import unittest
-
import numpy
+import pytest
from silx.gui.utils.testutils import SignalListener
+from silx.gui.plot.items.roi import RegionOfInterest
from silx.gui.plot.items import ItemChangedType
from silx.gui.plot import items
from .utils import PlotWidgetTestCase
@@ -44,8 +43,8 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
def testCurveChanged(self):
"""Test sigItemChanged for curve"""
- self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend='test')
- curve = self.plot.getCurve('test')
+ self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend="test")
+ curve = self.plot.getCurve("test")
listener = SignalListener()
curve.sigItemChanged.connect(listener)
@@ -59,8 +58,8 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
curve.setData(numpy.arange(100), numpy.arange(100))
# SymbolMixIn
- curve.setSymbol('Circle')
- curve.setSymbol('d')
+ curve.setSymbol("Circle")
+ curve.setSymbol("d")
curve.setSymbolSize(20)
# AlphaMixIn
@@ -68,49 +67,51 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Test for signals in Curve class
# ColorMixIn
- curve.setColor('yellow')
+ curve.setColor("yellow")
# YAxisMixIn
- curve.setYAxis('right')
+ curve.setYAxis("right")
# FillMixIn
curve.setFill(True)
# LineMixIn
- curve.setLineStyle(':')
- curve.setLineStyle(':') # Not sending event
+ curve.setLineStyle(":")
+ curve.setLineStyle(":") # Not sending event
curve.setLineWidth(2)
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.VISIBLE,
- ItemChangedType.VISIBLE,
- ItemChangedType.ZVALUE,
- ItemChangedType.DATA,
- ItemChangedType.SYMBOL,
- ItemChangedType.SYMBOL,
- ItemChangedType.SYMBOL_SIZE,
- ItemChangedType.ALPHA,
- ItemChangedType.COLOR,
- ItemChangedType.YAXIS,
- ItemChangedType.FILL,
- ItemChangedType.LINE_STYLE,
- ItemChangedType.LINE_WIDTH])
+ self.assertEqual(
+ listener.arguments(argumentIndex=0),
+ [
+ ItemChangedType.VISIBLE,
+ ItemChangedType.VISIBLE,
+ ItemChangedType.ZVALUE,
+ ItemChangedType.DATA,
+ ItemChangedType.SYMBOL,
+ ItemChangedType.SYMBOL,
+ ItemChangedType.SYMBOL_SIZE,
+ ItemChangedType.ALPHA,
+ ItemChangedType.COLOR,
+ ItemChangedType.YAXIS,
+ ItemChangedType.FILL,
+ ItemChangedType.LINE_STYLE,
+ ItemChangedType.LINE_WIDTH,
+ ],
+ )
def testHistogramChanged(self):
"""Test sigItemChanged for Histogram"""
- self.plot.addHistogram(
- numpy.arange(10), edges=numpy.arange(11), legend='test')
- histogram = self.plot.getHistogram('test')
+ self.plot.addHistogram(numpy.arange(10), edges=numpy.arange(11), legend="test")
+ histogram = self.plot.getHistogram("test")
listener = SignalListener()
histogram.sigItemChanged.connect(listener)
# Test signals in Histogram class
histogram.setData(numpy.zeros(10), numpy.arange(11))
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.DATA])
+ self.assertEqual(listener.arguments(argumentIndex=0), [ItemChangedType.DATA])
def testImageDataChanged(self):
"""Test sigItemChanged for ImageData"""
- self.plot.addImage(numpy.arange(100).reshape(10, 10), legend='test')
- image = self.plot.getImage('test')
+ self.plot.addImage(numpy.arange(100).reshape(10, 10), legend="test")
+ image = self.plot.getImage("test")
listener = SignalListener()
image.sigItemChanged.connect(listener)
@@ -118,7 +119,7 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# ColormapMixIn
colormap = self.plot.getDefaultColormap().copy()
image.setColormap(colormap)
- image.getColormap().setName('viridis')
+ image.getColormap().setName("viridis")
# Test of signals in ImageBase class
image.setOrigin(10)
@@ -127,18 +128,22 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Test of signals in ImageData class
image.setData(numpy.ones((10, 10)))
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.COLORMAP,
- ItemChangedType.COLORMAP,
- ItemChangedType.POSITION,
- ItemChangedType.SCALE,
- ItemChangedType.COLORMAP,
- ItemChangedType.DATA])
+ self.assertEqual(
+ listener.arguments(argumentIndex=0),
+ [
+ ItemChangedType.COLORMAP,
+ ItemChangedType.COLORMAP,
+ ItemChangedType.POSITION,
+ ItemChangedType.SCALE,
+ ItemChangedType.COLORMAP,
+ ItemChangedType.DATA,
+ ],
+ )
def testImageRgbaChanged(self):
"""Test sigItemChanged for ImageRgba"""
- self.plot.addImage(numpy.ones((10, 10, 3)), legend='rgb')
- image = self.plot.getImage('rgb')
+ self.plot.addImage(numpy.ones((10, 10, 3)), legend="rgb")
+ image = self.plot.getImage("rgb")
listener = SignalListener()
image.sigItemChanged.connect(listener)
@@ -146,13 +151,12 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Test of signals in ImageRgba class
image.setData(numpy.zeros((10, 10, 3)))
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.DATA])
+ self.assertEqual(listener.arguments(argumentIndex=0), [ItemChangedType.DATA])
def testMarkerChanged(self):
"""Test sigItemChanged for markers"""
- self.plot.addMarker(10, 20, legend='test')
- marker = self.plot._getMarker('test')
+ self.plot.addMarker(10, 20, legend="test")
+ marker = self.plot._getMarker("test")
listener = SignalListener()
marker.sigItemChanged.connect(listener)
@@ -160,42 +164,45 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Test signals in _BaseMarker
marker.setPosition(10, 10)
marker.setPosition(10, 10) # Not sending event
- marker.setText('toto')
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION,
- ItemChangedType.TEXT])
+ marker.setText("toto")
+ self.assertEqual(
+ listener.arguments(argumentIndex=0),
+ [ItemChangedType.POSITION, ItemChangedType.TEXT],
+ )
# XMarker
- self.plot.addXMarker(10, legend='x')
- marker = self.plot._getMarker('x')
+ self.plot.addXMarker(10, legend="x")
+ marker = self.plot._getMarker("x")
listener = SignalListener()
marker.sigItemChanged.connect(listener)
marker.setPosition(20, 20)
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION])
+ self.assertEqual(
+ listener.arguments(argumentIndex=0), [ItemChangedType.POSITION]
+ )
# YMarker
- self.plot.addYMarker(10, legend='x')
- marker = self.plot._getMarker('x')
+ self.plot.addYMarker(10, legend="x")
+ marker = self.plot._getMarker("x")
listener = SignalListener()
marker.sigItemChanged.connect(listener)
marker.setPosition(20, 20)
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION])
+ self.assertEqual(
+ listener.arguments(argumentIndex=0), [ItemChangedType.POSITION]
+ )
def testScatterChanged(self):
"""Test sigItemChanged for scatter"""
data = numpy.arange(10)
- self.plot.addScatter(data, data, data, legend='test')
- scatter = self.plot.getScatter('test')
+ self.plot.addScatter(data, data, data, legend="test")
+ scatter = self.plot.getScatter("test")
listener = SignalListener()
scatter.sigItemChanged.connect(listener)
# ColormapMixIn
- scatter.getColormap().setName('viridis')
+ scatter.getColormap().setName("viridis")
# Test of signals in Scatter class
scatter.setData((0, 1, 2), (1, 0, 2), (0, 1, 2))
@@ -203,44 +210,48 @@ class TestSigItemChangedSignal(PlotWidgetTestCase):
# Visualization mode changed
scatter.setVisualization(scatter.Visualization.SOLID)
- self.assertEqual(listener.arguments(),
- [(ItemChangedType.COLORMAP,),
- (ItemChangedType.DATA,),
- (ItemChangedType.COLORMAP,),
- (ItemChangedType.VISUALIZATION_MODE,)])
+ self.assertEqual(
+ listener.arguments(),
+ [
+ (ItemChangedType.COLORMAP,),
+ (ItemChangedType.DATA,),
+ (ItemChangedType.COLORMAP,),
+ (ItemChangedType.VISUALIZATION_MODE,),
+ ],
+ )
def testShapeChanged(self):
"""Test sigItemChanged for shape"""
- data = numpy.array((1., 10.))
- self.plot.addShape(data, data, legend='test', shape='rectangle')
- shape = self.plot._getItem(kind='item', legend='test')
+ data = numpy.array((1.0, 10.0))
+ self.plot.addShape(data, data, legend="test", shape="rectangle")
+ shape = self.plot._getItem(kind="item", legend="test")
listener = SignalListener()
shape.sigItemChanged.connect(listener)
shape.setOverlay(True)
- shape.setPoints(((2., 2.), (3., 3.)))
+ shape.setPoints(((2.0, 2.0), (3.0, 3.0)))
- self.assertEqual(listener.arguments(),
- [(ItemChangedType.OVERLAY,),
- (ItemChangedType.DATA,)])
+ self.assertEqual(
+ listener.arguments(), [(ItemChangedType.OVERLAY,), (ItemChangedType.DATA,)]
+ )
class TestSymbol(PlotWidgetTestCase):
- """Test item's symbol """
+ """Test item's symbol"""
def test(self):
"""Test sigItemChanged for curve"""
- self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend='test')
- curve = self.plot.getCurve('test')
+ self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend="test")
+ curve = self.plot.getCurve("test")
# SymbolMixIn
- curve.setSymbol('o')
+ curve.setSymbol("o")
name = curve.getSymbolName()
- self.assertEqual('Circle', name)
+ self.assertEqual("Circle", name)
- name = curve.getSymbolName('d')
- self.assertEqual('Diamond', name)
+ name = curve.getSymbolName("d")
+ self.assertEqual("Diamond", name)
class TestVisibleExtent(PlotWidgetTestCase):
@@ -254,7 +265,7 @@ class TestVisibleExtent(PlotWidgetTestCase):
curve.setData((1, 2, 3), (0, 1, 2))
histogram = items.Histogram()
- histogram.setData((0, 1, 2), (1, 5/3, 7/3, 3))
+ histogram.setData((0, 1, 2), (1, 5 / 3, 7 / 3, 3))
image = items.ImageData()
image.setOrigin((1, 0))
@@ -272,10 +283,10 @@ class TestVisibleExtent(PlotWidgetTestCase):
xaxis.setLimits(0, 100)
yaxis.setLimits(0, 100)
self.plot.addItem(item)
- self.assertEqual(item.getVisibleBounds(), (1., 3., 0., 2.))
+ self.assertEqual(item.getVisibleBounds(), (1.0, 3.0, 0.0, 2.0))
xaxis.setLimits(0.5, 2.5)
- self.assertEqual(item.getVisibleBounds(), (1, 2.5, 0., 2.))
+ self.assertEqual(item.getVisibleBounds(), (1, 2.5, 0.0, 2.0))
yaxis.setLimits(0.5, 1.5)
self.assertEqual(item.getVisibleBounds(), (1, 2.5, 0.5, 1.5))
@@ -350,11 +361,205 @@ class TestImageDataAggregated(PlotWidgetTestCase):
# Zoom-out
for i in range(4):
xmin, xmax = self.plot.getXAxis().getLimits()
- ymin, ymax = self.plot.getYAxis().getLimits()
+ ymin, ymax = self.plot.getYAxis().getLimits()
self.plot.setLimits(
- xmin - (xmax - xmin)/2,
- xmax + (xmax - xmin)/2,
- ymin - (ymax - ymin)/2,
- ymax + (ymax - ymin)/2,
+ xmin - (xmax - xmin) / 2,
+ xmax + (xmax - xmin) / 2,
+ ymin - (ymax - ymin) / 2,
+ ymax + (ymax - ymin) / 2,
)
self.qapp.processEvents()
+
+
+def testRegionOfInterestText():
+ roi = RegionOfInterest()
+
+ listener = SignalListener()
+ roi.sigItemChanged.connect(listener)
+
+ assert roi.getName() == roi.getText()
+
+ roi.setText("some text")
+ assert listener.arguments(argumentIndex=0) == [ItemChangedType.TEXT]
+ listener.clear()
+ assert roi.getText() == "some text"
+
+ roi.setName("new_name")
+ assert listener.arguments(argumentIndex=0) == [ItemChangedType.NAME]
+ listener.clear()
+ assert roi.getText() == "some text"
+
+ roi.setText(None)
+ assert listener.arguments(argumentIndex=0) == [ItemChangedType.TEXT]
+ listener.clear()
+ assert roi.getText() == "new_name"
+
+ roi.setName("even_newer_name")
+ assert listener.arguments(argumentIndex=0) == [
+ ItemChangedType.NAME,
+ ItemChangedType.TEXT,
+ ]
+ assert roi.getText() == "even_newer_name"
+
+
+def testPlotAddItemsWithoutLegend(plotWidget):
+ curve1 = items.Curve()
+ curve1.setData([0, 10], [0, 20])
+ plotWidget.addItem(curve1)
+
+ curve2 = items.Curve()
+ curve2.setData([0, -10], [0, -20])
+ plotWidget.addItem(curve2)
+
+ assert plotWidget.getItems() == (curve1, curve2)
+
+ datarange = plotWidget.getDataRange()
+ assert datarange.x == (-10, 10)
+ assert datarange.y == (-20, 20)
+
+ plotWidget.resetZoom()
+ assert plotWidget.getXAxis().getLimits() == (-10, 10)
+ assert plotWidget.getYAxis().getLimits() == (-20, 20)
+
+
+def testPlotWidgetAddCurve(plotWidget):
+ curve = plotWidget.addCurve(x=(0, 1), y=(1, 0), legend="test", symbol="s")
+ assert isinstance(curve, items.Curve)
+ assert numpy.array_equal(curve.getXData(copy=False), (0, 1))
+ assert numpy.array_equal(curve.getYData(copy=False), (1, 0))
+ assert curve.getName() == "test"
+ assert curve.getSymbol() == "s"
+
+ curveUpdated = plotWidget.addCurve(
+ x=(0, 1, 2), y=(1, 0, 1), legend="test", symbol="o"
+ )
+ assert curveUpdated is curve
+ assert numpy.array_equal(curveUpdated.getXData(copy=False), (0, 1, 2))
+ assert numpy.array_equal(curveUpdated.getYData(copy=False), (1, 0, 1))
+ assert curveUpdated.getName() == "test"
+ assert curveUpdated.getSymbol() == "o"
+
+
+def testPlotWidgetAddImage(plotWidget):
+ image = plotWidget.addImage(((0, 1), (2, 3)), legend="test")
+ assert isinstance(image, items.ImageData)
+ assert numpy.array_equal(image.getData(copy=False), ((0, 1), (2, 3)))
+ assert image.getName() == "test"
+
+ imageUpdated = plotWidget.addImage([(0, 1)], legend="test")
+ assert imageUpdated is image
+ assert numpy.array_equal(image.getData(copy=False), [(0, 1)])
+ assert image.getName() == "test"
+
+ # Update with a 1pixel RGB image
+ imageRgb = plotWidget.addImage([[(0.0, 0.0, 1.0)]], legend="test")
+ assert isinstance(imageRgb, items.ImageRgba)
+ assert numpy.array_equal(imageRgb.getData(copy=False), [[(0.0, 0.0, 1.0)]])
+ assert imageRgb.getName() == "test"
+
+ # Update with a 1pixel RGB image
+ imageRgbUpdated = plotWidget.addImage([[(1.0, 0.0, 0.0)]], legend="test")
+ assert imageRgbUpdated is imageRgb
+ assert numpy.array_equal(imageRgbUpdated.getData(copy=False), [[(1.0, 0.0, 0.0)]])
+ assert imageRgbUpdated.getName() == "test"
+
+
+def testPlotWidgetAddScatter(plotWidget):
+ scatter = plotWidget.addScatter(
+ x=(0, 1), y=(0, 1), value=(0, 1), legend="test", symbol="s"
+ )
+ assert isinstance(scatter, items.Scatter)
+ assert numpy.array_equal(scatter.getXData(copy=False), (0, 1))
+ assert numpy.array_equal(scatter.getYData(copy=False), (0, 1))
+ assert numpy.array_equal(scatter.getValueData(copy=False), (0, 1))
+ assert scatter.getName() == "test"
+ assert scatter.getSymbol() == "s"
+
+
+def testPlotWidgetAddHistogram(plotWidget):
+ histogram = plotWidget.addHistogram(
+ histogram=[1], edges=(0, 1), legend="test", fill=True
+ )
+ assert isinstance(histogram, items.Histogram)
+ assert numpy.array_equal(histogram.getBinEdgesData(copy=False), (0, 1))
+ assert numpy.array_equal(histogram.getValueData(copy=False), [1])
+ assert histogram.getName() == "test"
+ assert histogram.isFill()
+
+
+def testPlotWidgetAddMarker(plotWidget):
+ marker = plotWidget.addMarker(x=0, y=1, legend="test")
+ assert isinstance(marker, items.Marker)
+ assert marker.getPosition() == (0, 1)
+ assert marker.getName() == "test"
+ assert plotWidget.getItems() == (marker,)
+
+ xmarker = plotWidget.addXMarker(1, legend="test")
+ assert isinstance(xmarker, items.XMarker)
+ assert xmarker.getPosition() == (1, None)
+ assert xmarker.getName() == "test"
+ assert plotWidget.getItems() == (xmarker,)
+
+ ymarker = plotWidget.addYMarker(2, legend="test")
+ assert isinstance(ymarker, items.YMarker)
+ assert ymarker.getPosition() == (None, 2)
+ assert ymarker.getName() == "test"
+ assert plotWidget.getItems() == (ymarker,)
+
+
+def testPlotWidgetAddShape(plotWidget):
+ shape = plotWidget.addShape(
+ xdata=(0, 1), ydata=(0, 1), legend="test", shape="polygon"
+ )
+ assert isinstance(shape, items.Shape)
+ assert numpy.array_equal(shape.getPoints(copy=False), ((0, 0), (1, 1)))
+ assert shape.getName() == "test"
+ assert shape.getType() == "polygon"
+
+
+@pytest.mark.parametrize(
+ "linestyle",
+ (
+ "",
+ "-",
+ "--",
+ "-.",
+ ":",
+ (0.0, None),
+ (0.5, ()),
+ (0.0, (5.0, 5.0)),
+ (4.0, (8.0, 4.0, 4.0, 4.0)),
+ ),
+)
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testLineStyle(qapp_utils, plotWidget, linestyle):
+ """Test different line styles for LineMixIn items"""
+ plotWidget.setGraphTitle(f"Line style: {linestyle}")
+
+ curve = plotWidget.addCurve((0, 1), (0, 1), linestyle=linestyle)
+ assert curve.getLineStyle() == linestyle
+
+ histogram = plotWidget.addHistogram((0.25, 0.75, 0.25), (0.0, 0.33, 0.66, 1.0))
+ histogram.setLineStyle(linestyle)
+ assert histogram.getLineStyle() == linestyle
+
+ polylines = plotWidget.addShape(
+ (0, 1), (1, 0), shape="polylines", linestyle=linestyle
+ )
+ assert polylines.getLineStyle() == linestyle
+
+ rectangle = plotWidget.addShape(
+ (0.4, 0.6), (0.4, 0.6), shape="rectangle", linestyle=linestyle
+ )
+ assert rectangle.getLineStyle() == linestyle
+
+ xmarker = plotWidget.addXMarker(0.5)
+ xmarker.setLineStyle(linestyle)
+ assert xmarker.getLineStyle() == linestyle
+
+ ymarker = plotWidget.addYMarker(0.5)
+ ymarker.setLineStyle(linestyle)
+ assert ymarker.getLineStyle() == linestyle
+
+ plotWidget.replot()
+ qapp_utils.qWait(100)
diff --git a/src/silx/gui/plot/test/testLegendSelector.py b/src/silx/gui/plot/test/testLegendSelector.py
index c40875d..a1f000a 100644
--- a/src/silx/gui/plot/test/testLegendSelector.py
+++ b/src/silx/gui/plot/test/testLegendSelector.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
@@ -30,7 +29,6 @@ __date__ = "15/05/2017"
import logging
-import unittest
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt
@@ -45,6 +43,7 @@ class TestLegendSelector(TestCaseQt):
def testLegendSelector(self):
"""Test copied from __main__ of LegendSelector in PyMca"""
+
class Notifier(qt.QObject):
def __init__(self):
qt.QObject.__init__(self)
@@ -52,22 +51,31 @@ class TestLegendSelector(TestCaseQt):
def signalReceived(self, **kw):
obj = self.sender()
- _logger.info('NOTIFIER -- signal received\n\tsender: %s',
- str(obj))
+ _logger.info("NOTIFIER -- signal received\n\tsender: %s", str(obj))
notifier = Notifier()
- legends = ['Legend0',
- 'Legend1',
- 'Long Legend 2',
- 'Foo Legend 3',
- 'Even Longer Legend 4',
- 'Short Leg 5',
- 'Dot symbol 6',
- 'Comma symbol 7']
- colors = [qt.Qt.darkRed, qt.Qt.green, qt.Qt.yellow, qt.Qt.darkCyan,
- qt.Qt.blue, qt.Qt.darkBlue, qt.Qt.red, qt.Qt.darkYellow]
- symbols = ['o', 't', '+', 'x', 's', 'd', '.', ',']
+ legends = [
+ "Legend0",
+ "Legend1",
+ "Long Legend 2",
+ "Foo Legend 3",
+ "Even Longer Legend 4",
+ "Short Leg 5",
+ "Dot symbol 6",
+ "Comma symbol 7",
+ ]
+ colors = [
+ qt.Qt.darkRed,
+ qt.Qt.green,
+ qt.Qt.yellow,
+ qt.Qt.darkCyan,
+ qt.Qt.blue,
+ qt.Qt.darkBlue,
+ qt.Qt.red,
+ qt.Qt.darkYellow,
+ ]
+ symbols = ["o", "t", "+", "x", "s", "d", ".", ","]
win = LegendSelector.LegendListView()
# win = LegendListContextMenu()
@@ -78,9 +86,9 @@ class TestLegendSelector(TestCaseQt):
for _idx, (l, c, s) in enumerate(zip(legends, colors, symbols)):
ddict = {
- 'color': qt.QColor(c),
- 'linewidth': 4,
- 'symbol': s,
+ "color": qt.QColor(c),
+ "linewidth": 4,
+ "symbol": s,
}
legend = l
llist.append((legend, ddict))
@@ -117,14 +125,15 @@ class TestRenameCurveDialog(TestCaseQt):
def testDialog(self):
"""Create dialog, change name and press OK"""
self.dialog = LegendSelector.RenameCurveDialog(
- None, 'curve1', ['curve1', 'curve2', 'curve3'])
+ None, "curve1", ["curve1", "curve2", "curve3"]
+ )
self.dialog.open()
self.qWaitForWindowExposed(self.dialog)
- self.keyClicks(self.dialog.lineEdit, 'changed')
+ self.keyClicks(self.dialog.lineEdit, "changed")
self.mouseClick(self.dialog.okButton, qt.Qt.LeftButton)
self.qapp.processEvents()
ret = self.dialog.result()
self.assertEqual(ret, qt.QDialog.Accepted)
newName = self.dialog.getText()
- self.assertEqual(newName, 'curve1changed')
+ self.assertEqual(newName, "curve1changed")
del self.dialog
diff --git a/src/silx/gui/plot/test/testLimitConstraints.py b/src/silx/gui/plot/test/testLimitConstraints.py
index 0bd8e50..04a53e1 100644
--- a/src/silx/gui/plot/test/testLimitConstraints.py
+++ b/src/silx/gui/plot/test/testLimitConstraints.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/test/testMaskToolsWidget.py b/src/silx/gui/plot/test/testMaskToolsWidget.py
index 522ca51..1428687 100644
--- a/src/silx/gui/plot/test/testMaskToolsWidget.py
+++ b/src/silx/gui/plot/test/testMaskToolsWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
@@ -31,7 +30,6 @@ __date__ = "17/01/2018"
import logging
import os.path
-import unittest
import numpy
@@ -42,8 +40,6 @@ from silx.gui.utils.testutils import getQToolButtonFromAction
from silx.gui.plot import PlotWindow, MaskToolsWidget
from .utils import PlotWidgetTestCase
-import fabio
-
_logger = logging.getLogger(__name__)
@@ -56,7 +52,7 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def setUp(self):
super(TestMaskToolsWidget, self).setUp()
- self.widget = MaskToolsWidget.MaskToolsDockWidget(plot=self.plot, name='TEST')
+ self.widget = MaskToolsWidget.MaskToolsDockWidget(plot=self.plot, name="TEST")
self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, self.widget)
self.maskWidget = self.widget.widget()
@@ -67,10 +63,10 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def testEmptyPlot(self):
"""Empty plot, display MaskToolsDockWidget, toggle multiple masks"""
- self.maskWidget.setMultipleMasks('single')
+ self.maskWidget.setMultipleMasks("single")
self.qapp.processEvents()
- self.maskWidget.setMultipleMasks('exclusive')
+ self.maskWidget.setMultipleMasks("exclusive")
self.qapp.processEvents()
def _drag(self):
@@ -100,12 +96,14 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
x, y = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset),
- (x, y + offset)] # Close polygon
+ star = [
+ (x, y + offset),
+ (x - offset, y - offset),
+ (x + offset, y),
+ (x - offset, y),
+ (x + offset, y - offset),
+ (x, y + offset),
+ ] # Close polygon
self.mouseMove(plot, pos=(0, 0))
for pos in star:
@@ -122,28 +120,33 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
x, y = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset)]
+ star = [
+ (x, y + offset),
+ (x - offset, y - offset),
+ (x + offset, y),
+ (x - offset, y),
+ (x + offset, y - offset),
+ ]
self.mouseMove(plot, pos=(0, 0))
for start, end in zip(star[:-1], star[1:]):
- self.mouseMove(plot, pos=start)
- self.mousePress(plot, qt.Qt.LeftButton, pos=start)
- self.qapp.processEvents()
- self.mouseMove(plot, pos=end)
- self.qapp.processEvents()
- self.mouseRelease(plot, qt.Qt.LeftButton, pos=end)
- self.qapp.processEvents()
+ self.mouseMove(plot, pos=start)
+ self.mousePress(plot, qt.Qt.LeftButton, pos=start)
+ self.qapp.processEvents()
+ self.mouseMove(plot, pos=end)
+ self.qapp.processEvents()
+ self.mouseRelease(plot, qt.Qt.LeftButton, pos=end)
+ self.qapp.processEvents()
def _isMaskItemSync(self):
"""Check if masks from item and tools are sync or not"""
if self.maskWidget.isItemMaskUpdated():
- return numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(),
- self.plot.getActiveImage().getMaskData(copy=False)))
+ return numpy.all(
+ numpy.equal(
+ self.maskWidget.getSelectionMask(),
+ self.plot.getActiveImage().getMaskData(copy=False),
+ )
+ )
else:
return True
@@ -151,30 +154,36 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
"""Plot with an image: test MaskToolsWidget interactions"""
# Add and remove a image (this should enable/disable GUI + change mask)
- self.plot.addImage(numpy.random.random(1024**2).reshape(1024, 1024),
- legend='test')
+ self.plot.addImage(
+ numpy.random.random(1024**2).reshape(1024, 1024), legend="test"
+ )
self.qapp.processEvents()
- self.plot.remove('test', kind='image')
+ self.plot.remove("test", kind="image")
self.qapp.processEvents()
- tests = [((0, 0), (1, 1)),
- ((1000, 1000), (1, 1)),
- ((0, 0), (-1, -1)),
- ((1000, 1000), (-1, -1))]
+ tests = [
+ ((0, 0), (1, 1)),
+ ((1000, 1000), (1, 1)),
+ ((0, 0), (-1, -1)),
+ ((1000, 1000), (-1, -1)),
+ ]
for itemMaskUpdated in (False, True):
for origin, scale in tests:
with self.subTest(origin=origin, scale=scale):
self.maskWidget.setItemMaskUpdated(itemMaskUpdated)
- self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024),
- legend='test',
- origin=origin,
- scale=scale)
+ self.plot.addImage(
+ numpy.arange(1024**2).reshape(1024, 1024),
+ legend="test",
+ origin=origin,
+ scale=scale,
+ )
self.qapp.processEvents()
self.assertEqual(
- self.maskWidget.isItemMaskUpdated(), itemMaskUpdated)
+ self.maskWidget.isItemMaskUpdated(), itemMaskUpdated
+ )
# Test draw rectangle #
toolButton = getQToolButtonFromAction(self.maskWidget.rectAction)
@@ -186,7 +195,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drag()
self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# unmask same region
@@ -194,7 +204,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drag()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# Test draw polygon #
@@ -207,7 +218,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drawPolygon()
self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# unmask same region
@@ -215,7 +227,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drawPolygon()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# Test draw pencil #
@@ -231,7 +244,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drawPencil()
self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# unmask same region
@@ -239,7 +253,8 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drawPencil()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.assertTrue(self._isMaskItemSync())
# Test no draw tool #
@@ -251,8 +266,7 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def __loadSave(self, file_format):
"""Plot with an image: test MaskToolsWidget operations"""
- self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024),
- legend='test')
+ self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024), legend="test")
self.qapp.processEvents()
# Draw a polygon mask
@@ -265,16 +279,18 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertFalse(numpy.all(numpy.equal(ref_mask, 0)))
with temp_dir() as tmp:
- mask_filename = os.path.join(tmp, 'mask.' + file_format)
+ mask_filename = os.path.join(tmp, "mask." + file_format)
self.maskWidget.save(mask_filename, file_format)
self.maskWidget.resetSelectionMask()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.maskWidget.load(mask_filename)
- self.assertTrue(numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(), ref_mask)))
+ self.assertTrue(
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), ref_mask))
+ )
def testLoadSaveNpy(self):
self.__loadSave("npy")
@@ -283,8 +299,7 @@ class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.__loadSave("msk")
def testSigMaskChangedEmitted(self):
- self.plot.addImage(numpy.arange(512**2).reshape(512, 512),
- legend='test')
+ self.plot.addImage(numpy.arange(512**2).reshape(512, 512), legend="test")
self.plot.resetZoom()
self.qapp.processEvents()
diff --git a/src/silx/gui/plot/test/testPixelIntensityHistoAction.py b/src/silx/gui/plot/test/testPixelIntensityHistoAction.py
index 14a467d..7fd87e8 100644
--- a/src/silx/gui/plot/test/testPixelIntensityHistoAction.py
+++ b/src/silx/gui/plot/test/testPixelIntensityHistoAction.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
@@ -30,7 +29,6 @@ __date__ = "02/03/2018"
import numpy
-import unittest
from silx.utils.testutils import ParametricTestCase
from silx.gui.utils.testutils import TestCaseQt, getQToolButtonFromAction
@@ -54,7 +52,7 @@ class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
def testShowAndHide(self):
"""Simple test that the plot is showing and hiding when activating the
action"""
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
+ self.plotImage.addImage(self.image, origin=(0, 0), legend="sino")
self.plotImage.show()
histoAction = self.plotImage.getIntensityHistogramAction()
@@ -68,7 +66,7 @@ class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
self.assertTrue(histoAction.getHistogramWidget().isVisible())
# test the pixel intensity diagram is hiding
- self.qapp.setActiveWindow(self.plotImage)
+ self.plotImage.activateWindow()
self.qapp.processEvents()
self.mouseMove(button)
self.mouseClick(button, qt.Qt.LeftButton)
@@ -77,19 +75,25 @@ class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
def testImageFormatInput(self):
"""Test multiple type as image input"""
- typesToTest = [numpy.uint8, numpy.int8, numpy.int16, numpy.int32,
- numpy.float32, numpy.float64]
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
+ typesToTest = [
+ numpy.uint8,
+ numpy.int8,
+ numpy.int16,
+ numpy.int32,
+ numpy.float32,
+ numpy.float64,
+ ]
+ self.plotImage.addImage(self.image, origin=(0, 0), legend="sino")
self.plotImage.show()
- button = getQToolButtonFromAction(
- self.plotImage.getIntensityHistogramAction())
+ button = getQToolButtonFromAction(self.plotImage.getIntensityHistogramAction())
self.mouseMove(button)
self.mouseClick(button, qt.Qt.LeftButton)
self.qapp.processEvents()
for typeToTest in typesToTest:
with self.subTest(typeToTest=typeToTest):
- self.plotImage.addImage(self.image.astype(typeToTest),
- origin=(0, 0), legend='sino')
+ self.plotImage.addImage(
+ self.image.astype(typeToTest), origin=(0, 0), legend="sino"
+ )
def testScatter(self):
"""Test that an histogram from a scatter is displayed"""
@@ -137,7 +141,7 @@ class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
data1 = items[0].getValueData(copy=False)
# Set another item to the plot
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
+ self.plotImage.addImage(self.image, origin=(0, 0), legend="sino")
self.qapp.processEvents()
data2 = items[0].getValueData(copy=False)
diff --git a/src/silx/gui/plot/test/testPlotActions.py b/src/silx/gui/plot/test/testPlotActions.py
index f38e05b..9f56aad 100644
--- a/src/silx/gui/plot/test/testPlotActions.py
+++ b/src/silx/gui/plot/test/testPlotActions.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
@@ -41,17 +40,13 @@ import numpy
@pytest.fixture
def colormap1():
- colormap = Colormap(name='gray',
- vmin=10.0, vmax=20.0,
- normalization='linear')
+ colormap = Colormap(name="gray", vmin=10.0, vmax=20.0, normalization="linear")
yield colormap
@pytest.fixture
def colormap2():
- colormap = Colormap(name='red',
- vmin=10.0, vmax=20.0,
- normalization='linear')
+ colormap = Colormap(name="red", vmin=10.0, vmax=20.0, normalization="linear")
yield colormap
@@ -71,25 +66,25 @@ def test_action_active_colormap(qapp_utils, plot, colormap1, colormap2):
defaultColormap = plot.getDefaultColormap()
assert colormapDialog.getColormap() is defaultColormap
- plot.addImage(data=numpy.random.rand(10, 10), legend='img1',
- origin=(0, 0),
- colormap=colormap1)
- plot.setActiveImage('img1')
+ plot.addImage(
+ data=numpy.random.rand(10, 10), legend="img1", origin=(0, 0), colormap=colormap1
+ )
+ plot.setActiveImage("img1")
assert colormapDialog.getColormap() is colormap1
- plot.addImage(data=numpy.random.rand(10, 10), legend='img2',
- origin=(0, 0), colormap=colormap2)
- plot.addImage(data=numpy.random.rand(10, 10), legend='img3',
- origin=(0, 0))
+ plot.addImage(
+ data=numpy.random.rand(10, 10), legend="img2", origin=(0, 0), colormap=colormap2
+ )
+ plot.addImage(data=numpy.random.rand(10, 10), legend="img3", origin=(0, 0))
- plot.setActiveImage('img3')
+ plot.setActiveImage("img3")
assert colormapDialog.getColormap() is defaultColormap
plot.getActiveImage().setColormap(colormap2)
assert colormapDialog.getColormap() is colormap2
- plot.remove('img2')
- plot.remove('img3')
- plot.remove('img1')
+ plot.remove("img2")
+ plot.remove("img3")
+ plot.remove("img1")
assert colormapDialog.getColormap() is defaultColormap
@@ -101,10 +96,11 @@ def test_action_show_hide_colormap_dialog(qapp_utils, plot, colormap1):
assert not plot.getColormapAction().isChecked()
plot.getColormapAction()._actionTriggered(checked=True)
assert plot.getColormapAction().isChecked()
- plot.addImage(data=numpy.random.rand(10, 10), legend='img1',
- origin=(0, 0), colormap=colormap1)
- colormap1.setName('red')
+ plot.addImage(
+ data=numpy.random.rand(10, 10), legend="img1", origin=(0, 0), colormap=colormap1
+ )
+ colormap1.setName("red")
plot.getColormapAction()._actionTriggered()
- colormap1.setName('blue')
+ colormap1.setName("blue")
colormapDialog.close()
assert not plot.getColormapAction().isChecked()
diff --git a/src/silx/gui/plot/test/testPlotInteraction.py b/src/silx/gui/plot/test/testPlotInteraction.py
index fba364e..a97a694 100644
--- a/src/silx/gui/plot/test/testPlotInteraction.py
+++ b/src/silx/gui/plot/test/testPlotInteraction.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016=2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,9 +27,10 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "01/09/2017"
+import pytest
-import unittest
from silx.gui import qt
+from silx.gui.plot import PlotWidget
from .utils import PlotWidgetTestCase
@@ -79,82 +79,154 @@ class TestSelectPolygon(PlotWidgetTestCase):
def test(self):
"""Test draw polygons + events"""
- self.plot.sigInteractiveModeChanged.connect(
- self._interactionModeChanged)
+ self.plot.sigInteractiveModeChanged.connect(self._interactionModeChanged)
- self.plot.setInteractiveMode(
- 'draw', shape='polygon', label='test', source=self)
+ self.plot.setInteractiveMode("draw", shape="polygon", label="test", source=self)
interaction = self.plot.getInteractiveMode()
- self.assertEqual(interaction['mode'], 'draw')
- self.assertEqual(interaction['shape'], 'polygon')
+ self.assertEqual(interaction["mode"], "draw")
+ self.assertEqual(interaction["shape"], "polygon")
- self.plot.sigInteractiveModeChanged.disconnect(
- self._interactionModeChanged)
+ self.plot.sigInteractiveModeChanged.disconnect(self._interactionModeChanged)
plot = self.plot.getWidgetHandle()
xCenter, yCenter = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
# Star polygon
- star = [(xCenter, yCenter + offset),
- (xCenter - offset, yCenter - offset),
- (xCenter + offset, yCenter),
- (xCenter - offset, yCenter),
- (xCenter + offset, yCenter - offset),
- (xCenter, yCenter + offset)] # Close polygon
+ star = [
+ (xCenter, yCenter + offset),
+ (xCenter - offset, yCenter - offset),
+ (xCenter + offset, yCenter),
+ (xCenter - offset, yCenter),
+ (xCenter + offset, yCenter - offset),
+ (xCenter, yCenter + offset),
+ ] # Close polygon
# Draw while dumping signals
events = self._draw(star)
# Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 6)
+ drawEvents = [event for event in events if event["event"].startswith("drawing")]
+ self.assertEqual(drawEvents[-1]["event"], "drawingFinished")
+ self.assertEqual(len(drawEvents[-1]["points"]), 6)
# Large square
- largeSquare = [(xCenter - offset, yCenter - offset),
- (xCenter + offset, yCenter - offset),
- (xCenter + offset, yCenter + offset),
- (xCenter - offset, yCenter + offset),
- (xCenter - offset, yCenter - offset)] # Close polygon
+ largeSquare = [
+ (xCenter - offset, yCenter - offset),
+ (xCenter + offset, yCenter - offset),
+ (xCenter + offset, yCenter + offset),
+ (xCenter - offset, yCenter + offset),
+ (xCenter - offset, yCenter - offset),
+ ] # Close polygon
# Draw while dumping signals
events = self._draw(largeSquare)
# Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 5)
+ drawEvents = [event for event in events if event["event"].startswith("drawing")]
+ self.assertEqual(drawEvents[-1]["event"], "drawingFinished")
+ self.assertEqual(len(drawEvents[-1]["points"]), 5)
# Rectangle too thin along X: Some points are ignored
- thinRectX = [(xCenter, yCenter - offset),
- (xCenter, yCenter + offset),
- (xCenter + 1, yCenter + offset),
- (xCenter + 1, yCenter - offset)] # Close polygon
+ thinRectX = [
+ (xCenter, yCenter - offset),
+ (xCenter, yCenter + offset),
+ (xCenter + 1, yCenter + offset),
+ (xCenter + 1, yCenter - offset),
+ ] # Close polygon
# Draw while dumping signals
events = self._draw(thinRectX)
# Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 3)
+ drawEvents = [event for event in events if event["event"].startswith("drawing")]
+ self.assertEqual(drawEvents[-1]["event"], "drawingFinished")
+ self.assertEqual(len(drawEvents[-1]["points"]), 3)
# Rectangle too thin along Y: Some points are ignored
- thinRectY = [(xCenter - offset, yCenter),
- (xCenter + offset, yCenter),
- (xCenter + offset, yCenter + 1),
- (xCenter - offset, yCenter + 1)] # Close polygon
+ thinRectY = [
+ (xCenter - offset, yCenter),
+ (xCenter + offset, yCenter),
+ (xCenter + offset, yCenter + 1),
+ (xCenter - offset, yCenter + 1),
+ ] # Close polygon
# Draw while dumping signals
events = self._draw(thinRectY)
# Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 3)
+ drawEvents = [event for event in events if event["event"].startswith("drawing")]
+ self.assertEqual(drawEvents[-1]["event"], "drawingFinished")
+ self.assertEqual(len(drawEvents[-1]["points"]), 3)
+
+
+@pytest.mark.parametrize("scale", ["linear", "log"])
+@pytest.mark.parametrize("xaxis", [True, False])
+@pytest.mark.parametrize("yaxis", [True, False])
+@pytest.mark.parametrize("y2axis", [True, False])
+def testZoomEnabledAxes(qapp, qWidgetFactory, scale, xaxis, yaxis, y2axis):
+ """Test PlotInteraction.setZoomEnabledAxes effect on zoom interaction"""
+ plotWidget = qWidgetFactory(PlotWidget)
+ plotWidget.getXAxis().setScale(scale)
+ plotWidget.getYAxis("left").setScale(scale)
+ plotWidget.getYAxis("right").setScale(scale)
+ qapp.processEvents()
+
+ xLimits = plotWidget.getXAxis().getLimits()
+ yLimits = plotWidget.getYAxis("left").getLimits()
+ y2Limits = plotWidget.getYAxis("right").getLimits()
+
+ interaction = plotWidget.interaction()
+
+ assert interaction.getZoomEnabledAxes() == (True, True, True)
+
+ enabledAxes = xaxis, yaxis, y2axis
+ interaction.setZoomEnabledAxes(*enabledAxes)
+ assert interaction.getZoomEnabledAxes() == enabledAxes
+
+ cx, cy = plotWidget.width() // 2, plotWidget.height() // 2
+ plotWidget.onMouseWheel(cx, cy, 10)
+ qapp.processEvents()
+
+ xZoomed = plotWidget.getXAxis().getLimits() != xLimits
+ yZoomed = plotWidget.getYAxis("left").getLimits() != yLimits
+ y2Zoomed = plotWidget.getYAxis("right").getLimits() != y2Limits
+
+ assert xZoomed == enabledAxes[0]
+ assert yZoomed == enabledAxes[1]
+ assert y2Zoomed == enabledAxes[2]
+
+
+@pytest.mark.parametrize("scale", ["linear", "log"])
+@pytest.mark.parametrize("zoomOnWheel", [True, False])
+def testZoomOnWheelEnabled(qapp, qWidgetFactory, zoomOnWheel, scale):
+ """Test PlotInteraction.setZoomOnWheelEnabled"""
+ plotWidget = qWidgetFactory(PlotWidget)
+ plotWidget.getXAxis().setScale(scale)
+ plotWidget.getYAxis("left").setScale(scale)
+ plotWidget.getYAxis("right").setScale(scale)
+ qapp.processEvents()
+
+ xLimits = plotWidget.getXAxis().getLimits()
+ yLimits = plotWidget.getYAxis("left").getLimits()
+ y2Limits = plotWidget.getYAxis("right").getLimits()
+
+ interaction = plotWidget.interaction()
+
+ assert interaction.isZoomOnWheelEnabled()
+
+ interaction.setZoomOnWheelEnabled(zoomOnWheel)
+ assert interaction.isZoomOnWheelEnabled() == zoomOnWheel
+
+ cx, cy = plotWidget.width() // 2, plotWidget.height() // 2
+ plotWidget.onMouseWheel(cx, cy, 10)
+ qapp.processEvents()
+
+ xZoomed = plotWidget.getXAxis().getLimits() != xLimits
+ yZoomed = plotWidget.getYAxis("left").getLimits() != yLimits
+ y2Zoomed = plotWidget.getYAxis("right").getLimits() != y2Limits
+
+ assert xZoomed == zoomOnWheel
+ assert yZoomed == zoomOnWheel
+ assert y2Zoomed == zoomOnWheel
diff --git a/src/silx/gui/plot/test/testPlotWidget.py b/src/silx/gui/plot/test/testPlotWidget.py
index f6e108d..842e880 100755
--- a/src/silx/gui/plot/test/testPlotWidget.py
+++ b/src/silx/gui/plot/test/testPlotWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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,7 +29,6 @@ __date__ = "03/01/2019"
import unittest
-import logging
import numpy
import pytest
@@ -40,7 +38,6 @@ from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
from silx.gui.plot import PlotWidget
-from silx.gui.plot.items.curve import CurveStyle
from silx.gui.plot.items import BoundingRect, XAxisExtent, YAxisExtent, Axis
from silx.gui.colors import Colormap
@@ -50,16 +47,12 @@ from .utils import PlotWidgetTestCase
SIZE = 1024
"""Size of the test image"""
-DATA_2D = numpy.arange(SIZE ** 2).reshape(SIZE, SIZE)
+DATA_2D = numpy.arange(SIZE**2).reshape(SIZE, SIZE)
"""Image data set"""
-logger = logging.getLogger(__name__)
-
-
class TestSpecialBackend(PlotWidgetTestCase, ParametricTestCase):
-
- def __init__(self, methodName='runTest', backend=None):
+ def __init__(self, methodName="runTest", backend=None):
TestCaseQt.__init__(self, methodName=methodName)
self.__backend = backend
@@ -80,7 +73,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
def testSetTitleLabels(self):
"""Set title and axes labels"""
- title, xlabel, ylabel = 'the title', 'x label', 'y label'
+ title, xlabel, ylabel = "the title", "x label", "y label"
self.plot.setGraphTitle(title)
self.plot.getXAxis().setLabel(xlabel)
self.plot.getYAxis().setLabel(ylabel)
@@ -90,10 +83,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertEqual(self.plot.getXAxis().getLabel(), xlabel)
self.assertEqual(self.plot.getYAxis().getLabel(), ylabel)
- def _checkLimits(self,
- expectedXLim=None,
- expectedYLim=None,
- expectedRatio=None):
+ def _checkLimits(self, expectedXLim=None, expectedYLim=None, expectedRatio=None):
"""Assert that limits are as expected"""
xlim = self.plot.getXAxis().getLimits()
ylim = self.plot.getYAxis().getLimits()
@@ -106,8 +96,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertEqual(expectedYLim, ylim)
if expectedRatio is not None:
- self.assertTrue(
- numpy.allclose(expectedRatio, ratio, atol=0.01))
+ self.assertTrue(numpy.allclose(expectedRatio, ratio, atol=0.01))
def testChangeLimitsWithAspectRatio(self):
self.plot.setKeepDataAspectRatio()
@@ -116,15 +105,15 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
ylim = self.plot.getYAxis().getLimits()
defaultRatio = abs(xlim[1] - xlim[0]) / abs(ylim[1] - ylim[0])
- self.plot.getXAxis().setLimits(1., 10.)
- self._checkLimits(expectedXLim=(1., 10.), expectedRatio=defaultRatio)
+ self.plot.getXAxis().setLimits(1.0, 10.0)
+ self._checkLimits(expectedXLim=(1.0, 10.0), expectedRatio=defaultRatio)
self.qapp.processEvents()
- self._checkLimits(expectedXLim=(1., 10.), expectedRatio=defaultRatio)
+ self._checkLimits(expectedXLim=(1.0, 10.0), expectedRatio=defaultRatio)
- self.plot.getYAxis().setLimits(1., 10.)
- self._checkLimits(expectedYLim=(1., 10.), expectedRatio=defaultRatio)
+ self.plot.getYAxis().setLimits(1.0, 10.0)
+ self._checkLimits(expectedYLim=(1.0, 10.0), expectedRatio=defaultRatio)
self.qapp.processEvents()
- self._checkLimits(expectedYLim=(1., 10.), expectedRatio=defaultRatio)
+ self._checkLimits(expectedYLim=(1.0, 10.0), expectedRatio=defaultRatio)
def testResizeWidget(self):
"""Test resizing the widget and receiving limitsChanged events"""
@@ -136,8 +125,8 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
ylim = self.plot.getYAxis().getLimits()
listener = SignalListener()
- self.plot.getXAxis().sigLimitsChanged.connect(listener.partial('x'))
- self.plot.getYAxis().sigLimitsChanged.connect(listener.partial('y'))
+ self.plot.getXAxis().sigLimitsChanged.connect(listener.partial("x"))
+ self.plot.getYAxis().sigLimitsChanged.connect(listener.partial("y"))
# Resize without aspect ratio
self.plot.resize(200, 300)
@@ -160,17 +149,17 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
def testAddRemoveItemSignals(self):
"""Test sigItemAdded and sigItemAboutToBeRemoved"""
listener = SignalListener()
- self.plot.sigItemAdded.connect(listener.partial('add'))
- self.plot.sigItemAboutToBeRemoved.connect(listener.partial('remove'))
+ self.plot.sigItemAdded.connect(listener.partial("add"))
+ self.plot.sigItemAboutToBeRemoved.connect(listener.partial("remove"))
- self.plot.addCurve((1, 2, 3), (3, 2, 1), legend='curve')
+ self.plot.addCurve((1, 2, 3), (3, 2, 1), legend="curve")
self.assertEqual(listener.callCount(), 1)
- curve = self.plot.getCurve('curve')
- self.plot.remove('curve')
+ curve = self.plot.getCurve("curve")
+ self.plot.remove("curve")
self.assertEqual(listener.callCount(), 2)
- self.assertEqual(listener.arguments(callIndex=0), ('add', curve))
- self.assertEqual(listener.arguments(callIndex=1), ('remove', curve))
+ self.assertEqual(listener.arguments(callIndex=0), ("add", curve))
+ self.assertEqual(listener.arguments(callIndex=1), ("remove", curve))
def testGetItems(self):
"""Test getItems method"""
@@ -184,7 +173,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.plot.addMarker(*marker_pos)
marker_x = 6
self.plot.addXMarker(marker_x)
- self.plot.addShape((0, 5), (2, 10), shape='rectangle')
+ self.plot.addShape((0, 5), (2, 10), shape="rectangle")
items = self.plot.getItems()
self.assertEqual(len(items), 6)
@@ -193,7 +182,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertTrue(numpy.all(numpy.equal(items[2].getXData(), scatter_x)))
self.assertTrue(numpy.all(numpy.equal(items[3].getPosition(), marker_pos)))
self.assertTrue(numpy.all(numpy.equal(items[4].getPosition()[0], marker_x)))
- self.assertEqual(items[5].getType(), 'rectangle')
+ self.assertEqual(items[5].getType(), "rectangle")
def testRemoveDiscardItem(self):
"""Test removeItem and discardItem"""
@@ -233,7 +222,7 @@ class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
# Back to default
- self.plot.setBackgroundColor('white')
+ self.plot.setBackgroundColor("white")
self.plot.setDataBackgroundColor(None)
color = self.plot.getBackgroundColor()
self.assertTrue(color.isValid())
@@ -249,116 +238,132 @@ class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
def setUp(self):
super(TestPlotImage, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
def testPlotColormapTemperature(self):
- self.plot.setGraphTitle('Temp. Linear')
+ self.plot.setGraphTitle("Temp. Linear")
- colormap = Colormap(name='temperature',
- normalization='linear',
- vmin=None,
- vmax=None)
+ colormap = Colormap(
+ name="temperature", normalization="linear", vmin=None, vmax=None
+ )
self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
def testPlotColormapGray(self):
self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Gray Linear')
+ self.plot.setGraphTitle("Gray Linear")
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
+ colormap = Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
def testPlotColormapTemperatureLog(self):
- self.plot.setGraphTitle('Temp. Log')
+ self.plot.setGraphTitle("Temp. Log")
- colormap = Colormap(name='temperature',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
+ colormap = Colormap(
+ name="temperature", normalization=Colormap.LOGARITHM, vmin=None, vmax=None
+ )
self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
def testPlotRgbRgba(self):
self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('RGB + RGBA')
+ self.plot.setGraphTitle("RGB + RGBA")
rgb = numpy.array(
- (((0, 0, 0), (128, 0, 0), (255, 0, 0)),
- ((0, 128, 0), (0, 128, 128), (0, 128, 255))),
- dtype=numpy.uint8)
+ (
+ ((0, 0, 0), (128, 0, 0), (255, 0, 0)),
+ ((0, 128, 0), (0, 128, 128), (0, 128, 255)),
+ ),
+ dtype=numpy.uint8,
+ )
- self.plot.addImage(rgb, legend="rgb_uint8",
- origin=(0, 0), scale=(1, 1),
- resetzoom=False)
+ self.plot.addImage(
+ rgb, legend="rgb_uint8", origin=(0, 0), scale=(1, 1), resetzoom=False
+ )
rgb = numpy.array(
- (((0, 0, 0), (32768, 0, 0), (65535, 0, 0)),
- ((0, 32768, 0), (0, 32768, 32768), (0, 32768, 65535))),
- dtype=numpy.uint16)
+ (
+ ((0, 0, 0), (32768, 0, 0), (65535, 0, 0)),
+ ((0, 32768, 0), (0, 32768, 32768), (0, 32768, 65535)),
+ ),
+ dtype=numpy.uint16,
+ )
- self.plot.addImage(rgb, legend="rgb_uint16",
- origin=(3, 2), scale=(2, 2),
- resetzoom=False)
+ self.plot.addImage(
+ rgb, legend="rgb_uint16", origin=(3, 2), scale=(2, 2), resetzoom=False
+ )
rgba = numpy.array(
- (((0, 0, 0, .5), (.5, 0, 0, 1), (1, 0, 0, .5)),
- ((0, .5, 0, 1), (0, .5, .5, 1), (0, 1, 1, .5))),
- dtype=numpy.float32)
+ (
+ ((0, 0, 0, 0.5), (0.5, 0, 0, 1), (1, 0, 0, 0.5)),
+ ((0, 0.5, 0, 1), (0, 0.5, 0.5, 1), (0, 1, 1, 0.5)),
+ ),
+ dtype=numpy.float32,
+ )
- self.plot.addImage(rgba, legend="rgba_float32",
- origin=(9, 6), scale=(1, 1),
- resetzoom=False)
+ self.plot.addImage(
+ rgba, legend="rgba_float32", origin=(9, 6), scale=(1, 1), resetzoom=False
+ )
self.plot.resetZoom()
def testPlotColormapCustom(self):
self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Custom colormap')
-
- colormap = Colormap(name=None,
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None,
- colors=((0., 0., 0.), (1., 0., 0.),
- (0., 1., 0.), (0., 0., 1.)))
- self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap,
- resetzoom=False)
-
- colormap = Colormap(name=None,
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None,
- colors=numpy.array(
- ((0, 0, 0, 0), (0, 0, 0, 128),
- (128, 128, 128, 128), (255, 255, 255, 255)),
- dtype=numpy.uint8))
- self.plot.addImage(DATA_2D, legend="image 2", colormap=colormap,
- origin=(DATA_2D.shape[0], 0),
- resetzoom=False)
+ self.plot.setGraphTitle("Custom colormap")
+
+ colormap = Colormap(
+ name=None,
+ normalization=Colormap.LINEAR,
+ vmin=None,
+ vmax=None,
+ colors=((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
+ )
+ self.plot.addImage(
+ DATA_2D, legend="image 1", colormap=colormap, resetzoom=False
+ )
+
+ colormap = Colormap(
+ name=None,
+ normalization=Colormap.LINEAR,
+ vmin=None,
+ vmax=None,
+ colors=numpy.array(
+ (
+ (0, 0, 0, 0),
+ (0, 0, 0, 128),
+ (128, 128, 128, 128),
+ (255, 255, 255, 255),
+ ),
+ dtype=numpy.uint8,
+ ),
+ )
+ self.plot.addImage(
+ DATA_2D,
+ legend="image 2",
+ colormap=colormap,
+ origin=(DATA_2D.shape[0], 0),
+ resetzoom=False,
+ )
self.plot.resetZoom()
def testPlotColormapNaNColor(self):
self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Colormap with NaN color')
+ self.plot.setGraphTitle("Colormap with NaN color")
colormap = Colormap()
- colormap.setNaNColor('red')
+ colormap.setNaNColor("red")
self.assertEqual(colormap.getNaNColor(), qt.QColor(255, 0, 0))
data = DATA_2D.astype(numpy.float32)
- data[len(data)//2:] = numpy.nan
- self.plot.addImage(data, legend="image 1", colormap=colormap,
- resetzoom=False)
+ data[len(data) // 2 :] = numpy.nan
+ self.plot.addImage(data, legend="image 1", colormap=colormap, resetzoom=False)
self.plot.resetZoom()
- colormap.setNaNColor((0., 1., 0., 1.))
+ colormap.setNaNColor((0.0, 1.0, 0.0, 1.0))
self.assertEqual(colormap.getNaNColor(), qt.QColor(0, 255, 0))
self.qapp.processEvents()
def testImageOriginScale(self):
"""Test of image with different origin and scale"""
- self.plot.setGraphTitle('origin and scale')
+ self.plot.setGraphTitle("origin and scale")
tests = [ # (origin, scale)
((10, 20), (1, 1)),
@@ -368,7 +373,7 @@ class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
(100, 2),
(-100, (1, 1)),
((10, 20), 2),
- ]
+ ]
for origin, scale in tests:
with self.subTest(origin=origin, scale=scale):
@@ -409,31 +414,30 @@ class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
def testPlotColormapDictAPI(self):
"""Test that the addImage API using a colormap dictionary is still
working"""
- self.plot.setGraphTitle('Temp. Log')
+ self.plot.setGraphTitle("Temp. Log")
colormap = {
- 'name': 'temperature',
- 'normalization': 'log',
- 'vmin': None,
- 'vmax': None
+ "name": "temperature",
+ "normalization": "log",
+ "vmin": None,
+ "vmax": None,
}
self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
def testPlotComplexImage(self):
"""Test that a complex image is displayed as its absolute value."""
data = numpy.linspace(1, 1j, 100).reshape(10, 10)
- self.plot.addImage(data, legend='complex')
+ self.plot.addImage(data, legend="complex")
image = self.plot.getActiveImage()
retrievedData = image.getData(copy=False)
- self.assertTrue(
- numpy.all(numpy.equal(retrievedData, numpy.absolute(data))))
+ self.assertTrue(numpy.all(numpy.equal(retrievedData, numpy.absolute(data))))
def testPlotBooleanImage(self):
"""Test that a boolean image is displayed and converted to int8."""
data = numpy.zeros((10, 10), dtype=bool)
data[::2, ::2] = True
- self.plot.addImage(data, legend='boolean')
+ self.plot.addImage(data, legend="boolean")
image = self.plot.getActiveImage()
retrievedData = image.getData(copy=False)
@@ -444,7 +448,7 @@ class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
"""Test with an alpha image layer"""
data = numpy.random.random((10, 10))
alpha = numpy.linspace(0, 1, 100).reshape(10, 10)
- self.plot.addImage(data, legend='image')
+ self.plot.addImage(data, legend="image")
image = self.plot.getActiveImage()
image.setData(data, alpha=alpha)
self.qapp.processEvents()
@@ -462,19 +466,19 @@ class TestPlotCurve(PlotWidgetTestCase):
def setUp(self):
super(TestPlotCurve, self).setUp()
- self.plot.setGraphTitle('Curve')
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.setGraphTitle("Curve")
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
self.plot.setActiveCurveHandling(False)
def testPlotCurveInfinite(self):
"""Test plot curves with not finite data"""
tests = {
- 'y all not finite': ([0, 1, 2], [numpy.inf, numpy.nan, -numpy.inf]),
- 'x all not finite': ([numpy.inf, numpy.nan, -numpy.inf], [0, 1, 2]),
- 'x some inf': ([0, numpy.inf, 2], [0, 1, 2]),
- 'y some inf': ([0, 1, 2], [0, numpy.inf, 2])
+ "y all not finite": ([0, 1, 2], [numpy.inf, numpy.nan, -numpy.inf]),
+ "x all not finite": ([numpy.inf, numpy.nan, -numpy.inf], [0, 1, 2]),
+ "x some inf": ([0, numpy.inf, 2], [0, 1, 2]),
+ "y some inf": ([0, 1, 2], [0, numpy.inf, 2]),
}
for name, args in tests.items():
with self.subTest(name):
@@ -484,65 +488,111 @@ class TestPlotCurve(PlotWidgetTestCase):
self.plot.clear()
def testPlotCurveColorFloat(self):
- color = numpy.array(numpy.random.random(3 * 1000),
- dtype=numpy.float32).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 1",
- replace=False, resetzoom=False,
- color=color,
- linestyle="", symbol="s")
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ color = numpy.array(numpy.random.random(3 * 1000), dtype=numpy.float32).reshape(
+ 1000, 3
+ )
+
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 1",
+ replace=False,
+ resetzoom=False,
+ color=color,
+ linestyle="",
+ symbol="s",
+ )
+ self.plot.addCurve(
+ self.xData2,
+ self.yData2,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
self.plot.resetZoom()
def testPlotCurveColorByte(self):
- color = numpy.array(255 * numpy.random.random(3 * 1000),
- dtype=numpy.uint8).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 1",
- replace=False, resetzoom=False,
- color=color,
- linestyle="", symbol="s")
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ color = numpy.array(
+ 255 * numpy.random.random(3 * 1000), dtype=numpy.uint8
+ ).reshape(1000, 3)
+
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 1",
+ replace=False,
+ resetzoom=False,
+ color=color,
+ linestyle="",
+ symbol="s",
+ )
+ self.plot.addCurve(
+ self.xData2,
+ self.yData2,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
self.plot.resetZoom()
def testPlotCurveColors(self):
- color = numpy.array(numpy.random.random(3 * 1000),
- dtype=numpy.float32).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=color, linestyle="-", symbol='o')
+ color = numpy.array(numpy.random.random(3 * 1000), dtype=numpy.float32).reshape(
+ 1000, 3
+ )
+
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color=color,
+ linestyle="-",
+ symbol="o",
+ )
self.plot.resetZoom()
# Test updating color array
# From array to array
newColors = numpy.ones((len(self.xData), 3), dtype=numpy.float32)
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=newColors, symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color=newColors,
+ symbol="o",
+ )
# Array to single color
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ symbol="o",
+ )
# single color to array
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=color, symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve 2",
+ replace=False,
+ resetzoom=False,
+ color=color,
+ symbol="o",
+ )
def testPlotBaselineNumpyArray(self):
"""simple test of the API with baseline as a numpy array"""
@@ -551,8 +601,9 @@ class TestPlotCurve(PlotWidgetTestCase):
y = numpy.arange(-4, 6, step=0.1) + my_sin
baseline = y - 1.0
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=baseline)
+ self.plot.addCurve(
+ x=x, y=y, color="grey", legend="curve1", fill=True, baseline=baseline
+ )
def testPlotBaselineScalar(self):
"""simple test of the API with baseline as an int"""
@@ -560,8 +611,9 @@ class TestPlotCurve(PlotWidgetTestCase):
my_sin = numpy.sin(x)
y = numpy.arange(-4, 6, step=0.1) + my_sin
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=0)
+ self.plot.addCurve(
+ x=x, y=y, color="grey", legend="curve1", fill=True, baseline=0
+ )
def testPlotBaselineList(self):
"""simple test of the API with baseline as an int"""
@@ -569,35 +621,70 @@ class TestPlotCurve(PlotWidgetTestCase):
my_sin = numpy.sin(x)
y = numpy.arange(-4, 6, step=0.1) + my_sin
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=list(range(0, 100, 1)))
+ self.plot.addCurve(
+ x=x,
+ y=y,
+ color="grey",
+ legend="curve1",
+ fill=True,
+ baseline=list(range(0, 100, 1)),
+ )
def testPlotCurveComplexData(self):
"""Test curve with complex data"""
- data = numpy.arange(100.) + 1j
+ data = numpy.arange(100.0) + 1j
self.plot.addCurve(x=data, y=data, xerror=data, yerror=data)
+ def testPlotCurveGapColor(self):
+ """Test dashed curve with gap color"""
+ data = numpy.arange(100)
+ self.plot.addCurve(
+ x=data, y=data, legend="curve1", linestyle="--", color="blue"
+ )
+ curve = self.plot.getCurve("curve1")
+ assert curve.getLineGapColor() is None
+ curve.setLineGapColor("red")
+ assert curve.getLineGapColor() == (1.0, 0.0, 0.0, 1.0)
+
class TestPlotHistogram(PlotWidgetTestCase):
"""Basic tests for add Histogram"""
+
def setUp(self):
super(TestPlotHistogram, self).setUp()
self.edges = numpy.arange(0, 10, step=1)
self.histogram = numpy.random.random(len(self.edges))
def testPlot(self):
- self.plot.addHistogram(histogram=self.histogram,
- edges=self.edges,
- legend='histogram1')
+ self.plot.addHistogram(
+ histogram=self.histogram, edges=self.edges, legend="histogram1"
+ )
def testPlotBaseline(self):
- self.plot.addHistogram(histogram=self.histogram,
- edges=self.edges,
- legend='histogram1',
- color='blue',
- baseline=-2,
- z=2,
- fill=True)
+ self.plot.addHistogram(
+ histogram=self.histogram,
+ edges=self.edges,
+ legend="histogram1",
+ color="blue",
+ baseline=-2,
+ z=2,
+ fill=True,
+ )
+
+ def testPlotGapColor(self):
+ """Test dashed histogram with gap color"""
+ data = numpy.arange(100)
+ self.plot.addHistogram(
+ histogram=self.histogram,
+ edges=self.edges,
+ legend="histogram1",
+ color="blue",
+ )
+ histogram = self.plot.getItems()[0]
+ assert histogram.getLineGapColor() is None
+ histogram.setLineGapColor("red")
+ assert histogram.getLineGapColor() == (1.0, 0.0, 0.0, 1.0)
+ histogram.setLineStyle(":")
class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
@@ -612,9 +699,8 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
def testScatterComplexData(self):
"""Test scatter item with complex data"""
- data = numpy.arange(100.) + 1j
- self.plot.addScatter(
- x=data, y=data, value=data, xerror=data, yerror=data)
+ data = numpy.arange(100.0) + 1j
+ self.plot.addScatter(x=data, y=data, value=data, xerror=data, yerror=data)
self.plot.resetZoom()
def testScatterVisualization(self):
@@ -624,16 +710,18 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
scatter = self.plot.getItems()[0]
- for visualization in ('solid',
- 'points',
- 'regular_grid',
- 'irregular_grid',
- 'binned_statistic',
- scatter.Visualization.SOLID,
- scatter.Visualization.POINTS,
- scatter.Visualization.REGULAR_GRID,
- scatter.Visualization.IRREGULAR_GRID,
- scatter.Visualization.BINNED_STATISTIC):
+ for visualization in (
+ "solid",
+ "points",
+ "regular_grid",
+ "irregular_grid",
+ "binned_statistic",
+ scatter.Visualization.SOLID,
+ scatter.Visualization.POINTS,
+ scatter.Visualization.REGULAR_GRID,
+ scatter.Visualization.IRREGULAR_GRID,
+ scatter.Visualization.BINNED_STATISTIC,
+ ):
with self.subTest(visualization=visualization):
scatter.setVisualization(visualization)
self.qapp.processEvents()
@@ -641,28 +729,30 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
def testGridVisualization(self):
"""Test regular and irregular grid mode with different points"""
points = { # name: (x, y, order)
- 'single point': ((1.,), (1.,), 'row'),
- 'horizontal line': ((0, 1, 2), (0, 0, 0), 'row'),
- 'horizontal line backward': ((2, 1, 0), (0, 0, 0), 'row'),
- 'vertical line': ((0, 0, 0), (0, 1, 2), 'row'),
- 'vertical line backward': ((0, 0, 0), (2, 1, 0), 'row'),
- 'grid fast x, +x +y': ((0, 1, 2, 0, 1, 2), (0, 0, 0, 1, 1, 1), 'row'),
- 'grid fast x, +x -y': ((0, 1, 2, 0, 1, 2), (1, 1, 1, 0, 0, 0), 'row'),
- 'grid fast x, -x -y': ((2, 1, 0, 2, 1, 0), (1, 1, 1, 0, 0, 0), 'row'),
- 'grid fast x, -x +y': ((2, 1, 0, 2, 1, 0), (0, 0, 0, 1, 1, 1), 'row'),
- 'grid fast y, +x +y': ((0, 0, 0, 1, 1, 1), (0, 1, 2, 0, 1, 2), 'column'),
- 'grid fast y, +x -y': ((0, 0, 0, 1, 1, 1), (2, 1, 0, 2, 1, 0), 'column'),
- 'grid fast y, -x -y': ((1, 1, 1, 0, 0, 0), (2, 1, 0, 2, 1, 0), 'column'),
- 'grid fast y, -x +y': ((1, 1, 1, 0, 0, 0), (0, 1, 2, 0, 1, 2), 'column'),
- }
+ "single point": ((1.0,), (1.0,), "row"),
+ "horizontal line": ((0, 1, 2), (0, 0, 0), "row"),
+ "horizontal line backward": ((2, 1, 0), (0, 0, 0), "row"),
+ "vertical line": ((0, 0, 0), (0, 1, 2), "row"),
+ "vertical line backward": ((0, 0, 0), (2, 1, 0), "row"),
+ "grid fast x, +x +y": ((0, 1, 2, 0, 1, 2), (0, 0, 0, 1, 1, 1), "row"),
+ "grid fast x, +x -y": ((0, 1, 2, 0, 1, 2), (1, 1, 1, 0, 0, 0), "row"),
+ "grid fast x, -x -y": ((2, 1, 0, 2, 1, 0), (1, 1, 1, 0, 0, 0), "row"),
+ "grid fast x, -x +y": ((2, 1, 0, 2, 1, 0), (0, 0, 0, 1, 1, 1), "row"),
+ "grid fast y, +x +y": ((0, 0, 0, 1, 1, 1), (0, 1, 2, 0, 1, 2), "column"),
+ "grid fast y, +x -y": ((0, 0, 0, 1, 1, 1), (2, 1, 0, 2, 1, 0), "column"),
+ "grid fast y, -x -y": ((1, 1, 1, 0, 0, 0), (2, 1, 0, 2, 1, 0), "column"),
+ "grid fast y, -x +y": ((1, 1, 1, 0, 0, 0), (0, 1, 2, 0, 1, 2), "column"),
+ }
self.plot.addScatter((), (), ())
scatter = self.plot.getItems()[0]
self.qapp.processEvents()
- for visualization in (scatter.Visualization.REGULAR_GRID,
- scatter.Visualization.IRREGULAR_GRID):
+ for visualization in (
+ scatter.Visualization.REGULAR_GRID,
+ scatter.Visualization.IRREGULAR_GRID,
+ ):
scatter.setVisualization(visualization)
self.assertIs(scatter.getVisualization(), visualization)
@@ -674,16 +764,19 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
order = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_MAJOR_ORDER)
+ scatter.VisualizationParameter.GRID_MAJOR_ORDER
+ )
self.assertEqual(ref_order, order)
ref_bounds = (x[0], y[0]), (x[-1], y[-1])
bounds = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_BOUNDS)
+ scatter.VisualizationParameter.GRID_BOUNDS
+ )
self.assertEqual(ref_bounds, bounds)
shape = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_SHAPE)
+ scatter.VisualizationParameter.GRID_SHAPE
+ )
self.plot.getXAxis().setLimits(numpy.min(x) - 1, numpy.max(x) + 1)
self.plot.getYAxis().setLimits(numpy.min(y) - 1, numpy.max(y) + 1)
@@ -701,12 +794,15 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
self.plot.addScatter((), (), ())
scatter = self.plot.getItems()[0]
scatter.setVisualization(scatter.Visualization.BINNED_STATISTIC)
- self.assertIs(scatter.getVisualization(),
- scatter.Visualization.BINNED_STATISTIC)
+ self.assertIs(
+ scatter.getVisualization(), scatter.Visualization.BINNED_STATISTIC
+ )
self.assertEqual(
scatter.getVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION),
- 'mean')
+ scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ "mean",
+ )
self.qapp.processEvents()
@@ -717,15 +813,17 @@ class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
scatter.setData(*numpy.random.random(3000).reshape(3, -1))
self.qapp.processEvents()
- for reduction in ('count', 'sum', 'mean'):
+ for reduction in ("count", "sum", "mean"):
with self.subTest(reduction=reduction):
scatter.setVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
- reduction)
+ scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION, reduction
+ )
self.assertEqual(
scatter.getVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION),
- reduction)
+ scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION
+ ),
+ reduction,
+ )
self.qapp.processEvents()
@@ -735,23 +833,23 @@ class TestPlotMarker(PlotWidgetTestCase):
def setUp(self):
super(TestPlotMarker, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
self.plot.getXAxis().setAutoScale(False)
self.plot.getYAxis().setAutoScale(False)
self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(0., 100., -100., 100.)
+ self.plot.setLimits(0.0, 100.0, -100.0, 100.0)
def testPlotMarkerX(self):
- self.plot.setGraphTitle('Markers X')
+ self.plot.setGraphTitle("Markers X")
markers = [
- (10., 'blue', False, False),
- (20., 'red', False, False),
- (40., 'green', True, False),
- (60., 'gray', True, True),
- (80., 'black', False, True),
+ (10.0, "blue", False, False),
+ (20.0, "red", False, False),
+ (40.0, "green", True, False),
+ (60.0, "gray", True, True),
+ (80.0, "black", False, True),
]
for x, color, select, drag in markers:
@@ -764,14 +862,14 @@ class TestPlotMarker(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerY(self):
- self.plot.setGraphTitle('Markers Y')
+ self.plot.setGraphTitle("Markers Y")
markers = [
- (-50., 'blue', False, False),
- (-30., 'red', False, False),
- (0., 'green', True, False),
- (10., 'gray', True, True),
- (80., 'black', False, True),
+ (-50.0, "blue", False, False),
+ (-30.0, "red", False, False),
+ (0.0, "green", True, False),
+ (10.0, "gray", True, True),
+ (80.0, "black", False, True),
]
for y, color, select, drag in markers:
@@ -784,14 +882,14 @@ class TestPlotMarker(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerPt(self):
- self.plot.setGraphTitle('Markers Pt')
+ self.plot.setGraphTitle("Markers Pt")
markers = [
- (10., -50., 'blue', False, False),
- (40., -30., 'red', False, False),
- (50., 0., 'green', True, False),
- (50., 20., 'gray', True, True),
- (70., 50., 'black', False, True),
+ (10.0, -50.0, "blue", False, False),
+ (40.0, -30.0, "red", False, False),
+ (50.0, 0.0, "green", True, False),
+ (50.0, 20.0, "gray", True, True),
+ (70.0, 50.0, "black", False, True),
]
for x, y, color, select, drag in markers:
name = "{0},{1}".format(x, y)
@@ -804,52 +902,45 @@ class TestPlotMarker(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerWithoutLegend(self):
- self.plot.setGraphTitle('Markers without legend')
+ self.plot.setGraphTitle("Markers without legend")
self.plot.getYAxis().setInverted(True)
# Markers without legend
self.plot.addMarker(10, 10)
self.plot.addMarker(10, 20)
- self.plot.addMarker(40, 50, text='test', symbol=None)
- self.plot.addMarker(40, 50, text='test', symbol='+')
+ self.plot.addMarker(40, 50, text="test", symbol=None)
+ self.plot.addMarker(40, 50, text="test", symbol="+")
self.plot.addXMarker(25)
self.plot.addXMarker(35)
- self.plot.addXMarker(45, text='test')
+ self.plot.addXMarker(45, text="test")
self.plot.addYMarker(55)
self.plot.addYMarker(65)
- self.plot.addYMarker(75, text='test')
+ self.plot.addYMarker(75, text="test")
self.plot.resetZoom()
def testPlotMarkerYAxis(self):
# Check only the API
- legend = self.plot.addMarker(10, 10)
- item = self.plot._getMarker(legend)
+ item = self.plot.addMarker(10, 10)
self.assertEqual(item.getYAxis(), "left")
- legend = self.plot.addMarker(10, 10, yaxis="right")
- item = self.plot._getMarker(legend)
+ item = self.plot.addMarker(10, 10, yaxis="right")
self.assertEqual(item.getYAxis(), "right")
- legend = self.plot.addMarker(10, 10, yaxis="left")
- item = self.plot._getMarker(legend)
+ item = self.plot.addMarker(10, 10, yaxis="left")
self.assertEqual(item.getYAxis(), "left")
- legend = self.plot.addXMarker(10, yaxis="right")
- item = self.plot._getMarker(legend)
+ item = self.plot.addXMarker(10, yaxis="right")
self.assertEqual(item.getYAxis(), "right")
- legend = self.plot.addXMarker(10, yaxis="left")
- item = self.plot._getMarker(legend)
+ item = self.plot.addXMarker(10, yaxis="left")
self.assertEqual(item.getYAxis(), "left")
- legend = self.plot.addYMarker(10, yaxis="right")
- item = self.plot._getMarker(legend)
+ item = self.plot.addYMarker(10, yaxis="right")
self.assertEqual(item.getYAxis(), "right")
- legend = self.plot.addYMarker(10, yaxis="left")
- item = self.plot._getMarker(legend)
+ item = self.plot.addYMarker(10, yaxis="left")
self.assertEqual(item.getYAxis(), "left")
self.plot.resetZoom()
@@ -857,39 +948,72 @@ class TestPlotMarker(PlotWidgetTestCase):
# TestPlotItem ################################################################
+
class TestPlotItem(PlotWidgetTestCase):
"""Basic tests for addItem."""
# Polygon coordinates and color
POLYGONS = [ # legend, x coords, y coords, color
- ('triangle', numpy.array((10, 30, 50)),
- numpy.array((55, 70, 55)), 'red'),
- ('square', numpy.array((10, 10, 50, 50)),
- numpy.array((10, 50, 50, 10)), 'green'),
- ('star', numpy.array((60, 70, 80, 60, 80)),
- numpy.array((25, 50, 25, 40, 40)), 'blue'),
- ('2 triangles-simple',
- numpy.array((90., 95., 100., numpy.nan, 90., 95., 100.)),
- numpy.array((25., 5., 25., numpy.nan, 30., 50., 30.)),
- 'pink'),
- ('2 triangles-extra NaN',
- numpy.array((numpy.nan, 90., 95., 100., numpy.nan, 0., 90., 95., 100., numpy.nan)),
- numpy.array((0., 55., 70., 55., numpy.nan, numpy.nan, 75., 90., 75., numpy.nan)),
- 'black'),
+ ("triangle", numpy.array((10, 30, 50)), numpy.array((55, 70, 55)), "red"),
+ (
+ "square",
+ numpy.array((10, 10, 50, 50)),
+ numpy.array((10, 50, 50, 10)),
+ "green",
+ ),
+ (
+ "star",
+ numpy.array((60, 70, 80, 60, 80)),
+ numpy.array((25, 50, 25, 40, 40)),
+ "blue",
+ ),
+ (
+ "2 triangles-simple",
+ numpy.array((90.0, 95.0, 100.0, numpy.nan, 90.0, 95.0, 100.0)),
+ numpy.array((25.0, 5.0, 25.0, numpy.nan, 30.0, 50.0, 30.0)),
+ "pink",
+ ),
+ (
+ "2 triangles-extra NaN",
+ numpy.array(
+ (
+ numpy.nan,
+ 90.0,
+ 95.0,
+ 100.0,
+ numpy.nan,
+ 0.0,
+ 90.0,
+ 95.0,
+ 100.0,
+ numpy.nan,
+ )
+ ),
+ numpy.array(
+ (
+ 0.0,
+ 55.0,
+ 70.0,
+ 55.0,
+ numpy.nan,
+ numpy.nan,
+ 75.0,
+ 90.0,
+ 75.0,
+ numpy.nan,
+ )
+ ),
+ "black",
+ ),
]
# Rectangle coordinantes and color
RECTANGLES = [ # legend, x coords, y coords, color
- ('square 1', numpy.array((1., 10.)),
- numpy.array((1., 10.)), 'red'),
- ('square 2', numpy.array((10., 20.)),
- numpy.array((10., 20.)), 'green'),
- ('square 3', numpy.array((20., 30.)),
- numpy.array((20., 30.)), 'blue'),
- ('rect 1', numpy.array((1., 30.)),
- numpy.array((35., 40.)), 'black'),
- ('line h', numpy.array((1., 30.)),
- numpy.array((45., 45.)), 'darkRed'),
+ ("square 1", numpy.array((1.0, 10.0)), numpy.array((1.0, 10.0)), "red"),
+ ("square 2", numpy.array((10.0, 20.0)), numpy.array((10.0, 20.0)), "green"),
+ ("square 3", numpy.array((20.0, 30.0)), numpy.array((20.0, 30.0)), "blue"),
+ ("rect 1", numpy.array((1.0, 30.0)), numpy.array((35.0, 40.0)), "black"),
+ ("line h", numpy.array((1.0, 30.0)), numpy.array((45.0, 45.0)), "darkRed"),
]
SCALES = Axis.LINEAR, Axis.LOGARITHMIC
@@ -897,12 +1021,12 @@ class TestPlotItem(PlotWidgetTestCase):
def setUp(self):
super(TestPlotItem, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
self.plot.getXAxis().setAutoScale(False)
self.plot.getYAxis().setAutoScale(False)
self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(0., 100., -100., 100.)
+ self.plot.setLimits(0.0, 100.0, -100.0, 100.0)
def testPlotItemPolygonFill(self):
for scale in self.SCALES:
@@ -910,12 +1034,19 @@ class TestPlotItem(PlotWidgetTestCase):
self.plot.clear()
self.plot.getXAxis().setScale(scale)
self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Item Fill %s' % scale)
+ self.plot.setGraphTitle("Item Fill %s" % scale)
for legend, xList, yList, color in self.POLYGONS:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False, linestyle='--',
- shape="polygon", fill=True, color=color)
+ self.plot.addShape(
+ xList,
+ yList,
+ legend=legend,
+ replace=False,
+ linestyle="--",
+ shape="polygon",
+ fill=True,
+ color=color,
+ )
self.plot.resetZoom()
def testPlotItemPolygonNoFill(self):
@@ -924,12 +1055,19 @@ class TestPlotItem(PlotWidgetTestCase):
self.plot.clear()
self.plot.getXAxis().setScale(scale)
self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Item No Fill %s' % scale)
+ self.plot.setGraphTitle("Item No Fill %s" % scale)
for legend, xList, yList, color in self.POLYGONS:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False, linestyle='--',
- shape="polygon", fill=False, color=color)
+ self.plot.addShape(
+ xList,
+ yList,
+ legend=legend,
+ replace=False,
+ linestyle="--",
+ shape="polygon",
+ fill=False,
+ color=color,
+ )
self.plot.resetZoom()
def testPlotItemRectangleFill(self):
@@ -938,12 +1076,18 @@ class TestPlotItem(PlotWidgetTestCase):
self.plot.clear()
self.plot.getXAxis().setScale(scale)
self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Rectangle Fill %s' % scale)
+ self.plot.setGraphTitle("Rectangle Fill %s" % scale)
for legend, xList, yList, color in self.RECTANGLES:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=True, color=color)
+ self.plot.addShape(
+ xList,
+ yList,
+ legend=legend,
+ replace=False,
+ shape="rectangle",
+ fill=True,
+ color=color,
+ )
self.plot.resetZoom()
def testPlotItemRectangleNoFill(self):
@@ -952,230 +1096,44 @@ class TestPlotItem(PlotWidgetTestCase):
self.plot.clear()
self.plot.getXAxis().setScale(scale)
self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Rectangle No Fill %s' % scale)
+ self.plot.setGraphTitle("Rectangle No Fill %s" % scale)
for legend, xList, yList, color in self.RECTANGLES:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=False, color=color)
+ self.plot.addShape(
+ xList,
+ yList,
+ legend=legend,
+ replace=False,
+ shape="rectangle",
+ fill=False,
+ color=color,
+ )
self.plot.resetZoom()
-class TestPlotActiveCurveImage(PlotWidgetTestCase):
- """Basic tests for active curve and image handling"""
- xData = numpy.arange(1000)
- yData = -500 + 100 * numpy.sin(xData)
- xData2 = xData + 1000
- yData2 = xData - 1000 + 200 * numpy.random.random(1000)
-
- def tearDown(self):
- self.plot.setActiveCurveHandling(False)
- super(TestPlotActiveCurveImage, self).tearDown()
-
- def testActiveCurveAndLabels(self):
- # Active curve handling off, no label change
- self.plot.setActiveCurveHandling(False)
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
- self.plot.addCurve((1, 2), (1, 2))
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.addCurve((1, 2), (2, 3), xlabel='x1', ylabel='y1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- # Active curve handling on, label changes
- self.plot.setActiveCurveHandling(True)
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
-
- # labels changed as active curve
- self.plot.addCurve((1, 2), (1, 2), legend='1',
- xlabel='x1', ylabel='y1')
- self.plot.setActiveCurve('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels not changed as not active curve
- self.plot.addCurve((1, 2), (2, 3), legend='2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels changed
- self.plot.setActiveCurve('2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.setActiveCurve('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- def testPlotActiveCurveSelectionMode(self):
- self.plot.clear()
- self.plot.setActiveCurveHandling(True)
- legend = "curve 1"
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
-
- # active curve should be None
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
-
- # active curve should be None when None is set as active curve
- self.plot.setActiveCurve(legend)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
- self.plot.setActiveCurve(None)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, None)
-
- # testing it automatically toggles if there is only one
- self.plot.setActiveCurveSelectionMode("legacy")
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
-
- # active curve should not change when None set as active curve
- self.assertEqual(self.plot.getActiveCurveSelectionMode(), "legacy")
- self.plot.setActiveCurve(None)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
-
- # situation where no curve is active
- self.plot.clear()
- self.plot.setActiveCurveHandling(True)
- self.assertEqual(self.plot.getActiveCurveSelectionMode(), "atmostone")
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- color="red")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
- self.plot.setActiveCurveSelectionMode("legacy")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
-
- # the first curve added should be active
- self.plot.clear()
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), legend)
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- color="red")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), legend)
-
- def testActiveCurveStyle(self):
- """Test change of active curve style"""
- self.plot.setActiveCurveHandling(True)
- self.plot.setActiveCurveStyle(color='black')
- style = self.plot.getActiveCurveStyle()
- self.assertEqual(style.getColor(), (0., 0., 0., 1.))
- self.assertIsNone(style.getLineStyle())
- self.assertIsNone(style.getLineWidth())
- self.assertIsNone(style.getSymbol())
- self.assertIsNone(style.getSymbolSize())
-
- self.plot.addCurve(x=self.xData, y=self.yData, legend="curve1")
- curve = self.plot.getCurve("curve1")
- curve.setColor('blue')
- curve.setLineStyle('-')
- curve.setLineWidth(1)
- curve.setSymbol('o')
- curve.setSymbolSize(5)
-
- # Check default current style
- defaultStyle = curve.getCurrentStyle()
- self.assertEqual(defaultStyle, CurveStyle(color='blue',
- linestyle='-',
- linewidth=1,
- symbol='o',
- symbolsize=5))
-
- # Activate curve with highlight color=black
- self.plot.setActiveCurve("curve1")
- style = curve.getCurrentStyle()
- self.assertEqual(style.getColor(), (0., 0., 0., 1.))
- self.assertEqual(style.getLineStyle(), '-')
- self.assertEqual(style.getLineWidth(), 1)
- self.assertEqual(style.getSymbol(), 'o')
- self.assertEqual(style.getSymbolSize(), 5)
-
- # Change highlight to linewidth=2
- self.plot.setActiveCurveStyle(linewidth=2)
- style = curve.getCurrentStyle()
- self.assertEqual(style.getColor(), (0., 0., 1., 1.))
- self.assertEqual(style.getLineStyle(), '-')
- self.assertEqual(style.getLineWidth(), 2)
- self.assertEqual(style.getSymbol(), 'o')
- self.assertEqual(style.getSymbolSize(), 5)
-
- self.plot.setActiveCurve(None)
- self.assertEqual(curve.getCurrentStyle(), defaultStyle)
-
- def testActiveImageAndLabels(self):
- # Active image handling always on, no API for toggling it
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
-
- # labels changed as active curve
- self.plot.addImage(numpy.arange(100).reshape(10, 10),
- legend='1', xlabel='x1', ylabel='y1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels not changed as not active curve
- self.plot.addImage(numpy.arange(100).reshape(10, 10),
- legend='2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels changed
- self.plot.setActiveImage('2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.setActiveImage('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
-
##############################################################################
# Log
##############################################################################
+
class TestPlotEmptyLog(PlotWidgetTestCase):
"""Basic tests for log plot"""
+
def testEmptyPlotTitleLabelsLog(self):
- self.plot.setGraphTitle('Empty Log Log')
- self.plot.getXAxis().setLabel('X')
- self.plot.getYAxis().setLabel('Y')
+ self.plot.setGraphTitle("Empty Log Log")
+ self.plot.getXAxis().setLabel("X")
+ self.plot.getYAxis().setLabel("Y")
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
self.plot.resetZoom()
class TestPlotAxes(TestCaseQt, ParametricTestCase):
-
# Test data
xData = numpy.arange(1, 10)
- yData = xData ** 2
+ yData = xData**2
- def __init__(self, methodName='runTest', backend=None):
+ def __init__(self, methodName="runTest", backend=None):
unittest.TestCase.__init__(self, methodName)
self.__backend = backend
@@ -1235,7 +1193,7 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
with self.subTest():
if setter is not None:
if not isinstance(value, tuple):
- value = (value, )
+ value = (value,)
setter(*value)
if getter is not None:
self.assertEqual(getter(), expected)
@@ -1325,22 +1283,34 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(self.plot.isYAxisInverted(), False)
def testLogXWithData(self):
- self.plot.setGraphTitle('Curve X: Log Y: Linear')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.setGraphTitle("Curve X: Log Y: Linear")
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
axis = self.plot.getXAxis()
axis.setScale(axis.LOGARITHMIC)
self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
def testLogYWithData(self):
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.setGraphTitle("Curve X: Linear Y: Log")
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
axis = self.plot.getYAxis()
axis.setScale(axis.LOGARITHMIC)
@@ -1349,11 +1319,17 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
def testLogYRightWithData(self):
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.setGraphTitle("Curve X: Linear Y: Log")
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
axis = self.plot.getYAxis(axis="right")
axis.setScale(axis.LOGARITHMIC)
@@ -1362,36 +1338,58 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
def testLimitsChanged_setLimits(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
self.plot.getXAxis().sigLimitsChanged.connect(listener.partial(axis="x"))
self.plot.getYAxis().sigLimitsChanged.connect(listener.partial(axis="y"))
- self.plot.getYAxis(axis="right").sigLimitsChanged.connect(listener.partial(axis="y2"))
+ self.plot.getYAxis(axis="right").sigLimitsChanged.connect(
+ listener.partial(axis="y2")
+ )
self.plot.setLimits(0, 1, 0, 1, 0, 1)
# at least one event per axis
self.assertEqual(len(set(listener.karguments(argumentName="axis"))), 3)
def testLimitsChanged_resetZoom(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
self.plot.getXAxis().sigLimitsChanged.connect(listener.partial(axis="x"))
self.plot.getYAxis().sigLimitsChanged.connect(listener.partial(axis="y"))
- self.plot.getYAxis(axis="right").sigLimitsChanged.connect(listener.partial(axis="y2"))
+ self.plot.getYAxis(axis="right").sigLimitsChanged.connect(
+ listener.partial(axis="y2")
+ )
self.plot.resetZoom()
# at least one event per axis
self.assertEqual(len(set(listener.karguments(argumentName="axis"))), 3)
def testLimitsChanged_setXLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
axis = self.plot.getXAxis()
axis.sigLimitsChanged.connect(listener)
@@ -1401,10 +1399,16 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(axis.getLimits(), (20.0, 30.0))
def testLimitsChanged_setYLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
axis = self.plot.getYAxis()
axis.sigLimitsChanged.connect(listener)
@@ -1414,10 +1418,16 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.assertEqual(axis.getLimits(), (20.0, 30.0))
def testLimitsChanged_setYRightLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=False,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
listener = SignalListener()
axis = self.plot.getYAxis(axis="right")
axis.sigLimitsChanged.connect(listener)
@@ -1482,9 +1492,9 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
self.qWaitForWindowExposed(self.plot)
margins = self.plot.getAxesMargins()
- self.assertEqual(margins, (.15, .1, .1, .15))
+ self.assertEqual(margins, (0.15, 0.1, 0.1, 0.15))
- for margins in ((0., 0., 0., 0.), (.15, .1, .1, .15)):
+ for margins in ((0.0, 0.0, 0.0, 0.0), (0.15, 0.1, 0.1, 0.15)):
with self.subTest(margins=margins):
self.plot.setAxesMargins(*margins)
self.qapp.processEvents()
@@ -1539,18 +1549,21 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
def testAxisExtent(self):
"""Test XAxisExtent and yAxisExtent"""
- for cls, axis in ((XAxisExtent, self.plot.getXAxis()),
- (YAxisExtent, self.plot.getYAxis())):
- for range_, logRange in (((2, 3), (2, 3)),
- ((-2, -1), (1, 100)),
- ((-1, 3), (3. * 0.9, 3. * 1.1))):
+ for cls, axis in (
+ (XAxisExtent, self.plot.getXAxis()),
+ (YAxisExtent, self.plot.getYAxis()),
+ ):
+ for range_, logRange in (
+ ((2, 3), (2, 3)),
+ ((-2, -1), (1, 100)),
+ ((-1, 3), (3.0 * 0.9, 3.0 * 1.1)),
+ ):
extent = cls()
extent.setRange(*range_)
self.plot.addItem(extent)
for isLog, plotRange in ((False, range_), (True, logRange)):
- with self.subTest(
- cls=cls.__name__, range=range_, isLog=isLog):
+ with self.subTest(cls=cls.__name__, range=range_, isLog=isLog):
axis._setLogarithmic(isLog)
self.plot.resetZoom()
self.qapp.processEvents()
@@ -1565,9 +1578,7 @@ class TestPlotAxes(TestCaseQt, ParametricTestCase):
for scale in ("linear", "log"):
xaxis.setScale(scale)
yaxis.setScale(scale)
- for limits in ((1e300, 1e308),
- (-1e308, 1e308),
- (1e-300, 2e-300)):
+ for limits in ((1e300, 1e308), (-1e308, 1e308), (1e-300, 2e-300)):
with self.subTest(scale=scale, limits=limits):
xaxis.setLimits(*limits)
self.qapp.processEvents()
@@ -1582,44 +1593,62 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
# Test data
xData = numpy.arange(1000) + 1
- yData = xData ** 2
+ yData = xData**2
def _setLabels(self):
- self.plot.getXAxis().setLabel('X')
- self.plot.getYAxis().setLabel('X * X')
+ self.plot.getXAxis().setLabel("X")
+ self.plot.getYAxis().setLabel("X * X")
def testPlotCurveLogX(self):
self._setLabels()
self.plot.getXAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('Curve X: Log Y: Linear')
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.setGraphTitle("Curve X: Log Y: Linear")
+
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
def testPlotCurveLogY(self):
self._setLabels()
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
+ self.plot.setGraphTitle("Curve X: Linear Y: Log")
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
def testPlotCurveLogXY(self):
self._setLabels()
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('Curve X: Log Y: Log')
+ self.plot.setGraphTitle("Curve X: Log Y: Log")
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend="curve",
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
def testPlotCurveErrorLogXY(self):
self.plot.getXAxis()._setLogarithmic(True)
@@ -1630,27 +1659,54 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
errors[::2] = self.xData[::2] + 1
tests = [ # name, xerror, yerror
- ('xerror=3', 3, None),
- ('xerror=N array', errors, None),
- ('xerror=Nx1 array', errors.reshape(len(errors), 1), None),
- ('xerror=2xN array', numpy.array((errors, errors)), None),
- ('yerror=6', None, 6),
- ('yerror=N array', None, errors ** 2),
- ('yerror=Nx1 array', None, (errors ** 2).reshape(len(errors), 1)),
- ('yerror=2xN array', None, numpy.array((errors, errors)) ** 2),
+ ("xerror=3", 3, None),
+ ("xerror=N array", errors, None),
+ ("xerror=Nx1 array", errors.reshape(len(errors), 1), None),
+ ("xerror=2xN array", numpy.array((errors, errors)), None),
+ ("yerror=6", None, 6),
+ ("yerror=N array", None, errors**2),
+ ("yerror=Nx1 array", None, (errors**2).reshape(len(errors), 1)),
+ ("yerror=2xN array", None, numpy.array((errors, errors)) ** 2),
]
for name, xError, yError in tests:
with self.subTest(name):
self.plot.setGraphTitle(name)
- self.plot.addCurve(self.xData, self.yData,
- legend=name,
- xerror=xError, yerror=yError,
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
+ self.plot.addCurve(
+ self.xData,
+ self.yData,
+ legend=name,
+ xerror=xError,
+ yerror=yError,
+ replace=False,
+ resetzoom=True,
+ color="green",
+ linestyle="-",
+ symbol="o",
+ )
self.qapp.processEvents()
+ if xError is None:
+ dataMin, dataMax = numpy.min(self.xData), numpy.max(self.xData)
+ else:
+ xMinusError = self.xData - numpy.atleast_2d(xError)[0]
+ dataMin = numpy.min(xMinusError[xMinusError > 0])
+ xPlusError = self.xData + numpy.atleast_2d(xError)[-1]
+ dataMax = numpy.max(xPlusError[xPlusError > 0])
+ plotMin, plotMax = self.plot.getXAxis().getLimits()
+ assert numpy.allclose((dataMin, dataMax), (plotMin, plotMax))
+
+ if yError is None:
+ dataMin, dataMax = numpy.min(self.yData), numpy.max(self.yData)
+ else:
+ yMinusError = self.yData - numpy.atleast_2d(yError)[0]
+ dataMin = numpy.min(yMinusError[yMinusError > 0])
+ yPlusError = self.yData + numpy.atleast_2d(yError)[-1]
+ dataMax = numpy.max(yPlusError[yPlusError > 0])
+ plotMin, plotMax = self.plot.getYAxis().getLimits()
+ assert numpy.allclose((dataMin, dataMax), (plotMin, plotMax))
+
self.plot.clear()
self.plot.resetZoom()
self.qapp.processEvents()
@@ -1659,12 +1715,12 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
"""Add a curve with negative data and toggle log axis"""
arange = numpy.arange(1000) + 1
tests = [ # name, xData, yData
- ('x>0, some negative y', arange, arange - 500),
- ('x>0, y<0', arange, -arange),
- ('some negative x, y>0', arange - 500, arange),
- ('x<0, y>0', -arange, arange),
- ('some negative x and y', arange - 500, arange - 500),
- ('x<0, y<0', -arange, -arange),
+ ("x>0, some negative y", arange, arange - 500),
+ ("x>0, y<0", arange, -arange),
+ ("some negative x, y>0", arange - 500, arange),
+ ("x<0, y>0", -arange, arange),
+ ("some negative x and y", arange - 500, arange - 500),
+ ("x<0, y<0", -arange, -arange),
]
for name, xData, yData in tests:
@@ -1686,54 +1742,65 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
yLim = self.plot.getYAxis().getLimits()
positives = xData > 0
if numpy.any(positives):
- self.assertTrue(numpy.allclose(
- xLim, (min(xData[positives]), max(xData[positives]))))
- self.assertEqual(
- yLim, (min(yData[positives]), max(yData[positives])))
+ self.assertTrue(
+ numpy.allclose(
+ xLim, (min(xData[positives]), max(xData[positives]))
+ )
+ )
else: # No positive x in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
+ self.assertEqual(xLim, (1.0, 100.0))
+ self.assertEqual(yLim, (min(yData), max(yData)))
# x axis and y axis log
+ previousXLim = self.plot.getXAxis().getLimits()
+ previousYLim = self.plot.getYAxis().getLimits()
self.plot.getYAxis()._setLogarithmic(True)
self.qapp.processEvents()
xLim = self.plot.getXAxis().getLimits()
yLim = self.plot.getYAxis().getLimits()
+
+ self.assertEqual(xLim, previousXLim)
positives = numpy.logical_and(xData > 0, yData > 0)
- if numpy.any(positives):
- self.assertTrue(numpy.allclose(
- xLim, (min(xData[positives]), max(xData[positives]))))
- self.assertTrue(numpy.allclose(
- yLim, (min(yData[positives]), max(yData[positives]))))
+ if previousYLim[0] > 0:
+ self.assertEqual(yLim, previousYLim)
+ elif numpy.any(positives):
+ expectedLimits = min(yData[positives]), max(yData[positives])
+ self.assertTrue(
+ numpy.allclose(yLim, expectedLimits),
+ f"{yLim} != {expectedLimits}",
+ )
else: # No positive x and y in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
+ self.assertEqual(yLim, (1.0, 100.0))
# y axis log
+ previousXLim = self.plot.getXAxis().getLimits()
self.plot.getXAxis()._setLogarithmic(False)
self.qapp.processEvents()
xLim = self.plot.getXAxis().getLimits()
yLim = self.plot.getYAxis().getLimits()
+ self.assertEqual(xLim, previousXLim)
positives = yData > 0
if numpy.any(positives):
- self.assertEqual(
- xLim, (min(xData[positives]), max(xData[positives])))
- self.assertTrue(numpy.allclose(
- yLim, (min(yData[positives]), max(yData[positives]))))
+ self.assertTrue(
+ numpy.allclose(
+ yLim, (min(yData[positives]), max(yData[positives]))
+ )
+ )
else: # No positive y in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
+ self.assertEqual(yLim, (1.0, 100.0))
# no log axis
+ previousXLim = self.plot.getXAxis().getLimits()
+ previousYLim = self.plot.getYAxis().getLimits()
self.plot.getYAxis()._setLogarithmic(False)
self.qapp.processEvents()
xLim = self.plot.getXAxis().getLimits()
- self.assertEqual(xLim, (min(xData), max(xData)))
+ self.assertEqual(xLim, previousXLim)
yLim = self.plot.getYAxis().getLimits()
- self.assertEqual(yLim, (min(yData), max(yData)))
+ self.assertEqual(yLim, previousYLim)
self.plot.clear()
self.plot.resetZoom()
@@ -1746,71 +1813,83 @@ class TestPlotImageLog(PlotWidgetTestCase):
def setUp(self):
super(TestPlotImageLog, self).setUp()
- self.plot.getXAxis().setLabel('Columns')
- self.plot.getYAxis().setLabel('Rows')
+ self.plot.getXAxis().setLabel("Columns")
+ self.plot.getYAxis().setLabel("Rows")
def testPlotColormapGrayLogX(self):
self.plot.getXAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Log Y: Linear')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
+ self.plot.setGraphTitle("CMap X: Log Y: Linear")
+
+ colormap = Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
+ self.plot.addImage(
+ DATA_2D,
+ legend="image 1",
+ origin=(1.0, 1.0),
+ scale=(1.0, 1.0),
+ resetzoom=False,
+ colormap=colormap,
+ )
self.plot.resetZoom()
def testPlotColormapGrayLogY(self):
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Linear Y: Log')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
+ self.plot.setGraphTitle("CMap X: Linear Y: Log")
+
+ colormap = Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
+ self.plot.addImage(
+ DATA_2D,
+ legend="image 1",
+ origin=(1.0, 1.0),
+ scale=(1.0, 1.0),
+ resetzoom=False,
+ colormap=colormap,
+ )
self.plot.resetZoom()
def testPlotColormapGrayLogXY(self):
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Log Y: Log')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
+ self.plot.setGraphTitle("CMap X: Log Y: Log")
+
+ colormap = Colormap(name="gray", normalization="linear", vmin=None, vmax=None)
+ self.plot.addImage(
+ DATA_2D,
+ legend="image 1",
+ origin=(1.0, 1.0),
+ scale=(1.0, 1.0),
+ resetzoom=False,
+ colormap=colormap,
+ )
self.plot.resetZoom()
def testPlotRgbRgbaLogXY(self):
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('RGB + RGBA X: Log Y: Log')
+ self.plot.setGraphTitle("RGB + RGBA X: Log Y: Log")
rgb = numpy.array(
- (((0, 0, 0), (128, 0, 0), (255, 0, 0)),
- ((0, 128, 0), (0, 128, 128), (0, 128, 256))),
- dtype=numpy.uint8)
+ (
+ ((0, 0, 0), (128, 0, 0), (255, 0, 0)),
+ ((0, 128, 0), (0, 128, 128), (0, 128, 255)),
+ ),
+ dtype=numpy.uint8,
+ )
- self.plot.addImage(rgb, legend="rgb",
- origin=(1, 1), scale=(10, 10),
- resetzoom=False)
+ self.plot.addImage(
+ rgb, legend="rgb", origin=(1, 1), scale=(10, 10), resetzoom=False
+ )
rgba = numpy.array(
- (((0, 0, 0, .5), (.5, 0, 0, 1), (1, 0, 0, .5)),
- ((0, .5, 0, 1), (0, .5, .5, 1), (0, 1, 1, .5))),
- dtype=numpy.float32)
-
- self.plot.addImage(rgba, legend="rgba",
- origin=(5., 5.), scale=(10., 10.),
- resetzoom=False)
+ (
+ ((0, 0, 0, 0.5), (0.5, 0, 0, 1), (1, 0, 0, 0.5)),
+ ((0, 0.5, 0, 1), (0, 0.5, 0.5, 1), (0, 1, 1, 0.5)),
+ ),
+ dtype=numpy.float32,
+ )
+
+ self.plot.addImage(
+ rgba, legend="rgba", origin=(5.0, 5.0), scale=(10.0, 10.0), resetzoom=False
+ )
self.plot.resetZoom()
@@ -1819,27 +1898,27 @@ class TestPlotMarkerLog(PlotWidgetTestCase):
# Test marker parameters
markers = [ # x, y, color, selectable, draggable
- (10., 10., 'blue', False, False),
- (20., 20., 'red', False, False),
- (40., 100., 'green', True, False),
- (40., 500., 'gray', True, True),
- (60., 800., 'black', False, True),
+ (10.0, 10.0, "blue", False, False),
+ (20.0, 20.0, "red", False, False),
+ (40.0, 100.0, "green", True, False),
+ (40.0, 500.0, "gray", True, True),
+ (60.0, 800.0, "black", False, True),
]
def setUp(self):
super(TestPlotMarkerLog, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
+ self.plot.getYAxis().setLabel("Rows")
+ self.plot.getXAxis().setLabel("Columns")
self.plot.getXAxis().setAutoScale(False)
self.plot.getYAxis().setAutoScale(False)
self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(1., 100., 1., 1000.)
+ self.plot.setLimits(1.0, 100.0, 1.0, 1000.0)
self.plot.getXAxis()._setLogarithmic(True)
self.plot.getYAxis()._setLogarithmic(True)
def testPlotMarkerXLog(self):
- self.plot.setGraphTitle('Markers X, Log axes')
+ self.plot.setGraphTitle("Markers X, Log axes")
for x, _, color, select, drag in self.markers:
name = str(x)
@@ -1851,7 +1930,7 @@ class TestPlotMarkerLog(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerYLog(self):
- self.plot.setGraphTitle('Markers Y, Log axes')
+ self.plot.setGraphTitle("Markers Y, Log axes")
for _, y, color, select, drag in self.markers:
name = str(y)
@@ -1863,7 +1942,7 @@ class TestPlotMarkerLog(PlotWidgetTestCase):
self.plot.resetZoom()
def testPlotMarkerPtLog(self):
- self.plot.setGraphTitle('Markers Pt, Log axes')
+ self.plot.setGraphTitle("Markers Pt, Log axes")
for x, y, color, select, drag in self.markers:
name = "{0},{1}".format(x, y)
@@ -1882,9 +1961,9 @@ class TestPlotWidgetSwitchBackend(PlotWidgetTestCase):
@pytest.mark.usefixtures("test_options")
def testSwitchBackend(self):
"""Test switching a plot with a few items"""
- backends = {'none': 'BackendBase', 'mpl': 'BackendMatplotlibQt'}
+ backends = {"none": "BackendBase", "mpl": "BackendMatplotlibQt"}
if self.test_options.WITH_GL_TEST:
- backends['gl'] = 'BackendOpenGL'
+ backends["gl"] = "BackendOpenGL"
self.plot.addImage(numpy.arange(100).reshape(10, 10))
self.plot.addCurve((-3, -2, -1), (1, 2, 3))
@@ -1906,208 +1985,65 @@ class TestPlotWidgetSwitchBackend(PlotWidgetTestCase):
self.assertEqual(self.plot.getItems(), items)
-class TestPlotWidgetSelection(PlotWidgetTestCase):
- """Test PlotWidget.selection and active items handling"""
-
- def _checkSelection(self, selection, current=None, selected=()):
- """Check current item and selected items."""
- self.assertIs(selection.getCurrentItem(), current)
- self.assertEqual(selection.getSelectedItems(), selected)
-
- def testSyncWithActiveItems(self):
- """Test update of PlotWidgetSelection according to active items"""
- listener = SignalListener()
-
- selection = self.plot.selection()
- selection.sigCurrentItemChanged.connect(listener)
- self._checkSelection(selection)
-
- # Active item is current
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- image = self.plot.getActiveImage()
- self.assertEqual(listener.callCount(), 1)
- self._checkSelection(selection, image, (image,))
-
- # No active = no current
- self.plot.setActiveImage(None)
- self.assertEqual(listener.callCount(), 2)
- self._checkSelection(selection)
-
- # Active item is current
- self.plot.setActiveImage('image')
- self.assertEqual(listener.callCount(), 3)
- self._checkSelection(selection, image, (image,))
-
- # Mosted recently "actived" item is current
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- scatter = self.plot.getActiveScatter()
- self.assertEqual(listener.callCount(), 4)
- self._checkSelection(selection, scatter, (scatter, image))
-
- # Previously mosted recently "actived" item is current
- self.plot.setActiveScatter(None)
- self.assertEqual(listener.callCount(), 5)
- self._checkSelection(selection, image, (image,))
-
- # Mosted recently "actived" item is current
- self.plot.setActiveScatter('scatter')
- self.assertEqual(listener.callCount(), 6)
- self._checkSelection(selection, scatter, (scatter, image))
-
- # No active = no current
- self.plot.setActiveImage(None)
- self.plot.setActiveScatter(None)
- self.assertEqual(listener.callCount(), 7)
- self._checkSelection(selection)
-
- # Mosted recently "actived" item is current
- self.plot.setActiveScatter('scatter')
- self.assertEqual(listener.callCount(), 8)
- self.plot.setActiveImage('image')
- self.assertEqual(listener.callCount(), 9)
- self._checkSelection(selection, image, (image, scatter))
-
- # Add a curve which is not active by default
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- curve = self.plot.getCurve('curve')
- self.assertEqual(listener.callCount(), 9)
- self._checkSelection(selection, image, (image, scatter))
-
- # Mosted recently "actived" item is current
- self.plot.setActiveCurve('curve')
- self.assertEqual(listener.callCount(), 10)
- self._checkSelection(selection, curve, (curve, image, scatter))
-
- # Add a curve which is not active by default
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve2')
- curve2 = self.plot.getCurve('curve2')
- self.assertEqual(listener.callCount(), 10)
- self._checkSelection(selection, curve, (curve, image, scatter))
-
- # Mosted recently "actived" item is current, previous curve is removed
- self.plot.setActiveCurve('curve2')
- self.assertEqual(listener.callCount(), 11)
- self._checkSelection(selection, curve2, (curve2, image, scatter))
-
- # No items = no current
- self.plot.clear()
- self.assertEqual(listener.callCount(), 12)
- self._checkSelection(selection)
-
- def testPlotWidgetWithItems(self):
- """Test init of selection on a plot with items"""
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- self.plot.setActiveCurve('curve')
-
- selection = self.plot.selection()
- self.assertIsNotNone(selection.getCurrentItem())
- selected = selection.getSelectedItems()
- self.assertEqual(len(selected), 3)
- self.assertIn(self.plot.getActiveCurve(), selected)
- self.assertIn(self.plot.getActiveImage(), selected)
- self.assertIn(self.plot.getActiveScatter(), selected)
-
- def testSetCurrentItem(self):
- """Test setCurrentItem"""
- # Add items to the plot
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- image = self.plot.getActiveImage()
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- scatter = self.plot.getActiveScatter()
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- self.plot.setActiveCurve('curve')
- curve = self.plot.getActiveCurve()
-
- selection = self.plot.selection()
- self.assertIsNotNone(selection.getCurrentItem())
- self.assertEqual(len(selection.getSelectedItems()), 3)
-
- # Set current to None reset all active items
- selection.setCurrentItem(None)
- self._checkSelection(selection)
- self.assertIsNone(self.plot.getActiveCurve())
- self.assertIsNone(self.plot.getActiveImage())
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active
- selection.setCurrentItem(image)
- self._checkSelection(selection, image, (image,))
- self.assertIsNone(self.plot.getActiveCurve())
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active and keeps other active
- selection.setCurrentItem(curve)
- self._checkSelection(selection, curve, (curve, image))
- self.assertIs(self.plot.getActiveCurve(), curve)
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active and keeps other active
- selection.setCurrentItem(scatter)
- self._checkSelection(selection, scatter, (scatter, curve, image))
- self.assertIs(self.plot.getActiveCurve(), curve)
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIs(self.plot.getActiveScatter(), scatter)
-
-
@pytest.mark.usefixtures("use_opengl")
class TestPlotWidget_Gl(TestPlotWidget):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotImage_Gl(TestPlotImage):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotCurve_Gl(TestPlotCurve):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotHistogram_Gl(TestPlotHistogram):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotScatter_Gl(TestPlotScatter):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotMarker_Gl(TestPlotMarker):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotItem_Gl(TestPlotItem):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotAxes_Gl(TestPlotAxes):
- backend="gl"
+ backend = "gl"
-@pytest.mark.usefixtures("use_opengl")
-class TestPlotActiveCurveImage_Gl(TestPlotActiveCurveImage):
- backend="gl"
@pytest.mark.usefixtures("use_opengl")
class TestPlotEmptyLog_Gl(TestPlotEmptyLog):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotCurveLog_Gl(TestPlotCurveLog):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotImageLog_Gl(TestPlotImageLog):
- backend="gl"
+ backend = "gl"
+
@pytest.mark.usefixtures("use_opengl")
class TestPlotMarkerLog_Gl(TestPlotMarkerLog):
- backend="gl"
+ backend = "gl"
-@pytest.mark.usefixtures("use_opengl")
-class TestPlotWidgetSelection_Gl(TestPlotWidgetSelection):
- backend="gl"
class TestSpecial_ExplicitMplBackend(TestSpecialBackend):
- backend="mpl"
+ backend = "mpl"
diff --git a/src/silx/gui/plot/test/testPlotWidgetActiveItem.py b/src/silx/gui/plot/test/testPlotWidgetActiveItem.py
new file mode 100755
index 0000000..99285a8
--- /dev/null
+++ b/src/silx/gui/plot/test/testPlotWidgetActiveItem.py
@@ -0,0 +1,416 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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.
+#
+# ###########################################################################*/
+"""Test PlotWidget active item"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "11/12/2023"
+
+
+import numpy
+import pytest
+
+from silx.gui.utils.testutils import SignalListener
+from silx.gui.plot.items.curve import CurveStyle
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testActiveCurveAndLabels(plotWidget):
+ # Active curve handling off, no label change
+ plotWidget.setActiveCurveHandling(False)
+ plotWidget.getXAxis().setLabel("XLabel")
+ plotWidget.getYAxis().setLabel("YLabel")
+ plotWidget.addCurve((1, 2), (1, 2))
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.addCurve((1, 2), (2, 3), xlabel="x1", ylabel="y1")
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.clear()
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ # Active curve handling on, label changes
+ plotWidget.setActiveCurveHandling(True)
+ plotWidget.getXAxis().setLabel("XLabel")
+ plotWidget.getYAxis().setLabel("YLabel")
+
+ # labels changed as active curve
+ plotWidget.addCurve((1, 2), (1, 2), legend="1", xlabel="x1", ylabel="y1")
+ plotWidget.setActiveCurve("1")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ # labels not changed as not active curve
+ plotWidget.addCurve((1, 2), (2, 3), legend="2")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ # labels changed
+ plotWidget.setActiveCurve("2")
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.setActiveCurve("1")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ plotWidget.clear()
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.setActiveCurveHandling(False)
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testPlotActiveCurveSelectionMode(plotWidget):
+ xData = numpy.arange(1000)
+ yData = -500 + 100 * numpy.sin(xData)
+ xData2 = xData + 1000
+ yData2 = xData - 1000 + 200 * numpy.random.random(1000)
+
+ plotWidget.clear()
+ plotWidget.setActiveCurveHandling(True)
+ legend = "curve 1"
+ plotWidget.addCurve(xData, yData, legend=legend, color="green")
+
+ # active curve should be None
+ assert plotWidget.getActiveCurve(just_legend=True) is None
+
+ # active curve should be None when None is set as active curve
+ plotWidget.setActiveCurve(legend)
+ current = plotWidget.getActiveCurve(just_legend=True)
+ assert current == legend
+ plotWidget.setActiveCurve(None)
+ current = plotWidget.getActiveCurve(just_legend=True)
+ assert current is None
+
+ # testing it automatically toggles if there is only one
+ plotWidget.setActiveCurveSelectionMode("legacy")
+ current = plotWidget.getActiveCurve(just_legend=True)
+ assert current == legend
+
+ # active curve should not change when None set as active curve
+ assert plotWidget.getActiveCurveSelectionMode() == "legacy"
+ plotWidget.setActiveCurve(None)
+ current = plotWidget.getActiveCurve(just_legend=True)
+ assert current == legend
+
+ # situation where no curve is active
+ plotWidget.clear()
+ plotWidget.setActiveCurveHandling(True)
+ assert plotWidget.getActiveCurveSelectionMode() == "atmostone"
+ plotWidget.addCurve(xData, yData, legend=legend, color="green")
+ assert plotWidget.getActiveCurve(just_legend=True) is None
+ plotWidget.addCurve(xData2, yData2, legend="curve 2", color="red")
+ assert plotWidget.getActiveCurve(just_legend=True) is None
+ plotWidget.setActiveCurveSelectionMode("legacy")
+ assert plotWidget.getActiveCurve(just_legend=True) is None
+
+ # the first curve added should be active
+ plotWidget.clear()
+ plotWidget.addCurve(xData, yData, legend=legend, color="green")
+ assert plotWidget.getActiveCurve(just_legend=True) == legend
+ plotWidget.addCurve(xData2, yData2, legend="curve 2", color="red")
+ assert plotWidget.getActiveCurve(just_legend=True) == legend
+
+ plotWidget.setActiveCurveHandling(False)
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testActiveCurveStyle(plotWidget):
+ """Test change of active curve style"""
+ plotWidget.setActiveCurveHandling(True)
+ plotWidget.setActiveCurveStyle(color="black")
+ style = plotWidget.getActiveCurveStyle()
+ assert style.getColor() == (0.0, 0.0, 0.0, 1.0)
+ assert style.getLineStyle() is None
+ assert style.getLineWidth() is None
+ assert style.getSymbol() is None
+ assert style.getSymbolSize() is None
+
+ xData = numpy.arange(1000)
+ yData = -500 + 100 * numpy.sin(xData)
+ plotWidget.addCurve(x=xData, y=yData, legend="curve1")
+ curve = plotWidget.getCurve("curve1")
+ curve.setColor("blue")
+ curve.setLineStyle("-")
+ curve.setLineWidth(1)
+ curve.setSymbol("o")
+ curve.setSymbolSize(5)
+
+ # Check default current style
+ defaultStyle = curve.getCurrentStyle()
+ assert defaultStyle == CurveStyle(
+ color="blue", linestyle="-", linewidth=1, symbol="o", symbolsize=5
+ )
+
+ # Activate curve with highlight color=black
+ plotWidget.setActiveCurve("curve1")
+ style = curve.getCurrentStyle()
+ assert style.getColor() == (0.0, 0.0, 0.0, 1.0)
+ assert style.getLineStyle() == "-"
+ assert style.getLineWidth() == 1
+ assert style.getSymbol() == "o"
+ assert style.getSymbolSize() == 5
+
+ # Change highlight to linewidth=2
+ plotWidget.setActiveCurveStyle(linewidth=2)
+ style = curve.getCurrentStyle()
+ assert style.getColor() == (0.0, 0.0, 1.0, 1.0)
+ assert style.getLineStyle() == "-"
+ assert style.getLineWidth() == 2
+ assert style.getSymbol() == "o"
+ assert style.getSymbolSize() == 5
+
+ plotWidget.setActiveCurve(None)
+ assert curve.getCurrentStyle() == defaultStyle
+
+ plotWidget.setActiveCurveHandling(False)
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testActiveImageAndLabels(plotWidget):
+ # Active image handling always on, no API for toggling it
+ plotWidget.getXAxis().setLabel("XLabel")
+ plotWidget.getYAxis().setLabel("YLabel")
+
+ # labels changed as active curve
+ plotWidget.addImage(
+ numpy.arange(100).reshape(10, 10), legend="1", xlabel="x1", ylabel="y1"
+ )
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ # labels not changed as not active curve
+ plotWidget.addImage(numpy.arange(100).reshape(10, 10), legend="2")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ # labels changed
+ plotWidget.setActiveImage("2")
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.setActiveImage("1")
+ assert plotWidget.getXAxis().getLabel() == "x1"
+ assert plotWidget.getYAxis().getLabel() == "y1"
+
+ plotWidget.clear()
+ assert plotWidget.getXAxis().getLabel() == "XLabel"
+ assert plotWidget.getYAxis().getLabel() == "YLabel"
+
+ plotWidget.setActiveCurveHandling(False)
+
+
+def _checkSelection(selection, current=None, selected=()):
+ """Check current item and selected items."""
+ assert selection.getCurrentItem() is current
+ assert selection.getSelectedItems() == selected
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testSelectionSyncWithActiveItems(plotWidget):
+ """Test update of PlotWidgetSelection according to active items"""
+ listener = SignalListener()
+
+ selection = plotWidget.selection()
+ selection.sigCurrentItemChanged.connect(listener)
+ _checkSelection(selection)
+
+ # Active item is current
+ plotWidget.addImage(((0, 1), (2, 3)), legend="image")
+ image = plotWidget.getActiveImage()
+ assert listener.callCount() == 1
+ _checkSelection(selection, image, (image,))
+
+ # No active = no current
+ plotWidget.setActiveImage(None)
+ assert listener.callCount() == 2
+ _checkSelection(selection)
+
+ # Active item is current
+ plotWidget.setActiveImage("image")
+ assert listener.callCount() == 3
+ _checkSelection(selection, image, (image,))
+
+ # Mosted recently "actived" item is current
+ plotWidget.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend="scatter")
+ scatter = plotWidget.getActiveScatter()
+ assert listener.callCount() == 4
+ _checkSelection(selection, scatter, (scatter, image))
+
+ # Previously mosted recently "actived" item is current
+ plotWidget.setActiveScatter(None)
+ assert listener.callCount() == 5
+ _checkSelection(selection, image, (image,))
+
+ # Mosted recently "actived" item is current
+ plotWidget.setActiveScatter("scatter")
+ assert listener.callCount() == 6
+ _checkSelection(selection, scatter, (scatter, image))
+
+ # No active = no current
+ plotWidget.setActiveImage(None)
+ plotWidget.setActiveScatter(None)
+ assert listener.callCount() == 7
+ _checkSelection(selection)
+
+ # Mosted recently "actived" item is current
+ plotWidget.setActiveScatter("scatter")
+ assert listener.callCount() == 8
+ plotWidget.setActiveImage("image")
+ assert listener.callCount() == 9
+ _checkSelection(selection, image, (image, scatter))
+
+ # Add a curve which is not active by default
+ plotWidget.addCurve((0, 1, 2), (0, 1, 2), legend="curve")
+ curve = plotWidget.getCurve("curve")
+ assert listener.callCount() == 9
+ _checkSelection(selection, image, (image, scatter))
+
+ # Mosted recently "actived" item is current
+ plotWidget.setActiveCurve("curve")
+ assert listener.callCount() == 10
+ _checkSelection(selection, curve, (curve, image, scatter))
+
+ # Add a curve which is not active by default
+ plotWidget.addCurve((0, 1, 2), (0, 1, 2), legend="curve2")
+ curve2 = plotWidget.getCurve("curve2")
+ assert listener.callCount() == 10
+ _checkSelection(selection, curve, (curve, image, scatter))
+
+ # Mosted recently "actived" item is current, previous curve is removed
+ plotWidget.setActiveCurve("curve2")
+ assert listener.callCount() == 11
+ _checkSelection(selection, curve2, (curve2, image, scatter))
+
+ # No items = no current
+ plotWidget.clear()
+ assert listener.callCount() == 12
+ _checkSelection(selection)
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testSelectionWithItems(plotWidget):
+ """Test init of selection on a plot with items"""
+ plotWidget.addImage(((0, 1), (2, 3)), legend="image")
+ plotWidget.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend="scatter")
+ plotWidget.addCurve((0, 1, 2), (0, 1, 2), legend="curve")
+ plotWidget.setActiveCurve("curve")
+
+ selection = plotWidget.selection()
+ assert selection.getCurrentItem() is not None
+ selected = selection.getSelectedItems()
+ assert len(selected) == 3
+ assert plotWidget.getActiveCurve() in selected
+ assert plotWidget.getActiveImage() in selected
+ assert plotWidget.getActiveScatter() in selected
+
+
+@pytest.mark.parametrize("plotWidget", ("mpl", "gl"), indirect=True)
+def testSelectionSetCurrentItem(plotWidget):
+ """Test setCurrentItem"""
+ # Add items to the plot
+ plotWidget.addImage(((0, 1), (2, 3)), legend="image")
+ image = plotWidget.getActiveImage()
+ plotWidget.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend="scatter")
+ scatter = plotWidget.getActiveScatter()
+ plotWidget.addCurve((0, 1, 2), (0, 1, 2), legend="curve")
+ plotWidget.setActiveCurve("curve")
+ curve = plotWidget.getActiveCurve()
+
+ selection = plotWidget.selection()
+ assert selection.getCurrentItem() is not None
+ assert len(selection.getSelectedItems()) == 3
+
+ # Set current to None reset all active items
+ selection.setCurrentItem(None)
+ _checkSelection(selection)
+ assert plotWidget.getActiveCurve() is None
+ assert plotWidget.getActiveImage() is None
+ assert plotWidget.getActiveScatter() is None
+
+ # Set current to an item makes it active
+ selection.setCurrentItem(image)
+ _checkSelection(selection, image, (image,))
+ assert plotWidget.getActiveCurve() is None
+ assert plotWidget.getActiveImage() is image
+ assert plotWidget.getActiveScatter() is None
+
+ # Set current to an item makes it active and keeps other active
+ selection.setCurrentItem(curve)
+ _checkSelection(selection, curve, (curve, image))
+ assert plotWidget.getActiveCurve() is curve
+ assert plotWidget.getActiveImage() is image
+ assert plotWidget.getActiveScatter() is None
+
+ # Set current to an item makes it active and keeps other active
+ selection.setCurrentItem(scatter)
+ _checkSelection(selection, scatter, (scatter, curve, image))
+ assert plotWidget.getActiveCurve() is curve
+ assert plotWidget.getActiveImage() is image
+ assert plotWidget.getActiveScatter() is scatter
+
+
+def testSetActiveCurveWithInstance(plotWidget):
+ """Test setting the active curve with a curve item instance"""
+ plotWidget.addCurve((0, 1), (0, 1), legend="curve0")
+ plotWidget.addCurve((0, 1), (1, 0), legend="curve1")
+ curve0, curve1 = plotWidget.getItems()
+
+ plotWidget.setActiveCurve(curve0)
+ assert plotWidget.getActiveCurve() is curve0
+
+ plotWidget.setActiveCurve(curve1)
+ assert plotWidget.getActiveCurve() is curve1
+
+ plotWidget.setActiveCurve(None)
+ assert plotWidget.getActiveCurve() is None
+
+
+def testSetActiveImageWithInstance(plotWidget):
+ """Test setting the active image with an image item instance"""
+ plotWidget.addImage(((0, 1), (2, 3)), legend="image")
+ image = plotWidget.getItems()[0]
+
+ plotWidget.setActiveImage(None)
+ assert plotWidget.getActiveImage() is None
+
+ plotWidget.setActiveImage(image)
+ assert plotWidget.getActiveImage() is image
+
+
+def testSetActiveScatterWithInstance(plotWidget):
+ """Test setting the active scatter with a scatter item instance"""
+ plotWidget.addScatter((0, 1), (0, 1), (0, 1), legend="scatter")
+ scatter = plotWidget.getItems()[0]
+
+ plotWidget.setActiveScatter(None)
+ assert plotWidget.getActiveScatter() is None
+
+ plotWidget.setActiveScatter(scatter)
+ assert plotWidget.getActiveScatter() is scatter
diff --git a/src/silx/gui/plot/test/testPlotWidgetDataMargins.py b/src/silx/gui/plot/test/testPlotWidgetDataMargins.py
new file mode 100644
index 0000000..4eb5134
--- /dev/null
+++ b/src/silx/gui/plot/test/testPlotWidgetDataMargins.py
@@ -0,0 +1,135 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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.
+#
+# ###########################################################################*/
+"""Test PlotWidget features related to data margins"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "11/05/2023"
+
+import numpy
+import pytest
+
+
+def testDefaultDataMargins(plotWidget):
+ """Test default PlotWidget data margins: No margins"""
+ assert plotWidget.getDataMargins() == (0, 0, 0, 0)
+
+
+def testResetZoomDataMarginsLinearAxes(qapp, plotWidget):
+ """Test PlotWidget.setDataMargins effect on resetZoom with linear axis scales"""
+
+ margins = 0.1, 0.2, 0.3, 0.4
+ plotWidget.setDataMargins(*margins)
+
+ plotWidget.resetZoom()
+ qapp.processEvents()
+
+ retrievedMargins = plotWidget.getDataMargins()
+ assert retrievedMargins == margins
+
+ dataRange = 100 - 1
+ expectedXLimits = 1 - 0.1 * dataRange, 100 + 0.2 * dataRange
+ expectedYLimits = 1 - 0.3 * dataRange, 100 + 0.4 * dataRange
+
+ assert plotWidget.getXAxis().getLimits() == expectedXLimits
+ assert plotWidget.getYAxis().getLimits() == expectedYLimits
+ assert plotWidget.getYAxis(axis="right").getLimits() == expectedYLimits
+
+
+def testResetZoomDataMarginsLogAxes(qapp, plotWidget):
+ """Test PlotWidget.setDataMargins effect on resetZoom with log axis scales"""
+ plotWidget.getXAxis().setScale("log")
+ plotWidget.getYAxis().setScale("log")
+
+ dataMargins = 0.1, 0.2, 0.3, 0.4
+ plotWidget.setDataMargins(*dataMargins)
+
+ plotWidget.resetZoom()
+ qapp.processEvents()
+
+ retrievedMargins = plotWidget.getDataMargins()
+ assert retrievedMargins == dataMargins
+
+ logMin, logMax = numpy.log10(1), numpy.log10(100)
+ logRange = logMax - logMin
+ expectedXLimits = pow(10.0, logMin - 0.1 * logRange), pow(
+ 10.0, logMax + 0.2 * logRange
+ )
+ expectedYLimits = pow(10.0, logMin - 0.3 * logRange), pow(
+ 10.0, logMax + 0.4 * logRange
+ )
+
+ assert plotWidget.getXAxis().getLimits() == expectedXLimits
+ assert plotWidget.getYAxis().getLimits() == expectedYLimits
+ assert plotWidget.getYAxis(axis="right").getLimits() == expectedYLimits
+
+
+@pytest.mark.parametrize("margins", [False, True, (0, 0, 0, 0)])
+def testSetLimitsNoDataMargins(plotWidget, margins):
+ """Test PlotWidget.setLimits without data margins"""
+ xlimits = 1, 2
+ ylimits = 3, 4
+ y2limits = 5, 6
+ plotWidget.setLimits(*xlimits, *ylimits, *y2limits, margins=margins)
+
+ assert plotWidget.getXAxis().getLimits() == xlimits
+ assert plotWidget.getYAxis().getLimits() == ylimits
+ assert plotWidget.getYAxis(axis="right").getLimits() == y2limits
+
+
+@pytest.mark.parametrize(
+ "margins,expectedLimits",
+ [
+ # margins=False: use limits as is
+ (
+ False,
+ (1, 2, 3, 4, 5, 6),
+ ),
+ # margins=True: apply data margins
+ (
+ True,
+ (1 - 0.1, 2 + 0.2, 3 - 0.3, 4 + 0.4, 5 - 0.3, 6 + 0.4),
+ ),
+ # margins=tuple: apply provided margins
+ (
+ (0.4, 0.3, 0.2, 0.1),
+ (1 - 0.4, 2 + 0.3, 3 - 0.2, 4 + 0.1, 5 - 0.2, 6 + 0.1),
+ ),
+ ],
+)
+def testSetLimitsWithDataMargins(qapp, plotWidget, margins, expectedLimits):
+ """Test PlotWidget.setLimits with data margins"""
+ dataMargins = 0.1, 0.2, 0.3, 0.4
+ limits = 1, 2, 3, 4, 5, 6
+
+ plotWidget.setDataMargins(*dataMargins)
+ plotWidget.setLimits(*limits, margins=margins)
+ qapp.processEvents()
+
+ retrievedLimits = (
+ *plotWidget.getXAxis().getLimits(),
+ *plotWidget.getYAxis().getLimits(),
+ *plotWidget.getYAxis(axis="right").getLimits(),
+ )
+ assert retrievedLimits == expectedLimits
diff --git a/src/silx/gui/plot/test/testPlotWidgetNoBackend.py b/src/silx/gui/plot/test/testPlotWidgetNoBackend.py
index 4914929..d9d5706 100644
--- a/src/silx/gui/plot/test/testPlotWidgetNoBackend.py
+++ b/src/silx/gui/plot/test/testPlotWidgetNoBackend.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 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
@@ -35,6 +34,8 @@ from silx.utils.testutils import ParametricTestCase
import numpy
+import silx
+from silx.gui.colors import rgba
from silx.gui.plot.PlotWidget import PlotWidget
from silx.gui.plot.items.histogram import _getHistogramCurve, _computeEdges
@@ -45,9 +46,9 @@ class TestPlot(unittest.TestCase):
def testPlotTitleLabels(self):
"""Create a Plot and set the labels"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
- title, xlabel, ylabel = 'the title', 'x label', 'y label'
+ title, xlabel, ylabel = "the title", "x label", "y label"
plot.setGraphTitle(title)
plot.getXAxis().setLabel(xlabel)
plot.getYAxis().setLabel(ylabel)
@@ -59,26 +60,29 @@ class TestPlot(unittest.TestCase):
def testAddNoRemove(self):
"""add objects to the Plot"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
plot.addCurve(x=(1, 2, 3), y=(3, 2, 1))
- plot.addImage(numpy.arange(100.).reshape(10, -1))
- plot.addShape(numpy.array((1., 10.)),
- numpy.array((10., 10.)),
- shape="rectangle")
- plot.addXMarker(10.)
+ plot.addImage(numpy.arange(100.0).reshape(10, -1))
+ plot.addShape(
+ numpy.array((1.0, 10.0)), numpy.array((10.0, 10.0)), shape="rectangle"
+ )
+ plot.addXMarker(10.0)
class TestPlotRanges(ParametricTestCase):
"""Basic tests of Plot data ranges without backend"""
- _getValidValues = {True: lambda ar: ar > 0,
- False: lambda ar: numpy.ones(shape=ar.shape,
- dtype=bool)}
+ _getValidValues = {
+ True: lambda ar: ar > 0,
+ False: lambda ar: numpy.ones(shape=ar.shape, dtype=bool),
+ }
@staticmethod
def _getRanges(arrays, are_logs):
- gen = (TestPlotRanges._getValidValues[is_log](ar)
- for (ar, is_log) in zip(arrays, are_logs))
+ gen = (
+ TestPlotRanges._getValidValues[is_log](ar)
+ for (ar, is_log) in zip(arrays, are_logs)
+ )
indices = numpy.where(reduce(numpy.logical_and, gen))[0]
if len(indices) > 0:
ranges = [(ar[indices[0]], ar[indices[-1]]) for ar in arrays]
@@ -97,13 +101,15 @@ class TestPlotRanges(ParametricTestCase):
def testDataRangeNoPlot(self):
"""empty plot data range"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
@@ -115,27 +121,25 @@ class TestPlotRanges(ParametricTestCase):
def testDataRangeLeft(self):
"""left axis range"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
xData = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
yData = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
- plot.addCurve(x=xData,
- y=yData,
- legend='plot_0',
- yaxis='left')
+ plot.addCurve(x=xData, y=yData, legend="plot_0", yaxis="left")
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
- xRange, yRange = self._getRanges([xData, yData],
- [logX, logY])
+ xRange, yRange = self._getRanges([xData, yData], [logX, logY])
self.assertSequenceEqual(dataRange.x, xRange)
self.assertSequenceEqual(dataRange.y, yRange)
self.assertIsNone(dataRange.yright)
@@ -143,25 +147,23 @@ class TestPlotRanges(ParametricTestCase):
def testDataRangeRight(self):
"""right axis range"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
xData = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
yData = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
- plot.addCurve(x=xData,
- y=yData,
- legend='plot_0',
- yaxis='right')
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ plot.addCurve(x=xData, y=yData, legend="plot_0", yaxis="right")
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
- xRange, yRange = self._getRanges([xData, yData],
- [logX, logY])
+ xRange, yRange = self._getRanges([xData, yData], [logX, logY])
self.assertSequenceEqual(dataRange.x, xRange)
self.assertIsNone(dataRange.y)
self.assertSequenceEqual(dataRange.yright, yRange)
@@ -170,69 +172,70 @@ class TestPlotRanges(ParametricTestCase):
"""image data range"""
origin = (-10, 25)
- scale = (3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
-
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
-
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ scale = (3.0, 8.0)
+ image = numpy.arange(100.0).reshape(20, 5)
+
+ plot = PlotWidget(backend="none")
+ plot.addImage(image, origin=origin, scale=scale)
+
+ xRange = numpy.array([0.0, image.shape[1] * scale[0]]) + origin[0]
+ yRange = numpy.array([0.0, image.shape[0] * scale[1]]) + origin[1]
+
+ ranges = {
+ (False, False): (xRange, yRange),
+ (True, False): (None, None),
+ (True, True): (None, None),
+ (False, True): (None, None),
+ }
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
+ self.assertTrue(
+ numpy.array_equal(dataRange.x, xRange),
+ msg="{0} != {1}".format(dataRange.x, xRange),
+ )
+ self.assertTrue(
+ numpy.array_equal(dataRange.y, yRange),
+ msg="{0} != {1}".format(dataRange.y, yRange),
+ )
self.assertIsNone(dataRange.yright)
def testDataRangeLeftRight(self):
"""right+left axis range"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
xData_l = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
yData_l = numpy.arange(10) - 1.9 # range : -1.9 , 7.1
- plot.addCurve(x=xData_l,
- y=yData_l,
- legend='plot_l',
- yaxis='left')
+ plot.addCurve(x=xData_l, y=yData_l, legend="plot_l", yaxis="left")
xData_r = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
yData_r = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
- plot.addCurve(x=xData_r,
- y=yData_r,
- legend='plot_r',
- yaxis='right')
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ plot.addCurve(x=xData_r, y=yData_r, legend="plot_r", yaxis="right")
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
- xRangeL, yRangeL = self._getRanges([xData_l, yData_l],
- [logX, logY])
- xRangeR, yRangeR = self._getRanges([xData_r, yData_r],
- [logX, logY])
+ xRangeL, yRangeL = self._getRanges([xData_l, yData_l], [logX, logY])
+ xRangeR, yRangeR = self._getRanges([xData_r, yData_r], [logX, logY])
xRangeLR = self._getRangesMinmax([xRangeL, xRangeR])
self.assertSequenceEqual(dataRange.x, xRangeLR)
self.assertSequenceEqual(dataRange.y, yRangeL)
@@ -245,51 +248,42 @@ class TestPlotRanges(ParametricTestCase):
# image sets x min and y max
# plot_left sets y min
# plot_right sets x max (and yright)
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
origin = (-10, 5)
- scale = (3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
+ scale = (3.0, 8.0)
+ image = numpy.arange(100.0).reshape(20, 5)
- plot.addImage(image,
- origin=origin, scale=scale, legend='image')
+ plot.addImage(image, origin=origin, scale=scale, legend="image")
xData_l = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
yData_l = numpy.arange(10) - 1.9 # range : -1.9 , 7.1
- plot.addCurve(x=xData_l,
- y=yData_l,
- legend='plot_l',
- yaxis='left')
+ plot.addCurve(x=xData_l, y=yData_l, legend="plot_l", yaxis="left")
xData_r = numpy.arange(10) + 4.1 # range : 4.1 , 13.1
yData_r = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
- plot.addCurve(x=xData_r,
- y=yData_r,
- legend='plot_r',
- yaxis='right')
-
- imgXRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- imgYRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ plot.addCurve(x=xData_r, y=yData_r, legend="plot_r", yaxis="right")
+
+ imgXRange = numpy.array([0.0, image.shape[1] * scale[0]]) + origin[0]
+ imgYRange = numpy.array([0.0, image.shape[0] * scale[1]]) + origin[1]
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
- xRangeL, yRangeL = self._getRanges([xData_l, yData_l],
- [logX, logY])
- xRangeR, yRangeR = self._getRanges([xData_r, yData_r],
- [logX, logY])
+ xRangeL, yRangeL = self._getRanges([xData_l, yData_l], [logX, logY])
+ xRangeR, yRangeR = self._getRanges([xData_r, yData_r], [logX, logY])
if logX or logY:
xRangeLR = self._getRangesMinmax([xRangeL, xRangeR])
else:
- xRangeLR = self._getRangesMinmax([xRangeL,
- xRangeR,
- imgXRange])
+ xRangeLR = self._getRangesMinmax([xRangeL, xRangeR, imgXRange])
yRangeL = self._getRangesMinmax([yRangeL, imgYRange])
self.assertSequenceEqual(dataRange.x, xRangeLR)
self.assertSequenceEqual(dataRange.y, yRangeL)
@@ -299,83 +293,97 @@ class TestPlotRanges(ParametricTestCase):
"""image data range, negative scale"""
origin = (-10, 25)
- scale = (-3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
+ scale = (-3.0, 8.0)
+ image = numpy.arange(100.0).reshape(20, 5)
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
+ plot = PlotWidget(backend="none")
+ plot.addImage(image, origin=origin, scale=scale)
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
+ xRange = numpy.array([0.0, image.shape[1] * scale[0]]) + origin[0]
xRange.sort() # negative scale!
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ yRange = numpy.array([0.0, image.shape[0] * scale[1]]) + origin[1]
+
+ ranges = {
+ (False, False): (xRange, yRange),
+ (True, False): (None, None),
+ (True, True): (None, None),
+ (False, True): (None, None),
+ }
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
+ self.assertTrue(
+ numpy.array_equal(dataRange.x, xRange),
+ msg="{0} != {1}".format(dataRange.x, xRange),
+ )
+ self.assertTrue(
+ numpy.array_equal(dataRange.y, yRange),
+ msg="{0} != {1}".format(dataRange.y, yRange),
+ )
self.assertIsNone(dataRange.yright)
def testDataRangeImageNegativeScaleY(self):
"""image data range, negative scale"""
origin = (-10, 25)
- scale = (3., -8.)
- image = numpy.arange(100.).reshape(20, 5)
+ scale = (3.0, -8.0)
+ image = numpy.arange(100.0).reshape(20, 5)
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
+ plot = PlotWidget(backend="none")
+ plot.addImage(image, origin=origin, scale=scale)
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
+ xRange = numpy.array([0.0, image.shape[1] * scale[0]]) + origin[0]
+ yRange = numpy.array([0.0, image.shape[0] * scale[1]]) + origin[1]
yRange.sort() # negative scale!
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
+ ranges = {
+ (False, False): (xRange, yRange),
+ (True, False): (None, None),
+ (True, True): (None, None),
+ (False, True): (None, None),
+ }
+
+ for logX, logY in (
+ (False, False),
+ (True, False),
+ (True, True),
+ (False, True),
+ (False, False),
+ ):
with self.subTest(logX=logX, logY=logY):
plot.getXAxis()._setLogarithmic(logX)
plot.getYAxis()._setLogarithmic(logY)
dataRange = plot.getDataRange()
xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
+ self.assertTrue(
+ numpy.array_equal(dataRange.x, xRange),
+ msg="{0} != {1}".format(dataRange.x, xRange),
+ )
+ self.assertTrue(
+ numpy.array_equal(dataRange.y, yRange),
+ msg="{0} != {1}".format(dataRange.y, yRange),
+ )
self.assertIsNone(dataRange.yright)
def testDataRangeHiddenCurve(self):
"""curves with a hidden curve"""
- plot = PlotWidget(backend='none')
- plot.addCurve((0, 1), (0, 1), legend='shown')
- plot.addCurve((0, 1, 2), (5, 5, 5), legend='hidden')
+ plot = PlotWidget(backend="none")
+ plot.addCurve((0, 1), (0, 1), legend="shown")
+ plot.addCurve((0, 1, 2), (5, 5, 5), legend="hidden")
range1 = plot.getDataRange()
self.assertEqual(range1.x, (0, 2))
self.assertEqual(range1.y, (0, 5))
- plot.hideCurve('hidden')
+ plot.hideCurve("hidden")
range2 = plot.getDataRange()
self.assertEqual(range2.x, (0, 1))
self.assertEqual(range2.y, (0, 1))
@@ -387,108 +395,108 @@ class TestPlotGetCurveImage(unittest.TestCase):
def testGetCurve(self):
"""PlotWidget.getCurve and Plot.getActiveCurve tests"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No curve
curve = plot.getCurve()
self.assertIsNone(curve) # No curve
plot.setActiveCurveHandling(True)
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 0')
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 1')
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 2')
- plot.setActiveCurve('curve 0')
+ plot.addCurve(x=(0, 1), y=(0, 1), legend="curve 0")
+ plot.addCurve(x=(0, 1), y=(0, 1), legend="curve 1")
+ plot.addCurve(x=(0, 1), y=(0, 1), legend="curve 2")
+ plot.setActiveCurve("curve 0")
# Active curve
active = plot.getActiveCurve()
- self.assertEqual(active.getName(), 'curve 0')
+ self.assertEqual(active.getName(), "curve 0")
curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 0')
+ self.assertEqual(curve.getName(), "curve 0")
# No active curve and curves
plot.setActiveCurveHandling(False)
active = plot.getActiveCurve()
self.assertIsNone(active) # No active curve
curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 2') # Last added curve
+ self.assertEqual(curve.getName(), "curve 2") # Last added curve
# Last curve hidden
- plot.hideCurve('curve 2', True)
+ plot.hideCurve("curve 2", True)
curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 1') # Last added curve
+ self.assertEqual(curve.getName(), "curve 1") # Last added curve
# All curves hidden
- plot.hideCurve('curve 1', True)
- plot.hideCurve('curve 0', True)
+ plot.hideCurve("curve 1", True)
+ plot.hideCurve("curve 0", True)
curve = plot.getCurve()
self.assertIsNone(curve)
def testGetCurveOldApi(self):
"""old API PlotWidget.getCurve and Plot.getActiveCurve tests"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No curve
curve = plot.getCurve()
self.assertIsNone(curve) # No curve
plot.setActiveCurveHandling(True)
- x = numpy.arange(10.).astype(numpy.float32)
+ x = numpy.arange(10.0).astype(numpy.float32)
y = x * x
- plot.addCurve(x=x, y=y, legend='curve 0', info=["whatever"])
- plot.addCurve(x=x, y=2*x, legend='curve 1', info="anything")
- plot.setActiveCurve('curve 0')
+ plot.addCurve(x=x, y=y, legend="curve 0", info=["whatever"])
+ plot.addCurve(x=x, y=2 * x, legend="curve 1", info="anything")
+ plot.setActiveCurve("curve 0")
# Active curve (4 elements)
xOut, yOut, legend, info = plot.getActiveCurve()[:4]
- self.assertEqual(legend, 'curve 0')
- self.assertTrue(numpy.allclose(xOut, x), 'curve 0 wrong x data')
- self.assertTrue(numpy.allclose(yOut, y), 'curve 0 wrong y data')
+ self.assertEqual(legend, "curve 0")
+ self.assertTrue(numpy.allclose(xOut, x), "curve 0 wrong x data")
+ self.assertTrue(numpy.allclose(yOut, y), "curve 0 wrong y data")
# Active curve (5 elements)
xOut, yOut, legend, info, params = plot.getCurve("curve 1")
- self.assertEqual(legend, 'curve 1')
- self.assertEqual(info, 'anything')
- self.assertTrue(numpy.allclose(xOut, x), 'curve 1 wrong x data')
- self.assertTrue(numpy.allclose(yOut, 2 * x), 'curve 1 wrong y data')
+ self.assertEqual(legend, "curve 1")
+ self.assertEqual(info, "anything")
+ self.assertTrue(numpy.allclose(xOut, x), "curve 1 wrong x data")
+ self.assertTrue(numpy.allclose(yOut, 2 * x), "curve 1 wrong y data")
def testGetImage(self):
"""PlotWidget.getImage and PlotWidget.getActiveImage tests"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No image
image = plot.getImage()
self.assertIsNone(image)
- plot.addImage(((0, 1), (2, 3)), legend='image 0')
- plot.addImage(((0, 1), (2, 3)), legend='image 1')
+ plot.addImage(((0, 1), (2, 3)), legend="image 0")
+ plot.addImage(((0, 1), (2, 3)), legend="image 1")
# Active image
active = plot.getActiveImage()
- self.assertEqual(active.getName(), 'image 0')
+ self.assertEqual(active.getName(), "image 0")
image = plot.getImage()
- self.assertEqual(image.getName(), 'image 0')
+ self.assertEqual(image.getName(), "image 0")
# No active image
- plot.addImage(((0, 1), (2, 3)), legend='image 2')
+ plot.addImage(((0, 1), (2, 3)), legend="image 2")
plot.setActiveImage(None)
active = plot.getActiveImage()
self.assertIsNone(active)
image = plot.getImage()
- self.assertEqual(image.getName(), 'image 2')
+ self.assertEqual(image.getName(), "image 2")
# Active image
- plot.setActiveImage('image 1')
+ plot.setActiveImage("image 1")
active = plot.getActiveImage()
- self.assertEqual(active.getName(), 'image 1')
+ self.assertEqual(active.getName(), "image 1")
image = plot.getImage()
- self.assertEqual(image.getName(), 'image 1')
+ self.assertEqual(image.getName(), "image 1")
def testGetImageOldApi(self):
"""PlotWidget.getImage and PlotWidget.getActiveImage old API tests"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No image
image = plot.getImage()
@@ -497,18 +505,18 @@ class TestPlotGetCurveImage(unittest.TestCase):
image = numpy.arange(10).astype(numpy.float32)
image.shape = 5, 2
- plot.addImage(image, legend='image 0', info=["Hi!"])
+ plot.addImage(image, legend="image 0", info=["Hi!"])
# Active image
data, legend, info, something, params = plot.getActiveImage()
- self.assertEqual(legend, 'image 0')
+ self.assertEqual(legend, "image 0")
self.assertEqual(info, ["Hi!"])
self.assertTrue(numpy.allclose(data, image), "image 0 data not correct")
def testGetAllImages(self):
"""PlotWidget.getAllImages test"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No image
images = plot.getAllImages()
@@ -516,35 +524,34 @@ class TestPlotGetCurveImage(unittest.TestCase):
# 2 images
data = numpy.arange(100).reshape(10, 10)
- plot.addImage(data, legend='1')
- plot.addImage(data, origin=(10, 10), legend='2')
+ plot.addImage(data, legend="1")
+ plot.addImage(data, origin=(10, 10), legend="2")
images = plot.getAllImages(just_legend=True)
- self.assertEqual(list(images), ['1', '2'])
+ self.assertEqual(list(images), ["1", "2"])
images = plot.getAllImages(just_legend=False)
self.assertEqual(len(images), 2)
- self.assertEqual(images[0].getName(), '1')
- self.assertEqual(images[1].getName(), '2')
+ self.assertEqual(images[0].getName(), "1")
+ self.assertEqual(images[1].getName(), "2")
class TestPlotAddScatter(unittest.TestCase):
"""Test of plot addScatter"""
def testAddGetScatter(self):
-
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
# No curve
scatter = plot._getItem(kind="scatter")
self.assertIsNone(scatter) # No curve
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 0')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 1')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 2')
- plot._setActiveItem('scatter', 'scatter 0')
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 0")
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 1")
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 2")
+ plot.setActiveScatter("scatter 0")
# Active scatter
- active = plot._getActiveItem(kind='scatter')
- self.assertEqual(active.getName(), 'scatter 0')
+ active = plot.getActiveScatter()
+ self.assertEqual(active.getName(), "scatter 0")
# check default values
self.assertAlmostEqual(active.getSymbolSize(), active._DEFAULT_SYMBOL_SIZE)
@@ -562,26 +569,26 @@ class TestPlotAddScatter(unittest.TestCase):
self.assertEqual(s0.getSymbol(), "d")
self.assertAlmostEqual(s0.getAlpha(), 0.777)
- scatter1 = plot._getItem(kind='scatter', legend='scatter 1')
- self.assertEqual(scatter1.getName(), 'scatter 1')
+ scatter1 = plot._getItem(kind="scatter", legend="scatter 1")
+ self.assertEqual(scatter1.getName(), "scatter 1")
def testGetAllScatters(self):
"""PlotWidget.getAllImages test"""
- plot = PlotWidget(backend='none')
+ plot = PlotWidget(backend="none")
items = plot.getItems()
self.assertEqual(len(items), 0)
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 0')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 1')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 2')
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 0")
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 1")
+ plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend="scatter 2")
items = plot.getItems()
self.assertEqual(len(items), 3)
- self.assertEqual(items[0].getName(), 'scatter 0')
- self.assertEqual(items[1].getName(), 'scatter 1')
- self.assertEqual(items[2].getName(), 'scatter 2')
+ self.assertEqual(items[0].getName(), "scatter 0")
+ self.assertEqual(items[1].getName(), "scatter 1")
+ self.assertEqual(items[2].getName(), "scatter 2")
class TestPlotHistogram(unittest.TestCase):
@@ -594,13 +601,13 @@ class TestPlotHistogram(unittest.TestCase):
edgesCenter = numpy.array([-0.5, 0.5, 1.5, 2.5])
# testing x values for right
- edges = _computeEdges(x, 'right')
+ edges = _computeEdges(x, "right")
numpy.testing.assert_array_equal(edges, edgesRight)
- edges = _computeEdges(x, 'center')
+ edges = _computeEdges(x, "center")
numpy.testing.assert_array_equal(edges, edgesCenter)
- edges = _computeEdges(x, 'left')
+ edges = _computeEdges(x, "left")
numpy.testing.assert_array_equal(edges, edgesLeft)
def testHistogramCurve(self):
@@ -608,11 +615,71 @@ class TestPlotHistogram(unittest.TestCase):
edges = numpy.array([0, 1, 2, 3])
xHisto, yHisto = _getHistogramCurve(y, edges)
- numpy.testing.assert_array_equal(
- yHisto, numpy.array([3, 3, 2, 2, 5, 5]))
+ numpy.testing.assert_array_equal(yHisto, numpy.array([3, 3, 2, 2, 5, 5]))
y = numpy.array([-3, 2, 5, 0])
edges = numpy.array([-2, -1, 0, 1, 2])
xHisto, yHisto = _getHistogramCurve(y, edges)
numpy.testing.assert_array_equal(
- yHisto, numpy.array([-3, -3, 2, 2, 5, 5, 0, 0]))
+ yHisto, numpy.array([-3, -3, 2, 2, 5, 5, 0, 0])
+ )
+
+
+def testSetDefaultColors(qWidgetFactory):
+ """Basic test of PlotWidget.get|setDefaultColors"""
+ plot = qWidgetFactory(PlotWidget)
+
+ # By default using config
+ assert numpy.array_equal(
+ plot.getDefaultColors(), silx.config.DEFAULT_PLOT_CURVE_COLORS
+ )
+
+ # Use own colors
+ colors = "red", "green", "blue"
+ plot.setDefaultColors(colors)
+ assert plot.getDefaultColors() == colors
+
+ # Reset to default
+ plot.setDefaultColors(None)
+ assert numpy.array_equal(
+ plot.getDefaultColors(), silx.config.DEFAULT_PLOT_CURVE_COLORS
+ )
+
+
+def testSetDefaultColorsAddCurve(qWidgetFactory):
+ """Test that PlotWidget.setDefaultColors reset color index"""
+ plot = qWidgetFactory(PlotWidget)
+
+ plot.addCurve((0, 1), (0, 0), legend="curve0")
+ plot.addCurve((0, 1), (1, 1), legend="curve1")
+ plot.addCurve((0, 1), (2, 2), legend="curve2")
+
+ colors = "#123456", "#abcdef"
+ plot.setDefaultColors(colors)
+ assert plot.getDefaultColors() == colors
+
+ # Check that the color index is reset
+ curve = plot.addCurve((1, 2), (0, 1), legend="newcurve")
+ assert curve.getColor() == rgba(colors[0])
+
+
+def testDefaultColorsUpdateConfig(qWidgetFactory):
+ """Test that color index is reset if needed when default colors config is updated"""
+ plot = qWidgetFactory(PlotWidget)
+
+ plot.addCurve((0, 1), (0, 0), legend="curve0")
+ plot.addCurve((0, 1), (1, 1), legend="curve1")
+ plot.addCurve((0, 1), (2, 2), legend="curve2")
+
+ previous_colors = silx.config.DEFAULT_PLOT_CURVE_COLORS
+ try:
+ colors = "#123456", "#abcdef"
+ silx.config.DEFAULT_PLOT_CURVE_COLORS = colors
+ assert plot.getDefaultColors() == colors
+
+ # Check that the color index is reset
+ curve = plot.addCurve((1, 2), (0, 1), legend="newcurve")
+ assert curve.getColor() == rgba(colors[0])
+
+ finally:
+ silx.config.DEFAULT_PLOT_CURVE_COLORS = previous_colors
diff --git a/src/silx/gui/plot/test/testPlotWindow.py b/src/silx/gui/plot/test/testPlotWindow.py
index 9e1497f..8f17bf1 100644
--- a/src/silx/gui/plot/test/testPlotWindow.py
+++ b/src/silx/gui/plot/test/testPlotWindow.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "27/06/2017"
-import unittest
import numpy
import pytest
@@ -73,12 +71,14 @@ class TestPlotWindow(TestCaseQt):
toolButton = getQToolButtonFromAction(action)
self.assertIsNot(toolButton, None)
self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.assertNotEqual(getter(), initialState,
- msg='"%s" state not changed' % action.text())
+ self.assertNotEqual(
+ getter(), initialState, msg='"%s" state not changed' % action.text()
+ )
self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.assertEqual(getter(), initialState,
- msg='"%s" state not changed' % action.text())
+ self.assertEqual(
+ getter(), initialState, msg='"%s" state not changed' % action.text()
+ )
# Trigger a zoom reset
self.mouseMove(self.plot)
@@ -89,8 +89,8 @@ class TestPlotWindow(TestCaseQt):
def testDockWidgets(self):
"""Test add/remove dock widgets"""
- dock1 = qt.QDockWidget('Test 1')
- dock1.setWidget(qt.QLabel('Test 1'))
+ dock1 = qt.QDockWidget("Test 1")
+ dock1.setWidget(qt.QLabel("Test 1"))
self.plot.addTabbedDockWidget(dock1)
self.qapp.processEvents()
@@ -98,17 +98,17 @@ class TestPlotWindow(TestCaseQt):
self.plot.removeDockWidget(dock1)
self.qapp.processEvents()
- dock2 = qt.QDockWidget('Test 2')
- dock2.setWidget(qt.QLabel('Test 2'))
+ dock2 = qt.QDockWidget("Test 2")
+ dock2.setWidget(qt.QLabel("Test 2"))
self.plot.addTabbedDockWidget(dock2)
self.qapp.processEvents()
- if qt.BINDING != 'PySide2':
- # Weird bug with PySide2 later upon gc.collect() when getting the layout
- self.assertNotEqual(self.plot.layout().indexOf(dock2),
- -1,
- "dock2 not properly displayed")
+ self.assertNotEqual(
+ self.plot.layout().indexOf(dock2),
+ -1,
+ "dock2 not properly displayed",
+ )
def testToolAspectRatio(self):
self.plot.toolBar()
@@ -129,12 +129,14 @@ class TestPlotWindow(TestCaseQt):
old = Colormap._computeAutoscaleRange
self._count = 0
+
def _computeAutoscaleRange(colormap, data):
self._count = self._count + 1
return 10, 20
+
Colormap._computeAutoscaleRange = _computeAutoscaleRange
try:
- colormap = Colormap(name='red')
+ colormap = Colormap(name="red")
self.plot.setVisible(True)
# Add an image
@@ -164,11 +166,10 @@ class TestPlotWindow(TestCaseQt):
ylimits = self.plot.getYAxis().getLimits()
isKeepAspectRatio = self.plot.isKeepDataAspectRatio()
- for backend in ('gl', 'mpl'):
+ for backend in ("gl", "mpl"):
with self.subTest():
self.plot.setBackend(backend)
self.plot.replot()
self.assertEqual(self.plot.getXAxis().getLimits(), xlimits)
self.assertEqual(self.plot.getYAxis().getLimits(), ylimits)
- self.assertEqual(
- self.plot.isKeepDataAspectRatio(), isKeepAspectRatio)
+ self.assertEqual(self.plot.isKeepDataAspectRatio(), isKeepAspectRatio)
diff --git a/src/silx/gui/plot/test/testRoiStatsWidget.py b/src/silx/gui/plot/test/testRoiStatsWidget.py
index eb29267..759ebe2 100644
--- a/src/silx/gui/plot/test/testRoiStatsWidget.py
+++ b/src/silx/gui/plot/test/testRoiStatsWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
@@ -33,47 +32,49 @@ from silx.gui.plot.ROIStatsWidget import ROIStatsWidget
from silx.gui.plot.CurvesROIWidget import ROI
from silx.gui.plot.items.roi import RectangleROI, PolygonROI
from silx.gui.plot.StatsWidget import UpdateMode
-import unittest
import numpy
-
class _TestRoiStatsBase(TestCaseQt):
"""Base class for several unittest relative to ROIStatsWidget"""
+
def setUp(self):
TestCaseQt.setUp(self)
# define plot
self.plot = PlotWindow()
- self.plot.addImage(numpy.arange(10000).reshape(100, 100),
- legend='img1')
- self.img_item = self.plot.getImage('img1')
- self.plot.addCurve(x=numpy.linspace(0, 10, 56), y=numpy.arange(56),
- legend='curve1')
- self.curve_item = self.plot.getCurve('curve1')
- self.plot.addHistogram(edges=numpy.linspace(0, 10, 56),
- histogram=numpy.arange(56), legend='histo1')
- self.histogram_item = self.plot.getHistogram(legend='histo1')
- self.plot.addScatter(x=numpy.linspace(0, 10, 56),
- y=numpy.linspace(0, 10, 56),
- value=numpy.arange(56),
- legend='scatter1')
- self.scatter_item = self.plot.getScatter(legend='scatter1')
+ self.plot.addImage(numpy.arange(10000).reshape(100, 100), legend="img1")
+ self.img_item = self.plot.getImage("img1")
+ self.plot.addCurve(
+ x=numpy.linspace(0, 10, 56), y=numpy.arange(56), legend="curve1"
+ )
+ self.curve_item = self.plot.getCurve("curve1")
+ self.plot.addHistogram(
+ edges=numpy.linspace(0, 10, 56), histogram=numpy.arange(56), legend="histo1"
+ )
+ self.histogram_item = self.plot.getHistogram(legend="histo1")
+ self.plot.addScatter(
+ x=numpy.linspace(0, 10, 56),
+ y=numpy.linspace(0, 10, 56),
+ value=numpy.arange(56),
+ legend="scatter1",
+ )
+ self.scatter_item = self.plot.getScatter(legend="scatter1")
# stats widget
self.statsWidget = ROIStatsWidget(plot=self.plot)
# define stats
stats = [
- ('sum', numpy.sum),
- ('mean', numpy.mean),
+ ("sum", numpy.sum),
+ ("mean", numpy.mean),
]
self.statsWidget.setStats(stats=stats)
# define rois
- self.roi1D = ROI(name='range1', fromdata=0, todata=4, type_='energy')
+ self.roi1D = ROI(name="range1", fromdata=0, todata=4, type_="energy")
self.rectangle_roi = RectangleROI()
self.rectangle_roi.setGeometry(origin=(0, 0), size=(20, 20))
- self.rectangle_roi.setName('Initial ROI')
+ self.rectangle_roi.setName("Initial ROI")
self.polygon_roi = PolygonROI()
points = numpy.array([[0, 5], [5, 0], [10, 5], [5, 10]])
self.polygon_roi.setPoints(points)
@@ -96,182 +97,164 @@ class TestRoiStatsCouple(_TestRoiStatsBase):
"""
Test different possible couple (roi, plotItem).
Check that:
-
+
* computation is correct if couple is valid
* raise an error if couple is invalid
"""
+
def testROICurve(self):
"""
- Test that the couple (ROI, curveItem) can be used for stats
+ Test that the couple (ROI, curveItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.curve_item)
+ item = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.curve_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
+ self.assertEqual(tableItems["sum"].text(), "253")
+ self.assertEqual(tableItems["mean"].text(), "11.0")
def testRectangleImage(self):
"""
- Test that the couple (RectangleROI, imageItem) can be used for stats
+ Test that the couple (RectangleROI, imageItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
- self.plot.addImage(numpy.ones(10000).reshape(100, 100),
- legend='img1')
+ self.plot.addImage(numpy.ones(10000).reshape(100, 100), legend="img1")
self.qapp.processEvents()
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), str(float(21*21)))
- self.assertEqual(tableItems['mean'].text(), '1.0')
+ self.assertEqual(tableItems["sum"].text(), str(float(21 * 21)))
+ self.assertEqual(tableItems["mean"].text(), "1.0")
def testPolygonImage(self):
"""
- Test that the couple (PolygonROI, imageItem) can be used for stats
+ Test that the couple (PolygonROI, imageItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.polygon_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.polygon_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '22750')
- self.assertEqual(tableItems['mean'].text(), '455.0')
+ self.assertEqual(tableItems["sum"].text(), "22750")
+ self.assertEqual(tableItems["mean"].text(), "455.0")
def testROIImage(self):
"""
- Test that the couple (ROI, imageItem) is raising an error
+ Test that the couple (ROI, imageItem) is raising an error
"""
with self.assertRaises(TypeError):
- self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.img_item)
+ self.statsWidget.addItem(roi=self.roi1D, plotItem=self.img_item)
def testRectangleCurve(self):
"""
- Test that the couple (rectangleROI, curveItem) is raising an error
+ Test that the couple (rectangleROI, curveItem) is raising an error
"""
with self.assertRaises(TypeError):
- self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.curve_item)
+ self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.curve_item)
def testROIHistogram(self):
"""
- Test that the couple (PolygonROI, imageItem) can be used for stats
+ Test that the couple (PolygonROI, imageItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
+ item = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.histogram_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
+ self.assertEqual(tableItems["sum"].text(), "253")
+ self.assertEqual(tableItems["mean"].text(), "11.0")
def testROIScatter(self):
"""
- Test that the couple (PolygonROI, imageItem) can be used for stats
+ Test that the couple (PolygonROI, imageItem) can be used for stats
"""
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.scatter_item)
+ item = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.scatter_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
+ self.assertEqual(tableItems["sum"].text(), "253")
+ self.assertEqual(tableItems["mean"].text(), "11.0")
class TestRoiStatsAddRemoveItem(_TestRoiStatsBase):
"""Test adding and removing (roi, plotItem) items"""
+
def testAddRemoveItems(self):
- item1 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.scatter_item)
+ item1 = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.scatter_item)
self.assertTrue(item1 is not None)
self.assertEqual(self.statsTable().rowCount(), 1)
- item2 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
+ item2 = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.histogram_item)
self.assertTrue(item2 is not None)
self.assertEqual(self.statsTable().rowCount(), 2)
# try to add twice the same item
- item3 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
+ item3 = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.histogram_item)
self.assertTrue(item3 is None)
self.assertEqual(self.statsTable().rowCount(), 2)
- item4 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.curve_item)
+ item4 = self.statsWidget.addItem(roi=self.roi1D, plotItem=self.curve_item)
self.assertTrue(item4 is not None)
self.assertEqual(self.statsTable().rowCount(), 3)
- self.statsWidget.removeItem(plotItem=item4._plot_item,
- roi=item4._roi)
+ self.statsWidget.removeItem(plotItem=item4._plot_item, roi=item4._roi)
self.assertEqual(self.statsTable().rowCount(), 2)
# try to remove twice the same item
- self.statsWidget.removeItem(plotItem=item4._plot_item,
- roi=item4._roi)
+ self.statsWidget.removeItem(plotItem=item4._plot_item, roi=item4._roi)
self.assertEqual(self.statsTable().rowCount(), 2)
- self.statsWidget.removeItem(plotItem=item2._plot_item,
- roi=item2._roi)
- self.statsWidget.removeItem(plotItem=item1._plot_item,
- roi=item1._roi)
+ self.statsWidget.removeItem(plotItem=item2._plot_item, roi=item2._roi)
+ self.statsWidget.removeItem(plotItem=item1._plot_item, roi=item1._roi)
self.assertEqual(self.statsTable().rowCount(), 0)
class TestRoiStatsRoiUpdate(_TestRoiStatsBase):
"""Test that the stats will be updated if the roi is updated"""
+
def testChangeRoi(self):
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '445410')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
+ self.assertEqual(tableItems["sum"].text(), "445410")
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
# update roi
self.rectangle_roi.setOrigin(position=(10, 10))
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
+ self.assertNotEqual(tableItems["sum"].text(), "445410")
+ self.assertNotEqual(tableItems["mean"].text(), "1010.0")
def testUpdateModeScenario(self):
"""Test update according to a simple scenario"""
self.statsWidget._setUpdateMode(UpdateMode.AUTO)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '445410')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
+ self.assertEqual(tableItems["sum"].text(), "445410")
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
self.statsWidget._setUpdateMode(UpdateMode.MANUAL)
self.rectangle_roi.setOrigin(position=(10, 10))
self.qapp.processEvents()
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
+ self.assertNotEqual(tableItems["sum"].text(), "445410")
+ self.assertNotEqual(tableItems["mean"].text(), "1010.0")
self.statsWidget._updateAllStats(is_request=True)
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
+ self.assertNotEqual(tableItems["sum"].text(), "445410")
+ self.assertNotEqual(tableItems["mean"].text(), "1010.0")
class TestRoiStatsPlotItemUpdate(_TestRoiStatsBase):
"""Test that the stats will be updated if the plot item is updated"""
+
def testChangeImage(self):
self.statsWidget._setUpdateMode(UpdateMode.AUTO)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['mean'].text(), '1010.0')
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
# update plot
- self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100),
- legend='img1')
- self.assertNotEqual(tableItems['mean'].text(), '1059.5')
+ self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100), legend="img1")
+ self.assertNotEqual(tableItems["mean"].text(), "1059.5")
def testUpdateModeScenario(self):
"""Test update according to a simple scenario"""
self.statsWidget._setUpdateMode(UpdateMode.MANUAL)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
+ item = self.statsWidget.addItem(roi=self.rectangle_roi, plotItem=self.img_item)
assert item is not None
tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['mean'].text(), '1010.0')
- self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100),
- legend='img1')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
+ self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100), legend="img1")
+ self.assertEqual(tableItems["mean"].text(), "1010.0")
self.statsWidget._updateAllStats(is_request=True)
- self.assertEqual(tableItems['mean'].text(), '1110.0')
+ self.assertEqual(tableItems["mean"].text(), "1110.0")
diff --git a/src/silx/gui/plot/test/testSaveAction.py b/src/silx/gui/plot/test/testSaveAction.py
index 9280fb6..f8ac7ee 100644
--- a/src/silx/gui/plot/test/testSaveAction.py
+++ b/src/silx/gui/plot/test/testSaveAction.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
@@ -40,9 +39,8 @@ from silx.gui.plot.actions.io import SaveAction
class TestSaveActionSaveCurvesAsSpec(unittest.TestCase):
-
def setUp(self):
- self.plot = PlotWidget(backend='none')
+ self.plot = PlotWidget(backend="none")
self.saveAction = SaveAction(plot=self.plot)
self.tempdir = tempfile.mkdtemp()
@@ -57,17 +55,16 @@ class TestSaveActionSaveCurvesAsSpec(unittest.TestCase):
self.plot.setGraphXLabel("graph x label")
self.plot.setGraphYLabel("graph y label")
- self.plot.addCurve([0, 1], [1, 2], "curve with labels",
- xlabel="curve0 X", ylabel="curve0 Y")
- self.plot.addCurve([-1, 3], [-6, 2], "curve with X label",
- xlabel="curve1 X")
- self.plot.addCurve([-2, 0], [8, 12], "curve with Y label",
- ylabel="curve2 Y")
+ self.plot.addCurve(
+ [0, 1], [1, 2], "curve with labels", xlabel="curve0 X", ylabel="curve0 Y"
+ )
+ self.plot.addCurve([-1, 3], [-6, 2], "curve with X label", xlabel="curve1 X")
+ self.plot.addCurve([-2, 0], [8, 12], "curve with Y label", ylabel="curve2 Y")
self.plot.addCurve([3, 1], [7, 6], "curve with no labels")
- self.saveAction._saveCurves(self.plot,
- self.out_fname,
- SaveAction.DEFAULT_ALL_CURVES_FILTERS[0]) # "All curves as SpecFile (*.dat)"
+ self.saveAction._saveCurves(
+ self.plot, self.out_fname, SaveAction.DEFAULT_ALL_CURVES_FILTERS[0]
+ ) # "All curves as SpecFile (*.dat)"
with open(self.out_fname, "rb") as f:
file_content = f.read()
@@ -100,33 +97,35 @@ class TestSaveActionExtension(PlotWidgetTestCase):
saveAction = SaveAction(plot=self.plot, parent=self.plot)
# Add a new file filter
- nameFilter = 'Dummy file (*.dummy)'
- saveAction.setFileFilter('all', nameFilter, self._dummySaveFunction)
- self.assertTrue(nameFilter in saveAction.getFileFilters('all'))
- self.assertEqual(saveAction.getFileFilters('all')[nameFilter],
- self._dummySaveFunction)
+ nameFilter = "Dummy file (*.dummy)"
+ saveAction.setFileFilter("all", nameFilter, self._dummySaveFunction)
+ self.assertTrue(nameFilter in saveAction.getFileFilters("all"))
+ self.assertEqual(
+ saveAction.getFileFilters("all")[nameFilter], self._dummySaveFunction
+ )
# Add a new file filter at a particular position
- nameFilter = 'Dummy file2 (*.dummy)'
- saveAction.setFileFilter('all', nameFilter,
- self._dummySaveFunction, index=3)
- self.assertTrue(nameFilter in saveAction.getFileFilters('all'))
- filters = saveAction.getFileFilters('all')
+ nameFilter = "Dummy file2 (*.dummy)"
+ saveAction.setFileFilter("all", nameFilter, self._dummySaveFunction, index=3)
+ self.assertTrue(nameFilter in saveAction.getFileFilters("all"))
+ filters = saveAction.getFileFilters("all")
self.assertEqual(filters[nameFilter], self._dummySaveFunction)
- self.assertEqual(list(filters.keys()).index(nameFilter),3)
+ self.assertEqual(list(filters.keys()).index(nameFilter), 3)
# Update an existing file filter
nameFilter = SaveAction.IMAGE_FILTER_EDF
- saveAction.setFileFilter('image', nameFilter, self._dummySaveFunction)
- self.assertEqual(saveAction.getFileFilters('image')[nameFilter],
- self._dummySaveFunction)
+ saveAction.setFileFilter("image", nameFilter, self._dummySaveFunction)
+ self.assertEqual(
+ saveAction.getFileFilters("image")[nameFilter], self._dummySaveFunction
+ )
# Change the position of an existing file filter
- nameFilter = 'Dummy file2 (*.dummy)'
- oldIndex = list(saveAction.getFileFilters('all')).index(nameFilter)
+ nameFilter = "Dummy file2 (*.dummy)"
+ oldIndex = list(saveAction.getFileFilters("all")).index(nameFilter)
newIndex = oldIndex - 1
- saveAction.setFileFilter('all', nameFilter,
- self._dummySaveFunction, index=newIndex)
- filters = saveAction.getFileFilters('all')
+ saveAction.setFileFilter(
+ "all", nameFilter, self._dummySaveFunction, index=newIndex
+ )
+ filters = saveAction.getFileFilters("all")
self.assertEqual(filters[nameFilter], self._dummySaveFunction)
self.assertEqual(list(filters.keys()).index(nameFilter), newIndex)
diff --git a/src/silx/gui/plot/test/testScatterMaskToolsWidget.py b/src/silx/gui/plot/test/testScatterMaskToolsWidget.py
index 447ee58..5dc14e1 100644
--- a/src/silx/gui/plot/test/testScatterMaskToolsWidget.py
+++ b/src/silx/gui/plot/test/testScatterMaskToolsWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
@@ -31,7 +30,6 @@ __date__ = "17/01/2018"
import logging
import os.path
-import unittest
import numpy
@@ -42,8 +40,6 @@ from silx.gui.utils.testutils import getQToolButtonFromAction
from silx.gui.plot import PlotWindow, ScatterMaskToolsWidget
from .utils import PlotWidgetTestCase
-import fabio
-
_logger = logging.getLogger(__name__)
@@ -57,7 +53,8 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def setUp(self):
super(TestScatterMaskToolsWidget, self).setUp()
self.widget = ScatterMaskToolsWidget.ScatterMaskToolsDockWidget(
- plot=self.plot, name='TEST')
+ plot=self.plot, name="TEST"
+ )
self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, self.widget)
self.maskWidget = self.widget.widget()
@@ -69,10 +66,10 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def testEmptyPlot(self):
"""Empty plot, display MaskToolsDockWidget, toggle multiple masks"""
- self.maskWidget.setMultipleMasks('single')
+ self.maskWidget.setMultipleMasks("single")
self.qapp.processEvents()
- self.maskWidget.setMultipleMasks('exclusive')
+ self.maskWidget.setMultipleMasks("exclusive")
self.qapp.processEvents()
def _drag(self):
@@ -103,12 +100,14 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
x, y = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset),
- (x, y + offset)] # Close polygon
+ star = [
+ (x, y + offset),
+ (x - offset, y - offset),
+ (x + offset, y),
+ (x - offset, y),
+ (x + offset, y - offset),
+ (x, y + offset),
+ ] # Close polygon
self.mouseMove(plot, pos=[0, 0])
for pos in star:
@@ -125,41 +124,44 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
x, y = plot.width() // 2, plot.height() // 2
offset = min(plot.width(), plot.height()) // 10
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset)]
+ star = [
+ (x, y + offset),
+ (x - offset, y - offset),
+ (x + offset, y),
+ (x - offset, y),
+ (x + offset, y - offset),
+ ]
self.mouseMove(plot, pos=[0, 0])
self.mouseMove(plot, pos=star[0])
self.mousePress(plot, qt.Qt.LeftButton, pos=star[0])
for pos in star[1:]:
self.mouseMove(plot, pos=pos)
- self.mouseRelease(
- plot, qt.Qt.LeftButton, pos=star[-1])
+ self.mouseRelease(plot, qt.Qt.LeftButton, pos=star[-1])
def testWithAScatter(self):
"""Plot with a Scatter: test MaskToolsWidget interactions"""
# Add and remove a scatter (this should enable/disable GUI + change mask)
self.plot.addScatter(
- x=numpy.arange(256),
- y=numpy.arange(256),
- value=numpy.random.random(256),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
+ x=numpy.arange(256),
+ y=numpy.arange(256),
+ value=numpy.random.random(256),
+ legend="test",
+ )
+ self.plot.setActiveScatter("test")
self.qapp.processEvents()
- self.plot.remove('test', kind='scatter')
+ self.plot.remove("test", kind="scatter")
self.qapp.processEvents()
self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.random.random(1000),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
+ x=numpy.arange(1000),
+ y=1000 * (numpy.arange(1000) % 20),
+ value=numpy.random.random(1000),
+ legend="test",
+ )
+ self.plot.setActiveScatter("test")
self.plot.resetZoom()
self.qapp.processEvents()
@@ -173,15 +175,13 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.qapp.processEvents()
self._drag()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertFalse(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# unmask same region
self.maskWidget.maskStateGroup.button(0).click()
self.qapp.processEvents()
self._drag()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertTrue(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# Test draw polygon #
toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction)
@@ -192,15 +192,13 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.maskWidget.maskStateGroup.button(1).click()
self.qapp.processEvents()
self._drawPolygon()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertFalse(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# unmask same region
self.maskWidget.maskStateGroup.button(0).click()
self.qapp.processEvents()
self._drawPolygon()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertTrue(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# Test draw pencil #
toolButton = getQToolButtonFromAction(self.maskWidget.pencilAction)
@@ -214,15 +212,13 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.maskWidget.maskStateGroup.button(1).click()
self.qapp.processEvents()
self._drawPencil()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertFalse(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# unmask same region
self.maskWidget.maskStateGroup.button(0).click()
self.qapp.processEvents()
self._drawPencil()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ self.assertTrue(numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
# Test no draw tool #
toolButton = getQToolButtonFromAction(self.maskWidget.browseAction)
@@ -233,11 +229,12 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def __loadSave(self, file_format):
self.plot.addScatter(
- x=numpy.arange(256),
- y=25 * (numpy.arange(256) % 10),
- value=numpy.random.random(256),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
+ x=numpy.arange(256),
+ y=25 * (numpy.arange(256) % 10),
+ value=numpy.random.random(256),
+ legend="test",
+ )
+ self.plot.setActiveScatter("test")
self.plot.resetZoom()
self.qapp.processEvents()
@@ -251,16 +248,18 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
self.assertFalse(numpy.all(numpy.equal(ref_mask, 0)))
with temp_dir() as tmp:
- mask_filename = os.path.join(tmp, 'mask.' + file_format)
+ mask_filename = os.path.join(tmp, "mask." + file_format)
self.maskWidget.save(mask_filename, file_format)
self.maskWidget.resetSelectionMask()
self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0))
+ )
self.maskWidget.load(mask_filename)
- self.assertTrue(numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(), ref_mask)))
+ self.assertTrue(
+ numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), ref_mask))
+ )
def testLoadSaveNpy(self):
self.__loadSave("npy")
@@ -271,22 +270,24 @@ class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
def testSigMaskChangedEmitted(self):
self.qapp.processEvents()
self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.ones((1000,)),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
+ x=numpy.arange(1000),
+ y=1000 * (numpy.arange(1000) % 20),
+ value=numpy.ones((1000,)),
+ legend="test",
+ )
+ self.plot.setActiveScatter("test")
self.plot.resetZoom()
self.qapp.processEvents()
- self.plot.remove('test', kind='scatter')
+ self.plot.remove("test", kind="scatter")
self.qapp.processEvents()
self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.random.random(1000),
- legend='test')
+ x=numpy.arange(1000),
+ y=1000 * (numpy.arange(1000) % 20),
+ value=numpy.random.random(1000),
+ legend="test",
+ )
l = []
diff --git a/src/silx/gui/plot/test/testScatterView.py b/src/silx/gui/plot/test/testScatterView.py
index d11d4d8..d6853b1 100644
--- a/src/silx/gui/plot/test/testScatterView.py
+++ b/src/silx/gui/plot/test/testScatterView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -29,8 +28,6 @@ __license__ = "MIT"
__date__ = "06/03/2018"
-import unittest
-
import numpy
from silx.gui.plot.items import Axis, Scatter
@@ -84,7 +81,7 @@ class TestScatterView(PlotWidgetTestCase):
scale = self.plot.getYAxis().getScale()
self.assertEqual(scale, Axis.LINEAR)
- title = 'Test ScatterView'
+ title = "Test ScatterView"
self.plot.setGraphTitle(title)
self.assertEqual(self.plot.getGraphTitle(), title)
@@ -108,13 +105,15 @@ class TestScatterView(PlotWidgetTestCase):
_pts = 100
_levels = 100
_fwhm = 50
- x = numpy.random.rand(_pts)*_levels
- y = numpy.random.rand(_pts)*_levels
- value = numpy.random.rand(_pts)*_levels
- x0 = x[int(_pts/2)]
- y0 = x[int(_pts/2)]
- #2D Gaussian kernel
- alpha = numpy.exp(-4*numpy.log(2) * ((x-x0)**2 + (y-y0)**2) / _fwhm**2)
+ x = numpy.random.rand(_pts) * _levels
+ y = numpy.random.rand(_pts) * _levels
+ value = numpy.random.rand(_pts) * _levels
+ x0 = x[int(_pts / 2)]
+ y0 = x[int(_pts / 2)]
+ # 2D Gaussian kernel
+ alpha = numpy.exp(
+ -4 * numpy.log(2) * ((x - x0) ** 2 + (y - y0) ** 2) / _fwhm**2
+ )
self.plot.setData(x, y, value, alpha=alpha)
self.qapp.processEvents()
diff --git a/src/silx/gui/plot/test/testStackView.py b/src/silx/gui/plot/test/testStackView.py
index 0d18113..5e0ead5 100644
--- a/src/silx/gui/plot/test/testStackView.py
+++ b/src/silx/gui/plot/test/testStackView.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "20/03/2017"
-import unittest
import numpy
from silx.gui.utils.testutils import TestCaseQt, SignalListener
@@ -50,8 +48,10 @@ class TestStackView(TestCaseQt):
self.stackview.show()
self.qWaitForWindowExposed(self.stackview)
self.mystack = numpy.fromfunction(
- lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.),
- (10, 20, 30)
+ lambda i, j, k: numpy.sin(i / 15.0)
+ + numpy.cos(j / 4.0)
+ + 2 * numpy.sin(k / 6.0),
+ (10, 20, 30),
)
def tearDown(self):
@@ -75,13 +75,11 @@ class TestStackView(TestCaseQt):
def testSetStack(self):
self.stackview.setStack(self.mystack)
- self.stackview.setColormap("viridis", autoscale=True)
+ self.stackview.setColormap("viridis")
my_trans_stack, params = self.stackview.getStack()
self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertEqual(params["colormap"]["name"],
- "viridis")
+ self.assertTrue(numpy.array_equal(self.mystack, my_trans_stack))
+ self.assertEqual(params["colormap"]["name"], "viridis")
def testSetStackPerspective(self):
self.stackview.setStack(self.mystack, perspective=1)
@@ -89,10 +87,15 @@ class TestStackView(TestCaseQt):
my_trans_stack, params = self.stackview.getCurrentView()
# get stack returns the transposed data, depending on the perspective
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]))
- self.assertTrue(numpy.array_equal(numpy.transpose(self.mystack, axes=(1, 0, 2)),
- my_trans_stack))
+ self.assertEqual(
+ my_trans_stack.shape,
+ (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]),
+ )
+ self.assertTrue(
+ numpy.array_equal(
+ numpy.transpose(self.mystack, axes=(1, 0, 2)), my_trans_stack
+ )
+ )
def testSetStackListOfImages(self):
loi = [self.mystack[i] for i in range(self.mystack.shape[0])]
@@ -101,10 +104,8 @@ class TestStackView(TestCaseQt):
my_orig_stack, params = self.stackview.getStack(returnNumpyArray=True)
my_trans_stack, params = self.stackview.getStack(returnNumpyArray=True)
self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertTrue(numpy.array_equal(self.mystack,
- my_orig_stack))
+ self.assertTrue(numpy.array_equal(self.mystack, my_trans_stack))
+ self.assertTrue(numpy.array_equal(self.mystack, my_orig_stack))
self.assertIsInstance(my_trans_stack, numpy.ndarray)
self.stackview.setStack(loi, perspective=2)
@@ -114,88 +115,100 @@ class TestStackView(TestCaseQt):
self.assertIs(my_orig_stack, loi)
# getCurrentView(copy=False) returns a ListOfImages whose .images
# attr is the original data
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[2], self.mystack.shape[0], self.mystack.shape[1]))
- self.assertTrue(numpy.array_equal(numpy.array(my_trans_stack),
- numpy.transpose(self.mystack, axes=(2, 0, 1))))
- self.assertIsInstance(my_trans_stack,
- ListOfImages) # returnNumpyArray=False by default in getStack
+ self.assertEqual(
+ my_trans_stack.shape,
+ (self.mystack.shape[2], self.mystack.shape[0], self.mystack.shape[1]),
+ )
+ self.assertTrue(
+ numpy.array_equal(
+ numpy.array(my_trans_stack),
+ numpy.transpose(self.mystack, axes=(2, 0, 1)),
+ )
+ )
+ self.assertIsInstance(
+ my_trans_stack, ListOfImages
+ ) # returnNumpyArray=False by default in getStack
self.assertIs(my_trans_stack.images, loi)
def testPerspective(self):
self.stackview.setStack(numpy.arange(24).reshape((2, 3, 4)))
- self.assertEqual(self.stackview._perspective, 0,
- "Default perspective is not 0 (dim1-dim2).")
+ self.assertEqual(
+ self.stackview._perspective, 0, "Default perspective is not 0 (dim1-dim2)."
+ )
self.stackview._StackView__planeSelection.setPerspective(1)
- self.assertEqual(self.stackview._perspective, 1,
- "Plane selection combobox not updating perspective")
+ self.assertEqual(
+ self.stackview._perspective,
+ 1,
+ "Plane selection combobox not updating perspective",
+ )
self.stackview.setStack(numpy.arange(6).reshape((1, 2, 3)))
- self.assertEqual(self.stackview._perspective, 1,
- "Perspective not preserved when calling setStack "
- "without specifying the perspective parameter.")
+ self.assertEqual(
+ self.stackview._perspective,
+ 1,
+ "Perspective not preserved when calling setStack "
+ "without specifying the perspective parameter.",
+ )
self.stackview.setStack(numpy.arange(24).reshape((2, 3, 4)), perspective=2)
- self.assertEqual(self.stackview._perspective, 2,
- "Perspective not set in setStack(..., perspective=2).")
+ self.assertEqual(
+ self.stackview._perspective,
+ 2,
+ "Perspective not set in setStack(..., perspective=2).",
+ )
def testDefaultTitle(self):
"""Test that the plot title contains the proper Z information"""
- self.stackview.setStack(numpy.arange(24).reshape((4, 3, 2)),
- calibrations=[(0, 1), (-10, 10), (3.14, 3.14)])
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=0")
+ self.stackview.setStack(
+ numpy.arange(24).reshape((4, 3, 2)),
+ calibrations=[(0, 1), (-10, 10), (3.14, 3.14)],
+ )
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=0")
self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=2")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=2")
self.stackview._StackView__planeSelection.setPerspective(1)
self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=-10")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=-10")
self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=10")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=10")
self.stackview._StackView__planeSelection.setPerspective(2)
self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=3.14")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=3.14")
self.stackview.setFrameNumber(1)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=6.28")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Image z=6.28")
def testCustomTitle(self):
"""Test setting the plot title with a user defined callback"""
- self.stackview.setStack(numpy.arange(24).reshape((4, 3, 2)),
- calibrations=[(0, 1), (-10, 10), (3.14, 3.14)])
+ self.stackview.setStack(
+ numpy.arange(24).reshape((4, 3, 2)),
+ calibrations=[(0, 1), (-10, 10), (3.14, 3.14)],
+ )
def title_callback(frame_idx):
return "Cubed index title %d" % (frame_idx**3)
self.stackview.setTitleCallback(title_callback)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 0")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Cubed index title 0")
self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 8")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Cubed index title 8")
# perspective should not matter, only frame index
self.stackview._StackView__planeSelection.setPerspective(1)
self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 0")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Cubed index title 0")
self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 8")
+ self.assertEqual(self.stackview._plot.getGraphTitle(), "Cubed index title 8")
with self.assertRaises(TypeError):
# setTitleCallback should not accept non-callable objects like strings
self.stackview.setTitleCallback(
- "Là, vous faites sirop de vingt-et-un et vous dites : "
- "beau sirop, mi-sirop, siroté, gagne-sirop, sirop-grelot,"
- " passe-montagne, sirop au bon goût.")
+ "Là, vous faites sirop de vingt-et-un et vous dites : "
+ "beau sirop, mi-sirop, siroté, gagne-sirop, sirop-grelot,"
+ " passe-montagne, sirop au bon goût."
+ )
def testStackFrameNumber(self):
self.stackview.setStack(self.mystack)
@@ -218,8 +231,10 @@ class TestStackViewMainWindow(TestCaseQt):
self.stackview.show()
self.qWaitForWindowExposed(self.stackview)
self.mystack = numpy.fromfunction(
- lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.),
- (10, 20, 30)
+ lambda i, j, k: numpy.sin(i / 15.0)
+ + numpy.cos(j / 4.0)
+ + 2 * numpy.sin(k / 6.0),
+ (10, 20, 30),
)
def tearDown(self):
@@ -230,19 +245,22 @@ class TestStackViewMainWindow(TestCaseQt):
def testSetStack(self):
self.stackview.setStack(self.mystack)
- self.stackview.setColormap("viridis", autoscale=True)
+ self.stackview.setColormap("viridis")
my_trans_stack, params = self.stackview.getStack()
self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertEqual(params["colormap"]["name"],
- "viridis")
+ self.assertTrue(numpy.array_equal(self.mystack, my_trans_stack))
+ self.assertEqual(params["colormap"]["name"], "viridis")
def testSetStackPerspective(self):
self.stackview.setStack(self.mystack, perspective=1)
my_trans_stack, params = self.stackview.getCurrentView()
# get stack returns the transposed data, depending on the perspective
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]))
- self.assertTrue(numpy.array_equal(numpy.transpose(self.mystack, axes=(1, 0, 2)),
- my_trans_stack))
+ self.assertEqual(
+ my_trans_stack.shape,
+ (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]),
+ )
+ self.assertTrue(
+ numpy.array_equal(
+ numpy.transpose(self.mystack, axes=(1, 0, 2)), my_trans_stack
+ )
+ )
diff --git a/src/silx/gui/plot/test/testStats.py b/src/silx/gui/plot/test/testStats.py
index 0a792a4..2a2793e 100644
--- a/src/silx/gui/plot/test/testStats.py
+++ b/src/silx/gui/plot/test/testStats.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -35,13 +34,11 @@ from silx.gui.plot import StatsWidget
from silx.gui.plot.stats import statshandler
from silx.gui.utils.testutils import TestCaseQt, SignalListener
from silx.gui.plot import Plot1D, Plot2D
-from silx.gui.plot3d.SceneWidget import SceneWidget
from silx.gui.plot.items.roi import RectangleROI, PolygonROI
-from silx.gui.plot.tools.roi import RegionOfInterestManager
+from silx.gui.plot.tools.roi import RegionOfInterestManager
from silx.gui.plot.stats.stats import Stats
from silx.gui.plot.CurvesROIWidget import ROI
from silx.utils.testutils import ParametricTestCase
-import unittest
import logging
import numpy
@@ -50,6 +47,7 @@ _logger = logging.getLogger(__name__)
class TestStatsBase(object):
"""Base class for stats TestCase"""
+
def setUp(self):
self.createCurveContext()
self.createImageContext()
@@ -70,51 +68,52 @@ class TestStatsBase(object):
self.plot1d = Plot1D()
x = range(20)
y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
+ self.plot1d.addCurve(x, y, legend="curve0")
self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
+ item=self.plot1d.getCurve("curve0"),
plot=self.plot1d,
onlimits=False,
- roi=None)
+ roi=None,
+ )
def createScatterContext(self):
self.scatterPlot = Plot2D()
- lgd = 'scatter plot'
+ lgd = "scatter plot"
self.xScatterData = numpy.array([0, 2, 3, 20, 50, 60, 36])
self.yScatterData = numpy.array([2, 3, 4, 26, 69, 6, 18])
self.valuesScatterData = numpy.array([5, 6, 7, 10, 90, 20, 5])
- self.scatterPlot.addScatter(self.xScatterData, self.yScatterData,
- self.valuesScatterData, legend=lgd)
+ self.scatterPlot.addScatter(
+ self.xScatterData, self.yScatterData, self.valuesScatterData, legend=lgd
+ )
self.scatterContext = stats._ScatterContext(
item=self.scatterPlot.getScatter(lgd),
plot=self.scatterPlot,
onlimits=False,
- roi=None
+ roi=None,
)
def createImageContext(self):
self.plot2d = Plot2D()
- self._imgLgd = 'test image'
- self.imageData = numpy.arange(32*128).reshape(32, 128)
- self.plot2d.addImage(data=self.imageData,
- legend=self._imgLgd, replace=False)
+ self._imgLgd = "test image"
+ self.imageData = numpy.arange(32 * 128).reshape(32, 128)
+ self.plot2d.addImage(data=self.imageData, legend=self._imgLgd, replace=False)
self.imageContext = stats._ImageContext(
item=self.plot2d.getImage(self._imgLgd),
plot=self.plot2d,
onlimits=False,
- roi=None
+ roi=None,
)
def getBasicStats(self):
return {
- 'min': stats.StatMin(),
- 'minCoords': stats.StatCoordMin(),
- 'max': stats.StatMax(),
- 'maxCoords': stats.StatCoordMax(),
- 'std': stats.Stat(name='std', fct=numpy.std),
- 'mean': stats.Stat(name='mean', fct=numpy.mean),
- 'com': stats.StatCOM()
+ "min": stats.StatMin(),
+ "minCoords": stats.StatCoordMin(),
+ "max": stats.StatMax(),
+ "maxCoords": stats.StatCoordMax(),
+ "std": stats.Stat(name="std", fct=numpy.std),
+ "mean": stats.Stat(name="mean", fct=numpy.mean),
+ "com": stats.StatCOM(),
}
@@ -122,6 +121,7 @@ class TestStats(TestStatsBase, TestCaseQt):
"""
Test :class:`BaseClass` class and inheriting classes
"""
+
def setUp(self):
TestCaseQt.setUp(self)
TestStatsBase.setUp(self)
@@ -134,41 +134,50 @@ class TestStats(TestStatsBase, TestCaseQt):
"""Test result for simple stats on a curve"""
_stats = self.getBasicStats()
xData = yData = numpy.array(range(20))
- self.assertEqual(_stats['min'].calculate(self.curveContext), 0)
- self.assertEqual(_stats['max'].calculate(self.curveContext), 19)
- self.assertEqual(_stats['minCoords'].calculate(self.curveContext), (0,))
- self.assertEqual(_stats['maxCoords'].calculate(self.curveContext), (19,))
- self.assertEqual(_stats['std'].calculate(self.curveContext), numpy.std(yData))
- self.assertEqual(_stats['mean'].calculate(self.curveContext), numpy.mean(yData))
+ self.assertEqual(_stats["min"].calculate(self.curveContext), 0)
+ self.assertEqual(_stats["max"].calculate(self.curveContext), 19)
+ self.assertEqual(_stats["minCoords"].calculate(self.curveContext), (0,))
+ self.assertEqual(_stats["maxCoords"].calculate(self.curveContext), (19,))
+ self.assertEqual(_stats["std"].calculate(self.curveContext), numpy.std(yData))
+ self.assertEqual(_stats["mean"].calculate(self.curveContext), numpy.mean(yData))
com = numpy.sum(xData * yData) / numpy.sum(yData)
- self.assertEqual(_stats['com'].calculate(self.curveContext), com)
+ self.assertEqual(_stats["com"].calculate(self.curveContext), com)
def testBasicStatsImage(self):
"""Test result for simple stats on an image"""
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext), 0)
- self.assertEqual(_stats['max'].calculate(self.imageContext), 128 * 32 - 1)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext), (0, 0))
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext), (127, 31))
- self.assertEqual(_stats['std'].calculate(self.imageContext), numpy.std(self.imageData))
- self.assertEqual(_stats['mean'].calculate(self.imageContext), numpy.mean(self.imageData))
+ self.assertEqual(_stats["min"].calculate(self.imageContext), 0)
+ self.assertEqual(_stats["max"].calculate(self.imageContext), 128 * 32 - 1)
+ self.assertEqual(_stats["minCoords"].calculate(self.imageContext), (0, 0))
+ self.assertEqual(_stats["maxCoords"].calculate(self.imageContext), (127, 31))
+ self.assertEqual(
+ _stats["std"].calculate(self.imageContext), numpy.std(self.imageData)
+ )
+ self.assertEqual(
+ _stats["mean"].calculate(self.imageContext), numpy.mean(self.imageData)
+ )
yData = numpy.sum(self.imageData.astype(numpy.float64), axis=1)
xData = numpy.sum(self.imageData.astype(numpy.float64), axis=0)
dataXRange = range(self.imageData.shape[1])
dataYRange = range(self.imageData.shape[0])
- ycom = numpy.sum(yData*dataYRange) / numpy.sum(yData)
- xcom = numpy.sum(xData*dataXRange) / numpy.sum(xData)
+ ycom = numpy.sum(yData * dataYRange) / numpy.sum(yData)
+ xcom = numpy.sum(xData * dataXRange) / numpy.sum(xData)
- self.assertEqual(_stats['com'].calculate(self.imageContext), (xcom, ycom))
+ self.assertEqual(_stats["com"].calculate(self.imageContext), (xcom, ycom))
def testStatsImageAdv(self):
"""Test that scale and origin are taking into account for images"""
image2Data = numpy.arange(32 * 128).reshape(32, 128)
- self.plot2d.addImage(data=image2Data, legend=self._imgLgd,
- replace=True, origin=(100, 10), scale=(2, 0.5))
+ self.plot2d.addImage(
+ data=image2Data,
+ legend=self._imgLgd,
+ replace=True,
+ origin=(100, 10),
+ scale=(2, 0.5),
+ )
image2Context = stats._ImageContext(
item=self.plot2d.getImage(self._imgLgd),
plot=self.plot2d,
@@ -176,18 +185,19 @@ class TestStats(TestStatsBase, TestCaseQt):
roi=None,
)
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(image2Context), 0)
+ self.assertEqual(_stats["min"].calculate(image2Context), 0)
+ self.assertEqual(_stats["max"].calculate(image2Context), 128 * 32 - 1)
+ self.assertEqual(_stats["minCoords"].calculate(image2Context), (100, 10))
self.assertEqual(
- _stats['max'].calculate(image2Context), 128 * 32 - 1)
+ _stats["maxCoords"].calculate(image2Context),
+ (127 * 2.0 + 100, 31 * 0.5 + 10),
+ )
self.assertEqual(
- _stats['minCoords'].calculate(image2Context), (100, 10))
+ _stats["std"].calculate(image2Context), numpy.std(self.imageData)
+ )
self.assertEqual(
- _stats['maxCoords'].calculate(image2Context), (127*2. + 100,
- 31 * 0.5 + 10))
- self.assertEqual(_stats['std'].calculate(image2Context),
- numpy.std(self.imageData))
- self.assertEqual(_stats['mean'].calculate(image2Context),
- numpy.mean(self.imageData))
+ _stats["mean"].calculate(image2Context), numpy.mean(self.imageData)
+ )
yData = numpy.sum(self.imageData, axis=1)
xData = numpy.sum(self.imageData, axis=0)
@@ -197,30 +207,36 @@ class TestStats(TestStatsBase, TestCaseQt):
ycom = numpy.sum(yData * dataYRange) / numpy.sum(yData)
ycom = (ycom * 0.5) + 10
xcom = numpy.sum(xData * dataXRange) / numpy.sum(xData)
- xcom = (xcom * 2.) + 100
- self.assertTrue(numpy.allclose(
- _stats['com'].calculate(image2Context), (xcom, ycom)))
+ xcom = (xcom * 2.0) + 100
+ self.assertTrue(
+ numpy.allclose(_stats["com"].calculate(image2Context), (xcom, ycom))
+ )
def testBasicStatsScatter(self):
"""Test result for simple stats on a scatter"""
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.scatterContext), 5)
- self.assertEqual(_stats['max'].calculate(self.scatterContext), 90)
- self.assertEqual(_stats['minCoords'].calculate(self.scatterContext), (0, 2))
- self.assertEqual(_stats['maxCoords'].calculate(self.scatterContext), (50, 69))
- self.assertEqual(_stats['std'].calculate(self.scatterContext), numpy.std(self.valuesScatterData))
- self.assertEqual(_stats['mean'].calculate(self.scatterContext), numpy.mean(self.valuesScatterData))
+ self.assertEqual(_stats["min"].calculate(self.scatterContext), 5)
+ self.assertEqual(_stats["max"].calculate(self.scatterContext), 90)
+ self.assertEqual(_stats["minCoords"].calculate(self.scatterContext), (0, 2))
+ self.assertEqual(_stats["maxCoords"].calculate(self.scatterContext), (50, 69))
+ self.assertEqual(
+ _stats["std"].calculate(self.scatterContext),
+ numpy.std(self.valuesScatterData),
+ )
+ self.assertEqual(
+ _stats["mean"].calculate(self.scatterContext),
+ numpy.mean(self.valuesScatterData),
+ )
data = self.valuesScatterData.astype(numpy.float64)
comx = numpy.sum(self.xScatterData * data) / numpy.sum(data)
comy = numpy.sum(self.yScatterData * data) / numpy.sum(data)
- self.assertEqual(_stats['com'].calculate(self.scatterContext),
- (comx, comy))
+ self.assertEqual(_stats["com"].calculate(self.scatterContext), (comx, comy))
def testKindNotManagedByStat(self):
"""Make sure an exception is raised if we try to execute calculate
of the base class"""
- b = stats.StatBase(name='toto', compatibleKinds='curve')
+ b = stats.StatBase(name="toto", compatibleKinds="curve")
with self.assertRaises(NotImplementedError):
b.calculate(self.imageContext)
@@ -229,7 +245,7 @@ class TestStats(TestStatsBase, TestCaseQt):
Make sure an error is raised if we try to calculate a statistic with
a context not managed
"""
- myStat = stats.Stat(name='toto', fct=numpy.std, kinds=('curve'))
+ myStat = stats.Stat(name="toto", fct=numpy.std, kinds=("curve"))
myStat.calculate(self.curveContext)
with self.assertRaises(ValueError):
myStat.calculate(self.scatterContext)
@@ -241,43 +257,48 @@ class TestStats(TestStatsBase, TestCaseQt):
self.plot1d.getXAxis().setLimitsConstraints(minPos=2, maxPos=5)
curveContextOnLimits = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
+ item=self.plot1d.getCurve("curve0"),
plot=self.plot1d,
onlimits=True,
- roi=None)
+ roi=None,
+ )
self.assertEqual(stat.calculate(curveContextOnLimits), 2)
self.plot2d.getXAxis().setLimitsConstraints(minPos=32)
imageContextOnLimits = stats._ImageContext(
- item=self.plot2d.getImage('test image'),
+ item=self.plot2d.getImage("test image"),
plot=self.plot2d,
onlimits=True,
- roi=None)
+ roi=None,
+ )
self.assertEqual(stat.calculate(imageContextOnLimits), 32)
self.scatterPlot.getXAxis().setLimitsConstraints(minPos=40)
scatterContextOnLimits = stats._ScatterContext(
- item=self.scatterPlot.getScatter('scatter plot'),
+ item=self.scatterPlot.getScatter("scatter plot"),
plot=self.scatterPlot,
onlimits=True,
- roi=None)
+ roi=None,
+ )
self.assertEqual(stat.calculate(scatterContextOnLimits), 20)
class TestStatsFormatter(TestCaseQt):
"""Simple test to check usage of the :class:`StatsFormatter`"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.plot1d = Plot1D()
x = range(20)
y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
+ self.plot1d.addCurve(x, y, legend="curve0")
self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
+ item=self.plot1d.getCurve("curve0"),
plot=self.plot1d,
onlimits=False,
- roi=None)
+ roi=None,
+ )
self.stat = stats.StatMin()
@@ -292,27 +313,30 @@ class TestStatsFormatter(TestCaseQt):
simple cast to str"""
emptyFormatter = statshandler.StatFormatter()
self.assertEqual(
- emptyFormatter.format(self.stat.calculate(self.curveContext)), '0.000')
+ emptyFormatter.format(self.stat.calculate(self.curveContext)), "0.000"
+ )
def testSettedFormatter(self):
"""Make sure a formatter with no formatter definition will return a
simple cast to str"""
- formatter= statshandler.StatFormatter(formatter='{0:.3f}')
+ formatter = statshandler.StatFormatter(formatter="{0:.3f}")
self.assertEqual(
- formatter.format(self.stat.calculate(self.curveContext)), '0.000')
+ formatter.format(self.stat.calculate(self.curveContext)), "0.000"
+ )
class TestStatsHandler(TestCaseQt):
- """Make sure the StatHandler is correctly making the link between
+ """Make sure the StatHandler is correctly making the link between
:class:`StatBase` and :class:`StatFormatter` and checking the API is valid
"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.plot1d = Plot1D()
x = range(20)
y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
- self.curveItem = self.plot1d.getCurve('curve0')
+ self.plot1d.addCurve(x, y, legend="curve0")
+ self.curveItem = self.plot1d.getCurve("curve0")
self.stat = stats.StatMin()
@@ -325,91 +349,94 @@ class TestStatsHandler(TestCaseQt):
def testConstructor(self):
"""Make sure the constructor can deal will all possible arguments:
-
+
* tuple of :class:`StatBase` derivated classes
* tuple of tuples (:class:`StatBase`, :class:`StatFormatter`)
* tuple of tuples (str, pointer to function, kind)
"""
- handler0 = statshandler.StatsHandler(
- (stats.StatMin(), stats.StatMax())
- )
+ handler0 = statshandler.StatsHandler((stats.StatMin(), stats.StatMax()))
- res = handler0.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19')
+ res = handler0.calculate(item=self.curveItem, plot=self.plot1d, onlimits=False)
+ self.assertTrue("min" in res)
+ self.assertEqual(res["min"], "0")
+ self.assertTrue("max" in res)
+ self.assertEqual(res["max"], "19")
handler1 = statshandler.StatsHandler(
(
(stats.StatMin(), statshandler.StatFormatter(formatter=None)),
- (stats.StatMax(), statshandler.StatFormatter())
+ (stats.StatMax(), statshandler.StatFormatter()),
)
)
- res = handler1.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19.000')
+ res = handler1.calculate(item=self.curveItem, plot=self.plot1d, onlimits=False)
+ self.assertTrue("min" in res)
+ self.assertEqual(res["min"], "0")
+ self.assertTrue("max" in res)
+ self.assertEqual(res["max"], "19.000")
handler2 = statshandler.StatsHandler(
+ ((stats.StatMin(), None), (stats.StatMax(), statshandler.StatFormatter()))
+ )
+
+ res = handler2.calculate(item=self.curveItem, plot=self.plot1d, onlimits=False)
+ self.assertTrue("min" in res)
+ self.assertEqual(res["min"], "0")
+ self.assertTrue("max" in res)
+ self.assertEqual(res["max"], "19.000")
+
+ handler3 = statshandler.StatsHandler(
(
- (stats.StatMin(), None),
- (stats.StatMax(), statshandler.StatFormatter())
- ))
-
- res = handler2.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19.000')
-
- handler3 = statshandler.StatsHandler((
- (('amin', numpy.argmin), statshandler.StatFormatter()),
- ('amax', numpy.argmax)
- ))
-
- res = handler3.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('amin' in res)
- self.assertEqual(res['amin'], '0.000')
- self.assertTrue('amax' in res)
- self.assertEqual(res['amax'], '19')
+ (("amin", numpy.argmin), statshandler.StatFormatter()),
+ ("amax", numpy.argmax),
+ )
+ )
+
+ res = handler3.calculate(item=self.curveItem, plot=self.plot1d, onlimits=False)
+ self.assertTrue("amin" in res)
+ self.assertEqual(res["amin"], "0.000")
+ self.assertTrue("amax" in res)
+ self.assertEqual(res["amax"], "19")
with self.assertRaises(ValueError):
- statshandler.StatsHandler(('name'))
+ statshandler.StatsHandler(("name"))
class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
"""Basic test for StatsWidget with curves"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.plot = Plot1D()
self.plot.show()
x = range(20)
y = range(20)
- self.plot.addCurve(x, y, legend='curve0')
+ self.plot.addCurve(x, y, legend="curve0")
y = range(12, 32)
- self.plot.addCurve(x, y, legend='curve1')
+ self.plot.addCurve(x, y, legend="curve1")
y = range(-2, 18)
- self.plot.addCurve(x, y, legend='curve2')
+ self.plot.addCurve(x, y, legend="curve2")
self.widget = StatsWidget.StatsWidget(plot=self.plot)
self.statsTable = self.widget._statsTable
- mystats = statshandler.StatsHandler((
- stats.StatMin(),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatMax(),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatDelta(),
- ('std', numpy.std),
- ('mean', numpy.mean),
- stats.StatCOM()
- ))
+ mystats = statshandler.StatsHandler(
+ (
+ stats.StatMin(),
+ (
+ stats.StatCoordMin(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ stats.StatMax(),
+ (
+ stats.StatCoordMax(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ stats.StatDelta(),
+ ("std", numpy.std),
+ ("mean", numpy.mean),
+ stats.StatCOM(),
+ )
+ )
self.statsTable.setStats(mystats)
@@ -457,42 +484,44 @@ class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
def testRemoveCurve(self):
"""Make sure the Curves stats take into account the curve removal from
plot"""
- self.plot.removeCurve('curve2')
+ self.plot.removeCurve("curve2")
self.assertEqual(self.statsTable.rowCount(), 2)
for iRow in range(2):
- self.assertTrue(self.statsTable.item(iRow, 0).text() in ('curve0', 'curve1'))
+ self.assertTrue(
+ self.statsTable.item(iRow, 0).text() in ("curve0", "curve1")
+ )
- self.plot.removeCurve('curve0')
+ self.plot.removeCurve("curve0")
self.assertEqual(self.statsTable.rowCount(), 1)
- self.plot.removeCurve('curve1')
+ self.plot.removeCurve("curve1")
self.assertEqual(self.statsTable.rowCount(), 0)
def testAddCurve(self):
"""Make sure the Curves stats take into account the add curve action"""
- self.plot.addCurve(legend='curve3', x=range(10), y=range(10))
+ self.plot.addCurve(legend="curve3", x=range(10), y=range(10))
self.assertEqual(self.statsTable.rowCount(), 4)
def testUpdateCurveFromAddCurve(self):
"""Make sure the stats of the cuve will be removed after updating a
curve"""
- self.plot.addCurve(legend='curve0', x=range(10), y=range(10))
+ self.plot.addCurve(legend="curve0", x=range(10), y=range(10))
self.qapp.processEvents()
self.assertEqual(self.statsTable.rowCount(), 3)
- curve = self.plot._getItem(kind='curve', legend='curve0')
+ curve = self.plot._getItem(kind="curve", legend="curve0")
tableItems = self.statsTable._itemToTableItems(curve)
- self.assertEqual(tableItems['max'].text(), '9')
+ self.assertEqual(tableItems["max"].text(), "9")
def testUpdateCurveFromCurveObj(self):
- self.plot.getCurve('curve0').setData(x=range(4), y=range(4))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(4))
self.qapp.processEvents()
self.assertEqual(self.statsTable.rowCount(), 3)
- curve = self.plot._getItem(kind='curve', legend='curve0')
+ curve = self.plot._getItem(kind="curve", legend="curve0")
tableItems = self.statsTable._itemToTableItems(curve)
- self.assertEqual(tableItems['max'].text(), '3')
+ self.assertEqual(tableItems["max"].text(), "3")
def testSetAnotherPlot(self):
plot2 = Plot1D()
- plot2.addCurve(x=range(26), y=range(26), legend='new curve')
+ plot2.addCurve(x=range(26), y=range(26), legend="new curve")
self.statsTable.setPlot(plot2)
self.assertEqual(self.statsTable.rowCount(), 1)
self.qapp.processEvents()
@@ -502,50 +531,62 @@ class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
def testUpdateMode(self):
"""Make sure the update modes are well take into account"""
- self.plot.setActiveCurve('curve0')
+ self.plot.setActiveCurve("curve0")
for display_only_active in (True, False):
with self.subTest(display_only_active=display_only_active):
self.widget.setDisplayOnlyActiveItem(display_only_active)
- self.plot.getCurve('curve0').setData(x=range(4), y=range(4))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(4))
self.widget.setUpdateMode(StatsWidget.UpdateMode.AUTO)
update_stats_action = self.widget._options.getUpdateStatsAction()
# test from api
- self.assertEqual(self.widget.getUpdateMode(), StatsWidget.UpdateMode.AUTO)
+ self.assertEqual(
+ self.widget.getUpdateMode(), StatsWidget.UpdateMode.AUTO
+ )
self.widget.show()
# check stats change in auto mode
- self.plot.getCurve('curve0').setData(x=range(4), y=range(-1, 3))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(-1, 3))
self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == -1.)
+ tableItems = self.statsTable._itemToTableItems(
+ self.plot.getCurve("curve0")
+ )
+ curve0_min = tableItems["min"].text()
+ self.assertTrue(float(curve0_min) == -1.0)
- self.plot.getCurve('curve0').setData(x=range(4), y=range(1, 5))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(1, 5))
self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 1.)
+ tableItems = self.statsTable._itemToTableItems(
+ self.plot.getCurve("curve0")
+ )
+ curve0_min = tableItems["min"].text()
+ self.assertTrue(float(curve0_min) == 1.0)
# check stats change in manual mode only if requested
self.widget.setUpdateMode(StatsWidget.UpdateMode.MANUAL)
- self.assertEqual(self.widget.getUpdateMode(), StatsWidget.UpdateMode.MANUAL)
+ self.assertEqual(
+ self.widget.getUpdateMode(), StatsWidget.UpdateMode.MANUAL
+ )
- self.plot.getCurve('curve0').setData(x=range(4), y=range(2, 6))
+ self.plot.getCurve("curve0").setData(x=range(4), y=range(2, 6))
self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 1.)
+ tableItems = self.statsTable._itemToTableItems(
+ self.plot.getCurve("curve0")
+ )
+ curve0_min = tableItems["min"].text()
+ self.assertTrue(float(curve0_min) == 1.0)
update_stats_action.trigger()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 2.)
+ tableItems = self.statsTable._itemToTableItems(
+ self.plot.getCurve("curve0")
+ )
+ curve0_min = tableItems["min"].text()
+ self.assertTrue(float(curve0_min) == 2.0)
def testItemHidden(self):
"""Test if an item is hide, then the associated stats item is also
hide"""
- curve0 = self.plot.getCurve('curve0')
- curve1 = self.plot.getCurve('curve1')
- curve2 = self.plot.getCurve('curve2')
+ curve0 = self.plot.getCurve("curve0")
+ curve1 = self.plot.getCurve("curve1")
+ curve2 = self.plot.getCurve("curve2")
self.plot.show()
self.widget.show()
@@ -564,8 +605,8 @@ class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
self.qapp.processEvents()
self.assertTrue(self.statsTable.isRowHidden(1))
tableItems = self.statsTable._itemToTableItems(curve2)
- curve2_min = tableItems['min'].text()
- self.assertTrue(float(curve2_min) == -2.)
+ curve2_min = tableItems["min"].text()
+ self.assertTrue(float(curve2_min) == -2.0)
curve0.setVisible(False)
curve1.setVisible(False)
@@ -579,27 +620,38 @@ class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
class TestStatsWidgetWithImages(TestCaseQt):
"""Basic test for StatsWidget with images"""
- IMAGE_LEGEND = 'test image'
+ IMAGE_LEGEND = "test image"
def setUp(self):
TestCaseQt.setUp(self)
self.plot = Plot2D()
- self.plot.addImage(data=numpy.arange(128*128).reshape(128, 128),
- legend=self.IMAGE_LEGEND, replace=False)
+ self.plot.addImage(
+ data=numpy.arange(128 * 128).reshape(128, 128),
+ legend=self.IMAGE_LEGEND,
+ replace=False,
+ )
self.widget = StatsWidget.StatsTable(plot=self.plot)
- mystats = statshandler.StatsHandler((
- (stats.StatMin(), statshandler.StatFormatter()),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- (stats.StatMax(), statshandler.StatFormatter()),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- (stats.StatDelta(), statshandler.StatFormatter()),
- ('std', numpy.std),
- ('mean', numpy.mean),
- (stats.StatCOM(), statshandler.StatFormatter(None))
- ))
+ mystats = statshandler.StatsHandler(
+ (
+ (stats.StatMin(), statshandler.StatFormatter()),
+ (
+ stats.StatCoordMin(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ (stats.StatMax(), statshandler.StatFormatter()),
+ (
+ stats.StatCoordMax(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ (stats.StatDelta(), statshandler.StatFormatter()),
+ ("std", numpy.std),
+ ("mean", numpy.mean),
+ (stats.StatCOM(), statshandler.StatFormatter(None)),
+ )
+ )
self.widget.setStats(mystats)
@@ -614,17 +666,16 @@ class TestStatsWidgetWithImages(TestCaseQt):
TestCaseQt.tearDown(self)
def test(self):
- image = self.plot._getItem(
- kind='image', legend=self.IMAGE_LEGEND)
+ image = self.plot._getItem(kind="image", legend=self.IMAGE_LEGEND)
tableItems = self.widget._itemToTableItems(image)
- maxText = '{0:.3f}'.format((128 * 128) - 1)
- self.assertEqual(tableItems['legend'].text(), self.IMAGE_LEGEND)
- self.assertEqual(tableItems['min'].text(), '0.000')
- self.assertEqual(tableItems['max'].text(), maxText)
- self.assertEqual(tableItems['delta'].text(), maxText)
- self.assertEqual(tableItems['coords min'].text(), '0.0, 0.0')
- self.assertEqual(tableItems['coords max'].text(), '127.0, 127.0')
+ maxText = "{0:.3f}".format((128 * 128) - 1)
+ self.assertEqual(tableItems["legend"].text(), self.IMAGE_LEGEND)
+ self.assertEqual(tableItems["min"].text(), "0.000")
+ self.assertEqual(tableItems["max"].text(), maxText)
+ self.assertEqual(tableItems["delta"].text(), maxText)
+ self.assertEqual(tableItems["coords min"].text(), "0.0, 0.0")
+ self.assertEqual(tableItems["coords max"].text(), "127.0, 127.0")
def testItemHidden(self):
"""Test if an item is hide, then the associated stats item is also
@@ -639,28 +690,37 @@ class TestStatsWidgetWithImages(TestCaseQt):
class TestStatsWidgetWithScatters(TestCaseQt):
-
- SCATTER_LEGEND = 'scatter plot'
+ SCATTER_LEGEND = "scatter plot"
def setUp(self):
TestCaseQt.setUp(self)
self.scatterPlot = Plot2D()
- self.scatterPlot.addScatter([0, 1, 2, 20, 50, 60],
- [2, 3, 4, 26, 69, 6],
- [5, 6, 7, 10, 90, 20],
- legend=self.SCATTER_LEGEND)
+ self.scatterPlot.addScatter(
+ [0, 1, 2, 20, 50, 60],
+ [2, 3, 4, 26, 69, 6],
+ [5, 6, 7, 10, 90, 20],
+ legend=self.SCATTER_LEGEND,
+ )
self.widget = StatsWidget.StatsTable(plot=self.scatterPlot)
- mystats = statshandler.StatsHandler((
- stats.StatMin(),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatMax(),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatDelta(),
- ('std', numpy.std),
- ('mean', numpy.mean),
- stats.StatCOM()
- ))
+ mystats = statshandler.StatsHandler(
+ (
+ stats.StatMin(),
+ (
+ stats.StatCoordMin(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ stats.StatMax(),
+ (
+ stats.StatCoordMax(),
+ statshandler.StatFormatter(None, qt.QTableWidgetItem),
+ ),
+ stats.StatDelta(),
+ ("std", numpy.std),
+ ("mean", numpy.mean),
+ stats.StatCOM(),
+ )
+ )
self.widget.setStats(mystats)
@@ -675,15 +735,14 @@ class TestStatsWidgetWithScatters(TestCaseQt):
TestCaseQt.tearDown(self)
def testStats(self):
- scatter = self.scatterPlot._getItem(
- kind='scatter', legend=self.SCATTER_LEGEND)
+ scatter = self.scatterPlot._getItem(kind="scatter", legend=self.SCATTER_LEGEND)
tableItems = self.widget._itemToTableItems(scatter)
- self.assertEqual(tableItems['legend'].text(), self.SCATTER_LEGEND)
- self.assertEqual(tableItems['min'].text(), '5')
- self.assertEqual(tableItems['coords min'].text(), '0, 2')
- self.assertEqual(tableItems['max'].text(), '90')
- self.assertEqual(tableItems['coords max'].text(), '50, 69')
- self.assertEqual(tableItems['delta'].text(), '85')
+ self.assertEqual(tableItems["legend"].text(), self.SCATTER_LEGEND)
+ self.assertEqual(tableItems["min"].text(), "5")
+ self.assertEqual(tableItems["coords min"].text(), "0, 2")
+ self.assertEqual(tableItems["max"].text(), "90")
+ self.assertEqual(tableItems["coords max"].text(), "50, 69")
+ self.assertEqual(tableItems["delta"].text(), "85")
class TestEmptyStatsWidget(TestCaseQt):
@@ -695,25 +754,26 @@ class TestEmptyStatsWidget(TestCaseQt):
class TestLineWidget(TestCaseQt):
"""Some test for the StatsLineWidget."""
+
def setUp(self):
TestCaseQt.setUp(self)
- mystats = statshandler.StatsHandler((
- (stats.StatMin(), statshandler.StatFormatter()),
- ))
+ mystats = statshandler.StatsHandler(
+ ((stats.StatMin(), statshandler.StatFormatter()),)
+ )
self.plot = Plot1D()
self.plot.show()
self.x = range(20)
self.y0 = range(20)
- self.curve0 = self.plot.addCurve(self.x, self.y0, legend='curve0')
+ self.plot.addCurve(self.x, self.y0, legend="curve0")
self.y1 = range(12, 32)
- self.plot.addCurve(self.x, self.y1, legend='curve1')
+ self.plot.addCurve(self.x, self.y1, legend="curve1")
self.y2 = range(-2, 18)
- self.plot.addCurve(self.x, self.y2, legend='curve2')
- self.widget = StatsWidget.BasicGridStatsWidget(plot=self.plot,
- kind='curve',
- stats=mystats)
+ self.plot.addCurve(self.x, self.y2, legend="curve2")
+ self.widget = StatsWidget.BasicGridStatsWidget(
+ plot=self.plot, kind="curve", stats=mystats
+ )
def tearDown(self):
Stats._getContext.cache_clear()
@@ -731,27 +791,37 @@ class TestLineWidget(TestCaseQt):
def testProcessing(self):
self.widget._lineStatsWidget.setStatsOnVisibleData(False)
self.qapp.processEvents()
- self.plot.setActiveCurve(legend='curve0')
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '0.000')
- self.plot.setActiveCurve(legend='curve1')
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '12.000')
+ self.plot.setActiveCurve(legend="curve0")
+ self.assertTrue(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "0.000"
+ )
+ self.plot.setActiveCurve(legend="curve1")
+ self.assertTrue(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "12.000"
+ )
self.plot.getXAxis().setLimitsConstraints(minPos=2, maxPos=5)
self.widget.setStatsOnVisibleData(True)
self.qapp.processEvents()
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '14.000')
+ self.assertTrue(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "14.000"
+ )
self.plot.setActiveCurve(None)
self.assertIsNone(self.plot.getActiveCurve())
self.widget.setStatsOnVisibleData(False)
self.qapp.processEvents()
- self.assertFalse(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '14.000')
- self.widget.setKind('image')
- self.plot.addImage(numpy.arange(100*100).reshape(100, 100) + 0.312)
+ self.assertFalse(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "14.000"
+ )
+ self.widget.setKind("image")
+ self.plot.addImage(numpy.arange(100 * 100).reshape(100, 100) + 0.312)
self.qapp.processEvents()
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '0.312')
+ self.assertTrue(
+ self.widget._lineStatsWidget._statQlineEdit["min"].text() == "0.312"
+ )
def testUpdateMode(self):
"""Make sure the update modes are well take into account"""
- self.plot.setActiveCurve(self.curve0)
+ self.plot.setActiveCurve("curve0")
_autoRB = self.widget._options._autoRB
_manualRB = self.widget._options._manualRB
# test from api
@@ -760,10 +830,10 @@ class TestLineWidget(TestCaseQt):
self.assertFalse(_manualRB.isChecked())
# check stats change in auto mode
- curve0_min = self.widget._lineStatsWidget._statQlineEdit['min'].text()
+ curve0_min = self.widget._lineStatsWidget._statQlineEdit["min"].text()
new_y = numpy.array(self.y0) - 2.56
- self.plot.addCurve(x=self.x, y=new_y, legend=self.curve0)
- curve0_min2 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
+ self.plot.addCurve(x=self.x, y=new_y, legend="curve0")
+ curve0_min2 = self.widget._lineStatsWidget._statQlineEdit["min"].text()
self.assertTrue(curve0_min != curve0_min2)
# check stats change in manual mode only if requested
@@ -772,11 +842,11 @@ class TestLineWidget(TestCaseQt):
self.assertTrue(_manualRB.isChecked())
new_y = numpy.array(self.y0) - 1.2
- self.plot.addCurve(x=self.x, y=new_y, legend=self.curve0)
- curve0_min3 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
+ self.plot.addCurve(x=self.x, y=new_y, legend="curve0")
+ curve0_min3 = self.widget._lineStatsWidget._statQlineEdit["min"].text()
self.assertTrue(curve0_min3 == curve0_min2)
self.widget._options._updateRequested()
- curve0_min3 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
+ curve0_min3 = self.widget._lineStatsWidget._statQlineEdit["min"].text()
self.assertTrue(curve0_min3 != curve0_min2)
# test from gui
@@ -792,6 +862,7 @@ class TestLineWidget(TestCaseQt):
class TestUpdateModeWidget(TestCaseQt):
"""Test UpdateModeWidget"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.widget = StatsWidget.UpdateModeWidget(parent=None)
@@ -833,6 +904,7 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
"""
Test stats based on ROI
"""
+
def setUp(self):
TestCaseQt.setUp(self)
self.createRois()
@@ -856,7 +928,7 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
TestCaseQt.tearDown(self)
def createRois(self):
- self._1Droi = ROI(name='my1DRoi', fromdata=2.0, todata=5.0)
+ self._1Droi = ROI(name="my1DRoi", fromdata=2.0, todata=5.0)
self._2Droi_rect = RectangleROI()
self._2Droi_rect.setGeometry(size=(10, 10), origin=(10, 0))
self._2Droi_poly = PolygonROI()
@@ -866,30 +938,32 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
def createCurveContext(self):
TestStatsBase.createCurveContext(self)
self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
+ item=self.plot1d.getCurve("curve0"),
plot=self.plot1d,
onlimits=False,
- roi=self._1Droi)
+ roi=self._1Droi,
+ )
def createHistogramContext(self):
self.plotHisto = Plot1D()
x = range(20)
y = range(20)
- self.plotHisto.addHistogram(x, y, legend='histo0')
+ self.plotHisto.addHistogram(x, y, legend="histo0")
self.histoContext = stats._HistogramContext(
- item=self.plotHisto.getHistogram('histo0'),
+ item=self.plotHisto.getHistogram("histo0"),
plot=self.plotHisto,
onlimits=False,
- roi=self._1Droi)
+ roi=self._1Droi,
+ )
def createScatterContext(self):
TestStatsBase.createScatterContext(self)
self.scatterContext = stats._ScatterContext(
- item=self.scatterPlot.getScatter('scatter plot'),
+ item=self.scatterPlot.getScatter("scatter plot"),
plot=self.scatterPlot,
onlimits=False,
- roi=self._1Droi
+ roi=self._1Droi,
)
def createImageContext(self):
@@ -899,56 +973,68 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
item=self.plot2d.getImage(self._imgLgd),
plot=self.plot2d,
onlimits=False,
- roi=self._2Droi_rect
+ roi=self._2Droi_rect,
)
self.imageContext_2 = stats._ImageContext(
item=self.plot2d.getImage(self._imgLgd),
plot=self.plot2d,
onlimits=False,
- roi=self._2Droi_poly
+ roi=self._2Droi_poly,
)
def testErrors(self):
# test if onlimits is True and give also a roi
with self.assertRaises(ValueError):
- stats._CurveContext(item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=True,
- roi=self._1Droi)
+ stats._CurveContext(
+ item=self.plot1d.getCurve("curve0"),
+ plot=self.plot1d,
+ onlimits=True,
+ roi=self._1Droi,
+ )
# test if is a curve context and give an invalid 2D roi
with self.assertRaises(TypeError):
- stats._CurveContext(item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=False,
- roi=self._2Droi_rect)
+ stats._CurveContext(
+ item=self.plot1d.getCurve("curve0"),
+ plot=self.plot1d,
+ onlimits=False,
+ roi=self._2Droi_rect,
+ )
def testBasicStatsCurve(self):
"""Test result for simple stats on a curve"""
_stats = self.getBasicStats()
xData = yData = numpy.array(range(0, 10))
- self.assertEqual(_stats['min'].calculate(self.curveContext), 2)
- self.assertEqual(_stats['max'].calculate(self.curveContext), 5)
- self.assertEqual(_stats['minCoords'].calculate(self.curveContext), (2,))
- self.assertEqual(_stats['maxCoords'].calculate(self.curveContext), (5,))
- self.assertEqual(_stats['std'].calculate(self.curveContext), numpy.std(yData[2:6]))
- self.assertEqual(_stats['mean'].calculate(self.curveContext), numpy.mean(yData[2:6]))
+ self.assertEqual(_stats["min"].calculate(self.curveContext), 2)
+ self.assertEqual(_stats["max"].calculate(self.curveContext), 5)
+ self.assertEqual(_stats["minCoords"].calculate(self.curveContext), (2,))
+ self.assertEqual(_stats["maxCoords"].calculate(self.curveContext), (5,))
+ self.assertEqual(
+ _stats["std"].calculate(self.curveContext), numpy.std(yData[2:6])
+ )
+ self.assertEqual(
+ _stats["mean"].calculate(self.curveContext), numpy.mean(yData[2:6])
+ )
com = numpy.sum(xData[2:6] * yData[2:6]) / numpy.sum(yData[2:6])
- self.assertEqual(_stats['com'].calculate(self.curveContext), com)
+ self.assertEqual(_stats["com"].calculate(self.curveContext), com)
def testBasicStatsImageRectRoi(self):
"""Test result for simple stats on an image"""
self.assertEqual(self.imageContext.values.compressed().size, 121)
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext), 10)
- self.assertEqual(_stats['max'].calculate(self.imageContext), 1300)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext), (10, 0))
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext), (20.0, 10.0))
- self.assertAlmostEqual(_stats['std'].calculate(self.imageContext),
- numpy.std(self.imageData[0:11, 10:21]))
- self.assertAlmostEqual(_stats['mean'].calculate(self.imageContext),
- numpy.mean(self.imageData[0:11, 10:21]))
+ self.assertEqual(_stats["min"].calculate(self.imageContext), 10)
+ self.assertEqual(_stats["max"].calculate(self.imageContext), 1300)
+ self.assertEqual(_stats["minCoords"].calculate(self.imageContext), (10, 0))
+ self.assertEqual(_stats["maxCoords"].calculate(self.imageContext), (20.0, 10.0))
+ self.assertAlmostEqual(
+ _stats["std"].calculate(self.imageContext),
+ numpy.std(self.imageData[0:11, 10:21]),
+ )
+ self.assertAlmostEqual(
+ _stats["mean"].calculate(self.imageContext),
+ numpy.mean(self.imageData[0:11, 10:21]),
+ )
compressed_values = self.imageContext.values.compressed()
compressed_values = compressed_values.reshape(11, 11)
@@ -958,41 +1044,47 @@ class TestStatsROI(TestStatsBase, TestCaseQt):
dataYRange = range(11)
dataXRange = range(10, 21)
- ycom = numpy.sum(yData*dataYRange) / numpy.sum(yData)
- xcom = numpy.sum(xData*dataXRange) / numpy.sum(xData)
- self.assertEqual(_stats['com'].calculate(self.imageContext), (xcom, ycom))
+ ycom = numpy.sum(yData * dataYRange) / numpy.sum(yData)
+ xcom = numpy.sum(xData * dataXRange) / numpy.sum(xData)
+ self.assertEqual(_stats["com"].calculate(self.imageContext), (xcom, ycom))
def testBasicStatsImagePolyRoi(self):
"""Test a simple rectangle ROI"""
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext_2), 0)
- self.assertEqual(_stats['max'].calculate(self.imageContext_2), 2432)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext_2), (0.0, 0.0))
+ self.assertEqual(_stats["min"].calculate(self.imageContext_2), 0)
+ self.assertEqual(_stats["max"].calculate(self.imageContext_2), 2432)
+ self.assertEqual(_stats["minCoords"].calculate(self.imageContext_2), (0.0, 0.0))
# not 0.0, 19.0 because not fully in. Should all pixel have a weight,
# on to manage them in stats. For now 0 if the center is not in, else 1
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext_2), (0.0, 19.0))
+ self.assertEqual(
+ _stats["maxCoords"].calculate(self.imageContext_2), (0.0, 19.0)
+ )
def testBasicStatsScatter(self):
self.assertEqual(self.scatterContext.values.compressed().size, 2)
_stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.scatterContext), 6)
- self.assertEqual(_stats['max'].calculate(self.scatterContext), 7)
- self.assertEqual(_stats['minCoords'].calculate(self.scatterContext), (2, 3))
- self.assertEqual(_stats['maxCoords'].calculate(self.scatterContext), (3, 4))
- self.assertEqual(_stats['std'].calculate(self.scatterContext), numpy.std([6, 7]))
- self.assertEqual(_stats['mean'].calculate(self.scatterContext), numpy.mean([6, 7]))
+ self.assertEqual(_stats["min"].calculate(self.scatterContext), 6)
+ self.assertEqual(_stats["max"].calculate(self.scatterContext), 7)
+ self.assertEqual(_stats["minCoords"].calculate(self.scatterContext), (2, 3))
+ self.assertEqual(_stats["maxCoords"].calculate(self.scatterContext), (3, 4))
+ self.assertEqual(
+ _stats["std"].calculate(self.scatterContext), numpy.std([6, 7])
+ )
+ self.assertEqual(
+ _stats["mean"].calculate(self.scatterContext), numpy.mean([6, 7])
+ )
def testBasicHistogram(self):
_stats = self.getBasicStats()
xData = yData = numpy.array(range(2, 6))
- self.assertEqual(_stats['min'].calculate(self.histoContext), 2)
- self.assertEqual(_stats['max'].calculate(self.histoContext), 5)
- self.assertEqual(_stats['minCoords'].calculate(self.histoContext), (2,))
- self.assertEqual(_stats['maxCoords'].calculate(self.histoContext), (5,))
- self.assertEqual(_stats['std'].calculate(self.histoContext), numpy.std(yData))
- self.assertEqual(_stats['mean'].calculate(self.histoContext), numpy.mean(yData))
+ self.assertEqual(_stats["min"].calculate(self.histoContext), 2)
+ self.assertEqual(_stats["max"].calculate(self.histoContext), 5)
+ self.assertEqual(_stats["minCoords"].calculate(self.histoContext), (2,))
+ self.assertEqual(_stats["maxCoords"].calculate(self.histoContext), (5,))
+ self.assertEqual(_stats["std"].calculate(self.histoContext), numpy.std(yData))
+ self.assertEqual(_stats["mean"].calculate(self.histoContext), numpy.mean(yData))
com = numpy.sum(xData * yData) / numpy.sum(yData)
- self.assertEqual(_stats['com'].calculate(self.histoContext), com)
+ self.assertEqual(_stats["com"].calculate(self.histoContext), com)
class TestAdvancedROIImageContext(TestCaseQt):
@@ -1017,31 +1109,35 @@ class TestAdvancedROIImageContext(TestCaseQt):
roi_origins = [(0, 0), (2, 10), (14, 20)]
img_origins = [(0, 0), (14, 20), (2, 10)]
img_scales = [1.0, 0.5, 2.0]
- _stats = {'sum': stats.Stat(name='sum', fct=numpy.sum), }
+ _stats = {
+ "sum": stats.Stat(name="sum", fct=numpy.sum),
+ }
for roi_origin in roi_origins:
for img_origin in img_origins:
for img_scale in img_scales:
- with self.subTest(roi_origin=roi_origin,
- img_origin=img_origin,
- img_scale=img_scale):
- self.plot.addImage(self.data, legend='img',
- origin=img_origin,
- scale=img_scale)
+ with self.subTest(
+ roi_origin=roi_origin,
+ img_origin=img_origin,
+ img_scale=img_scale,
+ ):
+ self.plot.addImage(
+ self.data, legend="img", origin=img_origin, scale=img_scale
+ )
roi = RectangleROI()
roi.setGeometry(origin=roi_origin, size=(20, 20))
context = stats._ImageContext(
- item=self.plot.getImage('img'),
+ item=self.plot.getImage("img"),
plot=self.plot,
onlimits=False,
- roi=roi)
+ roi=roi,
+ )
x_start = int((roi_origin[0] - img_origin[0]) / img_scale)
x_end = int(x_start + (20 / img_scale)) + 1
- y_start = int((roi_origin[1] - img_origin[1])/ img_scale)
+ y_start = int((roi_origin[1] - img_origin[1]) / img_scale)
y_end = int(y_start + (20 / img_scale)) + 1
x_start = max(x_start, 0)
x_end = min(max(x_end, 0), self.data_dims[1])
y_start = max(y_start, 0)
y_end = min(max(y_end, 0), self.data_dims[0])
th_sum = numpy.sum(self.data[y_start:y_end, x_start:x_end])
- self.assertAlmostEqual(_stats['sum'].calculate(context),
- th_sum)
+ self.assertAlmostEqual(_stats["sum"].calculate(context), th_sum)
diff --git a/src/silx/gui/plot/test/testUtilsAxis.py b/src/silx/gui/plot/test/testUtilsAxis.py
index dd4a689..d749845 100644
--- a/src/silx/gui/plot/test/testUtilsAxis.py
+++ b/src/silx/gui/plot/test/testUtilsAxis.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "20/11/2018"
-import unittest
from silx.gui.plot import PlotWidget
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.plot.utils.axis import SyncAxes
@@ -52,7 +50,9 @@ class TestAxisSync(TestCaseQt):
def testMoveFirstAxis(self):
"""Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.plot1.getXAxis().setLimits(10, 500)
self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
@@ -61,7 +61,9 @@ class TestAxisSync(TestCaseQt):
def testMoveSecondAxis(self):
"""Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.plot2.getXAxis().setLimits(10, 500)
self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
@@ -70,7 +72,9 @@ class TestAxisSync(TestCaseQt):
def testMoveTwoAxes(self):
"""Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.plot1.getXAxis().setLimits(1, 50)
self.plot2.getXAxis().setLimits(10, 500)
@@ -80,7 +84,9 @@ class TestAxisSync(TestCaseQt):
def testDestruction(self):
"""Test synchronization when sync object is destroyed"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
del sync
self.plot1.getXAxis().setLimits(10, 500)
@@ -90,10 +96,13 @@ class TestAxisSync(TestCaseQt):
def testAxisDestruction(self):
"""Test synchronization when an axis disappear"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
# Destroy the plot is possible
import weakref
+
plot = weakref.ref(self.plot2)
self.plot2 = None
result = self.qWaitForDestroy(plot)
@@ -106,7 +115,9 @@ class TestAxisSync(TestCaseQt):
def testStop(self):
"""Test synchronization after calling stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
sync.stop()
self.plot1.getXAxis().setLimits(10, 500)
@@ -116,7 +127,9 @@ class TestAxisSync(TestCaseQt):
def testStopMovingStart(self):
"""Test synchronization after calling stop, moving an axis, then start again"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
sync.stop()
self.plot1.getXAxis().setLimits(10, 500)
self.plot2.getXAxis().setLimits(1, 50)
@@ -130,26 +143,40 @@ class TestAxisSync(TestCaseQt):
def testDoubleStop(self):
"""Test double stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
sync.stop()
self.assertRaises(RuntimeError, sync.stop)
def testDoubleStart(self):
"""Test double stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.assertRaises(RuntimeError, sync.start)
def testScale(self):
"""Test scale change"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
self.plot1.getXAxis().setScale(self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot1.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot2.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot3.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
+ self.assertEqual(
+ self.plot1.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC
+ )
+ self.assertEqual(
+ self.plot2.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC
+ )
+ self.assertEqual(
+ self.plot3.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC
+ )
def testDirection(self):
"""Test direction change"""
- _sync = SyncAxes([self.plot1.getYAxis(), self.plot2.getYAxis(), self.plot3.getYAxis()])
+ _sync = SyncAxes(
+ [self.plot1.getYAxis(), self.plot2.getYAxis(), self.plot3.getYAxis()]
+ )
self.plot1.getYAxis().setInverted(True)
self.assertEqual(self.plot1.getYAxis().isInverted(), True)
self.assertEqual(self.plot2.getYAxis().isInverted(), True)
@@ -161,8 +188,11 @@ class TestAxisSync(TestCaseQt):
self.plot1.getXAxis().setLimits(0, 200)
self.plot2.getXAxis().setLimits(0, 20)
self.plot3.getXAxis().setLimits(0, 2)
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
- syncLimits=False, syncCenter=True)
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
+ syncLimits=False,
+ syncCenter=True,
+ )
self.assertEqual(self.plot1.getXAxis().getLimits(), (0, 200))
self.assertEqual(self.plot2.getXAxis().getLimits(), (100 - 10, 100 + 10))
@@ -174,8 +204,12 @@ class TestAxisSync(TestCaseQt):
self.plot1.getXAxis().setLimits(0, 200)
self.plot2.getXAxis().setLimits(0, 20)
self.plot3.getXAxis().setLimits(0, 2)
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
- syncLimits=False, syncCenter=True, syncZoom=True)
+ _sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
+ syncLimits=False,
+ syncCenter=True,
+ syncZoom=True,
+ )
# Supposing all the plots use the same size
self.assertEqual(self.plot1.getXAxis().getLimits(), (0, 200))
@@ -194,7 +228,9 @@ class TestAxisSync(TestCaseQt):
def testRemoveAxis(self):
"""Test synchronization after construction"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
+ sync = SyncAxes(
+ [self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()]
+ )
sync.removeAxis(self.plot3.getXAxis())
self.plot1.getXAxis().setLimits(10, 500)
diff --git a/src/silx/gui/plot/test/utils.py b/src/silx/gui/plot/test/utils.py
index 64fca56..d48a467 100644
--- a/src/silx/gui/plot/test/utils.py
+++ b/src/silx/gui/plot/test/utils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -31,7 +30,6 @@ __date__ = "26/01/2018"
import logging
import pytest
-import unittest
from silx.gui.utils.testutils import TestCaseQt
@@ -48,6 +46,7 @@ class PlotWidgetTestCase(TestCaseQt):
plot attribute is the PlotWidget created for the test.
"""
+
__screenshot_already_taken = False
backend = None
diff --git a/src/silx/gui/plot/tools/CurveLegendsWidget.py b/src/silx/gui/plot/tools/CurveLegendsWidget.py
index 4a517dd..0ebea0d 100644
--- a/src/silx/gui/plot/tools/CurveLegendsWidget.py
+++ b/src/silx/gui/plot/tools/CurveLegendsWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides a widget to display :class:`PlotWidget` curve legends.
"""
-from __future__ import division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "20/07/2018"
@@ -77,11 +74,10 @@ class _LegendWidget(qt.QWidget):
return icon.getCurve()
def _update(self):
- """Update widget according to current curve state.
- """
+ """Update widget according to current curve state."""
curve = self.getCurve()
if curve is None:
- _logger.error('Curve no more exists')
+ _logger.error("Curve no more exists")
self.setVisible(False)
return
@@ -98,9 +94,11 @@ class _LegendWidget(qt.QWidget):
:param event: Kind of change
"""
- if event in (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.HIGHLIGHTED,
- items.ItemChangedType.HIGHLIGHTED_STYLE):
+ if event in (
+ items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.HIGHLIGHTED,
+ items.ItemChangedType.HIGHLIGHTED_STYLE,
+ ):
self._update()
@@ -145,7 +143,7 @@ class CurveLegendsWidget(qt.QWidget):
"""
previousPlot = self.getPlotWidget()
if previousPlot is not None:
- previousPlot.sigItemAdded.disconnect( self._itemAdded)
+ previousPlot.sigItemAdded.disconnect(self._itemAdded)
previousPlot.sigItemAboutToBeRemoved.disconnect(self._itemRemoved)
for legend in list(self._legends.keys()):
self._removeLegend(legend)
@@ -171,7 +169,7 @@ class CurveLegendsWidget(qt.QWidget):
elif len(args) == 2:
point = qt.QPoint(*args)
else:
- raise ValueError('Unsupported arguments')
+ raise ValueError("Unsupported arguments")
assert isinstance(point, qt.QPoint)
widget = self.childAt(point)
@@ -205,7 +203,7 @@ class CurveLegendsWidget(qt.QWidget):
curve = plot.getCurve(legend)
if curve is None:
- _logger.error('Curve not found: %s' % legend)
+ _logger.error("Curve not found: %s" % legend)
return
widget = _LegendWidget(parent=self, curve=curve)
@@ -219,7 +217,7 @@ class CurveLegendsWidget(qt.QWidget):
"""
widget = self._legends.pop(legend, None)
if widget is None:
- _logger.warning('Unknown legend: %s' % legend)
+ _logger.warning("Unknown legend: %s" % legend)
else:
self.layout().removeWidget(widget)
widget.setParent(None)
diff --git a/src/silx/gui/plot/tools/LimitsToolBar.py b/src/silx/gui/plot/tools/LimitsToolBar.py
index fc192a6..5ed09f7 100644
--- a/src/silx/gui/plot/tools/LimitsToolBar.py
+++ b/src/silx/gui/plot/tools/LimitsToolBar.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
@@ -25,9 +24,6 @@
"""A toolbar to display and edit limits of a PlotWidget
"""
-
-from __future__ import division
-
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "16/10/2017"
@@ -60,7 +56,7 @@ class LimitsToolBar(qt.QToolBar):
:param str title: See :class:`QToolBar`.
"""
- def __init__(self, parent=None, plot=None, title='Limits'):
+ def __init__(self, parent=None, plot=None, title="Limits"):
super(LimitsToolBar, self).__init__(title, parent)
assert plot is not None
self._plot = plot
@@ -78,32 +74,28 @@ class LimitsToolBar(qt.QToolBar):
xMin, xMax = self.plot.getXAxis().getLimits()
yMin, yMax = self.plot.getYAxis().getLimits()
- self.addWidget(qt.QLabel('Limits: '))
- self.addWidget(qt.QLabel(' X: '))
+ self.addWidget(qt.QLabel("Limits: "))
+ self.addWidget(qt.QLabel(" X: "))
self._xMinFloatEdit = FloatEdit(self, xMin)
- self._xMinFloatEdit.editingFinished[()].connect(
- self._xFloatEditChanged)
+ self._xMinFloatEdit.editingFinished[()].connect(self._xFloatEditChanged)
self.addWidget(self._xMinFloatEdit)
self._xMaxFloatEdit = FloatEdit(self, xMax)
- self._xMaxFloatEdit.editingFinished[()].connect(
- self._xFloatEditChanged)
+ self._xMaxFloatEdit.editingFinished[()].connect(self._xFloatEditChanged)
self.addWidget(self._xMaxFloatEdit)
- self.addWidget(qt.QLabel(' Y: '))
+ self.addWidget(qt.QLabel(" Y: "))
self._yMinFloatEdit = FloatEdit(self, yMin)
- self._yMinFloatEdit.editingFinished[()].connect(
- self._yFloatEditChanged)
+ self._yMinFloatEdit.editingFinished[()].connect(self._yFloatEditChanged)
self.addWidget(self._yMinFloatEdit)
self._yMaxFloatEdit = FloatEdit(self, yMax)
- self._yMaxFloatEdit.editingFinished[()].connect(
- self._yFloatEditChanged)
+ self._yMaxFloatEdit.editingFinished[()].connect(self._yFloatEditChanged)
self.addWidget(self._yMaxFloatEdit)
def _plotWidgetSlot(self, event):
"""Listen to :class:`PlotWidget` events."""
- if event['event'] not in ('limitsChanged',):
+ if event["event"] not in ("limitsChanged",):
return
xMin, xMax = self.plot.getXAxis().getLimits()
diff --git a/src/silx/gui/plot/tools/PlotToolButton.py b/src/silx/gui/plot/tools/PlotToolButton.py
new file mode 100644
index 0000000..3a14f77
--- /dev/null
+++ b/src/silx/gui/plot/tools/PlotToolButton.py
@@ -0,0 +1,92 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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 an abstract PlotToolButton that can be use to create
+plot tools for a toolbar.
+"""
+
+from __future__ import annotations
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "20/12/2023"
+
+
+import logging
+import weakref
+
+from silx.gui import qt
+
+
+_logger = logging.getLogger(__name__)
+
+
+class PlotToolButton(qt.QToolButton):
+ """A QToolButton connected to a :class:`~silx.gui.plot.PlotWidget`."""
+
+ def __init__(self, parent: qt.QWidget | None = None, plot=None):
+ super(PlotToolButton, self).__init__(parent)
+ self._plotRef = None
+ if plot is not None:
+ self.setPlot(plot)
+
+ def plot(self):
+ """
+ Returns the plot connected to the widget.
+ """
+ return None if self._plotRef is None else self._plotRef()
+
+ def setPlot(self, plot):
+ """
+ Set the plot connected to the widget
+
+ :param plot: :class:`.PlotWidget` instance on which to operate.
+ """
+ previousPlot = self.plot()
+
+ if previousPlot is plot:
+ return
+ if previousPlot is not None:
+ self._disconnectPlot(previousPlot)
+
+ if plot is None:
+ self._plotRef = None
+ else:
+ self._plotRef = weakref.ref(plot)
+ self._connectPlot(plot)
+
+ def _connectPlot(self, plot):
+ """
+ Called when the plot is connected to the widget
+
+ :param plot: :class:`.PlotWidget` instance
+ """
+ pass
+
+ def _disconnectPlot(self, plot):
+ """
+ Called when the plot is disconnected from the widget
+
+ :param plot: :class:`.PlotWidget` instance
+ """
+ pass
diff --git a/src/silx/gui/plot/tools/PositionInfo.py b/src/silx/gui/plot/tools/PositionInfo.py
index 8b95fbc..e3b8425 100644
--- a/src/silx/gui/plot/tools/PositionInfo.py
+++ b/src/silx/gui/plot/tools/PositionInfo.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,8 +26,6 @@
It can be configured to provide more information.
"""
-from __future__ import division
-
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "16/10/2017"
@@ -41,7 +38,6 @@ import weakref
import numpy
-from ....utils.deprecation import deprecated
from ... import qt
from .. import items
from ...widgets.ElidedLabel import ElidedLabel
@@ -59,12 +55,13 @@ class _PositionInfoLabel(ElidedLabel):
def sizeHint(self):
hint = super().sizeHint()
- width = self.fontMetrics().boundingRect('##############').width()
+ width = self.fontMetrics().boundingRect("##############").width()
return qt.QSize(max(hint.width(), width), hint.height())
# PositionInfo ################################################################
+
class PositionInfo(qt.QWidget):
"""QWidget displaying coords converted from data coords of the mouse.
@@ -118,7 +115,7 @@ class PositionInfo(qt.QWidget):
super(PositionInfo, self).__init__(parent)
if converters is None:
- converters = (('X', lambda x, y: x), ('Y', lambda x, y: y))
+ converters = (("X", lambda x, y: x), ("Y", lambda x, y: y))
self._fields = [] # To store (QLineEdit, name, function (x, y)->v)
@@ -129,10 +126,10 @@ class PositionInfo(qt.QWidget):
# Create all QLabel and store them with the corresponding converter
for name, func in converters:
- layout.addWidget(qt.QLabel('<b>' + name + ':</b>'))
+ layout.addWidget(qt.QLabel("<b>" + name + ":</b>"))
contentWidget = _PositionInfoLabel(self)
- contentWidget.setText('------')
+ contentWidget.setText("------")
layout.addWidget(contentWidget)
self._fields.append((contentWidget, name, func))
@@ -149,11 +146,6 @@ class PositionInfo(qt.QWidget):
"""
return self._plotRef()
- @property
- @deprecated(replacement='getPlotWidget', since_version='0.8.0')
- def plot(self):
- return self.getPlotWidget()
-
def getConverters(self):
"""Return the list of converters as 2-tuple (name, function)."""
return [(name, func) for _label, name, func in self._fields]
@@ -163,17 +155,18 @@ class PositionInfo(qt.QWidget):
:param dict event: Plot event
"""
- if event['event'] == 'mouseMoved':
- x, y = event['x'], event['y']
- xPixel, yPixel = event['xpixel'], event['ypixel']
+ if event["event"] == "mouseMoved":
+ x, y = event["x"], event["y"]
+ xPixel, yPixel = event["xpixel"], event["ypixel"]
self._updateStatusBar(x, y, xPixel, yPixel)
def updateInfo(self):
"""Update displayed information"""
plot = self.getPlotWidget()
if plot is None:
- _logger.error("Trying to update PositionInfo "
- "while PlotWidget no longer exists")
+ _logger.error(
+ "Trying to update PositionInfo " "while PlotWidget no longer exists"
+ )
return
widget = plot.getWidgetHandle()
@@ -196,15 +189,15 @@ class PositionInfo(qt.QWidget):
if plot is None:
return
- styleSheet = "color: rgb(0, 0, 0);" # Default style
+ styleSheet = "" # Default style
xData, yData = x, y
snappingMode = self.getSnappingMode()
# Snapping when crosshair either not requested or active
- if (snappingMode & (self.SNAPPING_CURVE | self.SNAPPING_SCATTER) and
- (not (snappingMode & self.SNAPPING_CROSSHAIR) or
- plot.getGraphCursor())):
+ if snappingMode & (self.SNAPPING_CURVE | self.SNAPPING_SCATTER) and (
+ not (snappingMode & self.SNAPPING_CROSSHAIR) or plot.getGraphCursor()
+ ):
styleSheet = "color: rgb(255, 0, 0);" # Style far from item
if snappingMode & self.SNAPPING_ACTIVE_ONLY:
@@ -216,7 +209,7 @@ class PositionInfo(qt.QWidget):
selectedItems.append(activeCurve)
if snappingMode & self.SNAPPING_SCATTER:
- activeScatter = plot._getActiveItem(kind='scatter')
+ activeScatter = plot.getActiveScatter()
if activeScatter:
selectedItems.append(activeScatter)
@@ -227,8 +220,11 @@ class PositionInfo(qt.QWidget):
kinds.append(items.Histogram)
if snappingMode & self.SNAPPING_SCATTER:
kinds.append(items.Scatter)
- selectedItems = [item for item in plot.getItems()
- if isinstance(item, tuple(kinds)) and item.isVisible()]
+ selectedItems = [
+ item
+ for item in plot.getItems()
+ if isinstance(item, tuple(kinds)) and item.isVisible()
+ ]
# Compute distance threshold
window = plot.window()
@@ -239,12 +235,12 @@ class PositionInfo(qt.QWidget):
ratio = qt.QGuiApplication.primaryScreen().devicePixelRatio()
# Baseline squared distance threshold
- distInPixels = (self.SNAP_THRESHOLD_DIST * ratio)**2
+ sqDistInPixels = (self.SNAP_THRESHOLD_DIST * ratio) ** 2
for item in selectedItems:
- if (snappingMode & self.SNAPPING_SYMBOLS_ONLY and (
- not isinstance(item, items.SymbolMixIn) or
- not item.getSymbol())):
+ if snappingMode & self.SNAPPING_SYMBOLS_ONLY and (
+ not isinstance(item, items.SymbolMixIn) or not item.getSymbol()
+ ):
# Only handled if item symbols are visible
continue
@@ -259,37 +255,42 @@ class PositionInfo(qt.QWidget):
yData = item.getValueData(copy=False)[index]
# Update label style sheet
- styleSheet = "color: rgb(0, 0, 0);"
+ styleSheet = ""
break
else: # Curve, Scatter
- xArray = item.getXData(copy=False)
- yArray = item.getYData(copy=False)
- closestIndex = numpy.argmin(
- pow(xArray - x, 2) + pow(yArray - y, 2))
-
- xClosest = xArray[closestIndex]
- yClosest = yArray[closestIndex]
+ result = item.pick(xPixel, yPixel)
+ if result is None:
+ continue
+ indices = result.getIndices(copy=False)
+ if indices is None:
+ continue
if isinstance(item, items.YAxisMixIn):
axis = item.getYAxis()
else:
- axis = 'left'
-
- closestInPixels = plot.dataToPixel(
- xClosest, yClosest, axis=axis)
- if closestInPixels is not None:
- curveDistInPixels = (
- (closestInPixels[0] - xPixel)**2 +
- (closestInPixels[1] - yPixel)**2)
-
- if curveDistInPixels <= distInPixels:
- # Update label style sheet
- styleSheet = "color: rgb(0, 0, 0);"
+ axis = "left"
+
+ xArray = item.getXData(copy=False)[indices]
+ yArray = item.getYData(copy=False)[indices]
+ pixelPositions = plot.dataToPixel(xArray, yArray, axis=axis)
+ if pixelPositions is None:
+ continue
+ sqDistances = (pixelPositions[0] - xPixel) ** 2 + (
+ pixelPositions[1] - yPixel
+ ) ** 2
+ if not numpy.any(numpy.isfinite(sqDistances)):
+ continue
+ closestIndex = numpy.nanargmin(sqDistances)
+ closestSqDistInPixels = sqDistances[closestIndex]
+
+ if closestSqDistInPixels <= sqDistInPixels:
+ # Update label style sheet
+ styleSheet = ""
- # if close enough, snap to data point coord
- xData, yData = xClosest, yClosest
- distInPixels = curveDistInPixels
+ # if close enough, snap to data point coord
+ xData, yData = xArray[closestIndex], yArray[closestIndex]
+ sqDistInPixels = closestSqDistInPixels
for label, name, func in self._fields:
label.setStyleSheet(styleSheet)
@@ -299,10 +300,11 @@ class PositionInfo(qt.QWidget):
text = self.valueToString(value)
label.setText(text)
except:
- label.setText('Error')
+ label.setText("Error")
_logger.error(
"Error while converting coordinates (%f, %f)"
- "with converter '%s'" % (xPixel, yPixel, name))
+ "with converter '%s'" % (xPixel, yPixel, name)
+ )
_logger.error(traceback.format_exc())
def valueToString(self, value):
@@ -311,7 +313,7 @@ class PositionInfo(qt.QWidget):
return ", ".join(value)
elif isinstance(value, numbers.Real):
# Use this for floats and int
- return '%.7g' % value
+ return "%.7g" % value
else:
# Fallback for other types
return str(value)
@@ -353,21 +355,3 @@ class PositionInfo(qt.QWidget):
:rtype: int
"""
return self._snappingMode
-
- _SNAPPING_LEGACY = (SNAPPING_CROSSHAIR |
- SNAPPING_ACTIVE_ONLY |
- SNAPPING_SYMBOLS_ONLY |
- SNAPPING_CURVE |
- SNAPPING_SCATTER)
- """Legacy snapping mode"""
-
- @property
- @deprecated(replacement="getSnappingMode", since_version="0.8")
- def autoSnapToActiveCurve(self):
- return self.getSnappingMode() == self._SNAPPING_LEGACY
-
- @autoSnapToActiveCurve.setter
- @deprecated(replacement="setSnappingMode", since_version="0.8")
- def autoSnapToActiveCurve(self, flag):
- self.setSnappingMode(
- self._SNAPPING_LEGACY if flag else self.SNAPPING_DISABLED)
diff --git a/src/silx/gui/plot/tools/RadarView.py b/src/silx/gui/plot/tools/RadarView.py
index 7076835..8ddb98b 100644
--- a/src/silx/gui/plot/tools/RadarView.py
+++ b/src/silx/gui/plot/tools/RadarView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
@@ -42,9 +41,9 @@ _logger = logging.getLogger(__name__)
class _DraggableRectItem(qt.QGraphicsRectItem):
"""RectItem which signals its change through visibleRectDragged."""
+
def __init__(self, *args, **kwargs):
- super(_DraggableRectItem, self).__init__(
- *args, **kwargs)
+ super(_DraggableRectItem, self).__init__(*args, **kwargs)
self._previousCursor = None
self.setFlag(qt.QGraphicsItem.ItemIsMovable)
@@ -82,8 +81,7 @@ class _DraggableRectItem(qt.QGraphicsRectItem):
def itemChange(self, change, value):
"""Callback called before applying changes to the item."""
- if (change == qt.QGraphicsItem.ItemPositionChange and
- not self._ignoreChange):
+ if change == qt.QGraphicsItem.ItemPositionChange and not self._ignoreChange:
# Makes sure that the visible area is in the data
# or that data is in the visible area if area is too wide
x, y = value.x(), value.y()
@@ -119,12 +117,12 @@ class _DraggableRectItem(qt.QGraphicsRectItem):
value.x() + self.rect().left(),
value.y() + self.rect().top(),
self.rect().width(),
- self.rect().height())
+ self.rect().height(),
+ )
return value
- return super(_DraggableRectItem, self).itemChange(
- change, value)
+ return super(_DraggableRectItem, self).itemChange(change, value)
def hoverEnterEvent(self, event):
"""Called when the mouse enters the rectangle area"""
@@ -161,37 +159,37 @@ class RadarView(qt.QGraphicsView):
It provides: left, top, width, height in data coordinates.
"""
- _DATA_PEN = qt.QPen(qt.QColor('white'))
- _DATA_BRUSH = qt.QBrush(qt.QColor('light gray'))
- _ACTIVEDATA_PEN = qt.QPen(qt.QColor('black'))
- _ACTIVEDATA_BRUSH = qt.QBrush(qt.QColor('transparent'))
+ _DATA_PEN = qt.QPen(qt.QColor("white"))
+ _DATA_BRUSH = qt.QBrush(qt.QColor("light gray"))
+ _ACTIVEDATA_PEN = qt.QPen(qt.QColor("black"))
+ _ACTIVEDATA_BRUSH = qt.QBrush(qt.QColor("transparent"))
_ACTIVEDATA_PEN.setWidth(2)
_ACTIVEDATA_PEN.setCosmetic(True)
- _VISIBLE_PEN = qt.QPen(qt.QColor('blue'))
+ _VISIBLE_PEN = qt.QPen(qt.QColor("blue"))
_VISIBLE_PEN.setWidth(2)
_VISIBLE_PEN.setCosmetic(True)
_VISIBLE_BRUSH = qt.QBrush(qt.QColor(0, 0, 0, 0))
- _TOOLTIP = 'Radar View:\nRed contour: Visible area\nGray area: The image'
+ _TOOLTIP = "Radar View:\nRed contour: Visible area\nGray area: The image"
_PIXMAP_SIZE = 256
def __init__(self, parent=None):
self.__plotRef = None
self._scene = qt.QGraphicsScene()
- self._dataRect = self._scene.addRect(0, 0, 1, 1,
- self._DATA_PEN,
- self._DATA_BRUSH)
- self._imageRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
+ self._dataRect = self._scene.addRect(
+ 0, 0, 1, 1, self._DATA_PEN, self._DATA_BRUSH
+ )
+ self._imageRect = self._scene.addRect(
+ 0, 0, 1, 1, self._ACTIVEDATA_PEN, self._ACTIVEDATA_BRUSH
+ )
self._imageRect.setVisible(False)
- self._scatterRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
+ self._scatterRect = self._scene.addRect(
+ 0, 0, 1, 1, self._ACTIVEDATA_PEN, self._ACTIVEDATA_BRUSH
+ )
self._scatterRect.setVisible(False)
- self._curveRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
+ self._curveRect = self._scene.addRect(
+ 0, 0, 1, 1, self._ACTIVEDATA_PEN, self._ACTIVEDATA_BRUSH
+ )
self._curveRect.setVisible(False)
self._visibleRect = _DraggableRectItem(0, 0, 1, 1)
@@ -203,7 +201,7 @@ class RadarView(qt.QGraphicsView):
self.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
self.setFocusPolicy(qt.Qt.NoFocus)
- self.setStyleSheet('border: 0px')
+ self.setStyleSheet("border: 0px")
self.setToolTip(self._TOOLTIP)
self.__reentrant = LockReentrant()
@@ -312,7 +310,7 @@ class RadarView(qt.QGraphicsView):
# As opposed to Plot. So invert RadarView when Plot is NOT inverted.
self.resetTransform()
if not inverted:
- self.scale(1., -1.)
+ self.scale(1.0, -1.0)
self.update()
def _viewRectDragged(self, left, top, width, height):
diff --git a/src/silx/gui/plot/tools/RulerToolButton.py b/src/silx/gui/plot/tools/RulerToolButton.py
new file mode 100644
index 0000000..55cc02f
--- /dev/null
+++ b/src/silx/gui/plot/tools/RulerToolButton.py
@@ -0,0 +1,183 @@
+# /*##########################################################################
+#
+# Copyright (c) 20023 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.
+#
+# ###########################################################################*/
+"""
+PlotToolButton to measure a distance in a plot
+"""
+
+__authors__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "30/10/2023"
+
+
+import logging
+import numpy
+import weakref
+import typing
+
+from silx.gui import icons
+
+from .PlotToolButton import PlotToolButton
+
+from silx.gui.plot.tools.roi import RegionOfInterestManager
+from silx.gui.plot.items.roi import LineROI
+from silx.gui.plot import items
+
+
+_logger = logging.getLogger(__name__)
+
+
+class _RulerROI(LineROI):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._formatFunction: typing.Optional[
+ typing.Callable[
+ [numpy.ndarray, numpy.ndarray], str
+ ]
+ ] = None
+ self.setColor("#001122") # Only there to trig updateStyle
+
+ def registerFormatFunction(
+ self,
+ fct: typing.Callable[
+ [numpy.ndarray, numpy.ndarray], str
+ ],
+ ):
+ """Register a function for the formatting of the label"""
+ self._formatFunction = fct
+
+ def _updatedStyle(self, event, style: items.CurveStyle):
+ style = items.CurveStyle(
+ color="red",
+ gapcolor="white",
+ linestyle=(0, (5, 5)),
+ linewidth=style.getLineWidth())
+ LineROI._updatedStyle(self, event, style)
+ self._handleLabel.setColor("black")
+ self._handleLabel.setBackgroundColor("#FFFFFF60")
+ self._handleLabel.setZValue(1000)
+
+ def setEndPoints(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray):
+ super().setEndPoints(startPoint=startPoint, endPoint=endPoint)
+ if self._formatFunction is not None:
+ ruler_text = self._formatFunction(
+ startPoint=startPoint, endPoint=endPoint
+ )
+ self._updateText(ruler_text)
+
+
+class RulerToolButton(PlotToolButton):
+ """
+ Button to active measurement between two point of the plot
+
+ An instance of `RulerToolButton` can be added to a plot toolbar like:
+ .. code-block:: python
+
+ plot = Plot2D()
+
+ rulerButton = RulerToolButton(parent=plot, plot=plot)
+ plot.toolBar().addWidget(rulerButton)
+ """
+
+ def __init__(
+ self,
+ parent=None,
+ plot=None,
+ ):
+ super().__init__(parent=parent, plot=plot)
+ self.setCheckable(True)
+ self._roiManager = None
+ self.__lastRoiCreated = None
+ self.setIcon(icons.getQIcon("ruler"))
+ self.toggled.connect(self._callback)
+ self._connectPlot(plot)
+
+ def setPlot(self, plot):
+ return super().setPlot(plot)
+
+ @property
+ def _lastRoiCreated(self):
+ if self.__lastRoiCreated is None:
+ return None
+ return self.__lastRoiCreated()
+
+ def _callback(self, *args, **kwargs):
+ if not self._roiManager:
+ return
+ if self._lastRoiCreated is not None:
+ self._lastRoiCreated.setVisible(self.isChecked())
+ if self.isChecked():
+ self._roiManager.start(_RulerROI, self)
+ self.__interactiveModeStarted(self._roiManager)
+ else:
+ source = self._roiManager.getInteractionSource()
+ if source is self:
+ self._roiManager.stop()
+
+ def __interactiveModeStarted(self, roiManager):
+ roiManager.sigInteractiveModeFinished.connect(self.__interactiveModeFinished)
+
+ def __interactiveModeFinished(self):
+ roiManager = self._roiManager
+ if roiManager is not None:
+ roiManager.sigInteractiveModeFinished.disconnect(
+ self.__interactiveModeFinished
+ )
+ self.setChecked(False)
+
+ def _connectPlot(self, plot):
+ """
+ Called when the plot is connected to the widget
+
+ :param plot: :class:`.PlotWidget` instance
+ """
+ if plot is None:
+ return
+ self._roiManager = RegionOfInterestManager(plot)
+ self._roiManager.sigRoiAdded.connect(self._registerCurrentROI)
+
+ def _disconnectPlot(self, plot):
+ if plot and self._lastRoiCreated is not None:
+ self._roiManager.removeRoi(self._lastRoiCreated)
+ self.__lastRoiCreated = None
+ return super()._disconnectPlot(plot)
+
+ def _registerCurrentROI(self, currentRoi):
+ if self._lastRoiCreated is None:
+ self.__lastRoiCreated = weakref.ref(currentRoi)
+ self._lastRoiCreated.registerFormatFunction(self.buildDistanceText)
+ elif currentRoi is not self._lastRoiCreated and self._roiManager is not None:
+ self._roiManager.removeRoi(self._lastRoiCreated)
+ currentRoi.registerFormatFunction(self.buildDistanceText)
+ self.__lastRoiCreated = weakref.ref(currentRoi)
+
+ def buildDistanceText(self, startPoint: numpy.ndarray, endPoint: numpy.ndarray) -> str:
+ """
+ Define the text to be displayed by the ruler.
+
+ It can be redefine to modify precision or handle other parameters
+ (handling pixel size to display metric distance, display distance
+ on each distance - for non-square pixels...)
+ """
+ distance = numpy.linalg.norm(endPoint - startPoint)
+ return f"{distance: .1f}px"
diff --git a/src/silx/gui/plot/tools/__init__.py b/src/silx/gui/plot/tools/__init__.py
index 09f468c..5b6b74c 100644
--- a/src/silx/gui/plot/tools/__init__.py
+++ b/src/silx/gui/plot/tools/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/matplotlib/__init__.py b/src/silx/gui/plot/tools/compare/__init__.py
index e787240..7f23852 100644
--- a/src/silx/gui/plot/matplotlib/__init__.py
+++ b/src/silx/gui/plot/tools/compare/__init__.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,16 +21,9 @@
# THE SOFTWARE.
#
# ###########################################################################*/
+"""This module provides tools related to the compare image plot.
+"""
-__authors__ = ["T. Vincent"]
+__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "15/07/2020"
-
-from silx.utils.deprecation import deprecated_warning
-
-deprecated_warning(type_='module',
- name=__file__,
- replacement='silx.gui.utils.matplotlib',
- since_version='0.14.0')
-
-from silx.gui.utils.matplotlib import FigureCanvasQTAgg # noqa
+__date__ = "09/06/2023"
diff --git a/src/silx/gui/plot/tools/compare/core.py b/src/silx/gui/plot/tools/compare/core.py
new file mode 100644
index 0000000..90dbb79
--- /dev/null
+++ b/src/silx/gui/plot/tools/compare/core.py
@@ -0,0 +1,198 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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 main objects shared by the compare image plot.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "09/06/2023"
+
+
+import numpy
+import enum
+import contextlib
+from typing import NamedTuple
+
+from silx.gui.plot.items.image import ImageBase
+from silx.gui.plot.items.core import ItemChangedType, ColormapMixIn
+
+from silx.opencl import ocl
+
+if ocl is not None:
+ try:
+ from silx.opencl import sift
+ except ImportError:
+ # sift module is not available (e.g., in official Debian packages)
+ sift = None
+else: # No OpenCL device or no pyopencl
+ sift = None
+
+
+@enum.unique
+class VisualizationMode(enum.Enum):
+ """Enum for each visualization mode available."""
+
+ ONLY_A = "a"
+ ONLY_B = "b"
+ VERTICAL_LINE = "vline"
+ HORIZONTAL_LINE = "hline"
+ COMPOSITE_RED_BLUE_GRAY = "rbgchannel"
+ COMPOSITE_RED_BLUE_GRAY_NEG = "rbgnegchannel"
+ COMPOSITE_A_MINUS_B = "aminusb"
+
+
+@enum.unique
+class AlignmentMode(enum.Enum):
+ """Enum for each alignment mode available."""
+
+ ORIGIN = "origin"
+ CENTER = "center"
+ STRETCH = "stretch"
+ AUTO = "auto"
+
+
+class AffineTransformation(NamedTuple):
+ """Description of a 2D affine transformation: translation, scale and
+ rotation.
+ """
+
+ tx: float
+ ty: float
+ sx: float
+ sy: float
+ rot: float
+
+
+class _CompareImageItem(ImageBase, ColormapMixIn):
+ """Description of a virtual item of images to compare, in order to share
+ the data through the silx components.
+ """
+
+ def __init__(self):
+ ImageBase.__init__(self)
+ ColormapMixIn.__init__(self)
+ self.__image1 = None
+ self.__image2 = None
+ self.__vizualisationMode = VisualizationMode.ONLY_A
+
+ def getImageData1(self):
+ return self.__image1
+
+ def getImageData2(self):
+ return self.__image2
+
+ def setImageData1(self, image1):
+ if self.__image1 is image1:
+ return
+ self.__image1 = image1
+ self._updated(ItemChangedType.DATA)
+
+ def setImageData2(self, image2):
+ if self.__image2 is image2:
+ return
+ self.__image2 = image2
+ self._updated(ItemChangedType.DATA)
+
+ def getVizualisationMode(self) -> VisualizationMode:
+ return self.__vizualisationMode
+
+ @contextlib.contextmanager
+ def _updateColormapRange(self, previousMode, mode):
+ """COMPOSITE_A_MINUS_B don't have the same data range than others.
+
+ If the colormap is using a fixed range, it is updated in order to set
+ a similar range with the new data.
+ """
+ normalize_colormap = (
+ previousMode == VisualizationMode.COMPOSITE_A_MINUS_B
+ or mode == VisualizationMode.COMPOSITE_A_MINUS_B
+ )
+ if normalize_colormap:
+ data = self._getConcatenatedData(copy=False)
+ if data is None or data.size == 0:
+ normalize_colormap = False
+ else:
+ std1 = numpy.nanstd(data)
+ mean1 = numpy.nanmean(data)
+ yield
+
+ def transfer(v, std1, mean1, std2, mean2):
+ """Transfer a value from a data range to another using statistics"""
+ if v is None:
+ return None
+ rv = (v - mean1) / std1
+ return rv * std2 + mean2
+
+ if normalize_colormap:
+ data = self._getConcatenatedData(copy=False)
+ if data is not None and data.size != 0:
+ std2 = numpy.nanstd(data)
+ mean2 = numpy.nanmean(data)
+ c = self.getColormap()
+ if c is not None:
+ vmin, vmax = c.getVRange()
+ vmin = transfer(vmin, std1, mean1, std2, mean2)
+ vmax = transfer(vmax, std1, mean1, std2, mean2)
+ c.setVRange(vmin, vmax)
+
+ def setVizualisationMode(self, mode: VisualizationMode):
+ if self.__vizualisationMode == mode:
+ return None
+ with self._updateColormapRange(self.__vizualisationMode, mode):
+ self.__vizualisationMode = mode
+ self._updated(ItemChangedType.DATA)
+
+ def _getConcatenatedData(self, copy=True):
+ if self.__image1 is None and self.__image2 is None:
+ return None
+ if self.__image1 is None:
+ return numpy.array(self.__image2, copy=copy)
+ if self.__image2 is None:
+ return numpy.array(self.__image1, copy=copy)
+
+ if self.__vizualisationMode == VisualizationMode.COMPOSITE_A_MINUS_B:
+ # In this case the histogram have to be special
+ if self.__image1.shape == self.__image2.shape:
+ return self.__image1.astype(numpy.float32) - self.__image2.astype(
+ numpy.float32
+ )
+ else:
+ d1 = self.__image1[numpy.isfinite(self.__image1)]
+ d2 = self.__image2[numpy.isfinite(self.__image2)]
+ return numpy.concatenate((d1, d2))
+
+ def _updated(self, event=None, checkVisibility=True):
+ # Synchronizes colormapped data if changed
+ if event in (ItemChangedType.DATA, ItemChangedType.MASK):
+ data = self._getConcatenatedData(copy=False)
+ return self._setColormappedData(data, copy=False)
+ super()._updated(event=event, checkVisibility=checkVisibility)
+
+ def getColormappedData(self, copy=True):
+ """
+ Reimplementation of the `ColormapMixIn.getColormappedData` method.
+
+ This is used to provide a consistent auto scale on the compared images.
+ """
+ return self._getConcatenatedData(copy=copy)
diff --git a/src/silx/gui/plot/tools/compare/profile.py b/src/silx/gui/plot/tools/compare/profile.py
new file mode 100644
index 0000000..afe0eba
--- /dev/null
+++ b/src/silx/gui/plot/tools/compare/profile.py
@@ -0,0 +1,173 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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 provides profile ROIs.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "09/06/2023"
+
+
+import numpy
+
+from silx.gui.plot.tools.profile import rois
+from silx.gui.plot.tools.profile import core
+from .core import _CompareImageItem
+
+
+COLOR_A = "C0"
+COLOR_B = "C8"
+
+
+class ProfileImageLineROI(rois.ProfileImageLineROI):
+ """ROI for a compare image profile between 2 points.
+
+ The X profile of this ROI is the projection into one of the x/y axes,
+ using its scale and its orientation.
+ """
+
+ def computeProfile(self, item):
+ if not isinstance(item, _CompareImageItem):
+ raise TypeError("Unexpected class %s" % type(item))
+
+ origin = item.getOrigin()
+ scale = item.getScale()
+ method = self.getProfileMethod()
+ lineWidth = self.getProfileLineWidth()
+ roiInfo = self._getRoiInfo()
+
+ def createProfile2(currentData):
+ coords, profile, _area, profileName, xLabel = core.createProfile(
+ roiInfo=roiInfo,
+ currentData=currentData,
+ origin=origin,
+ scale=scale,
+ lineWidth=lineWidth,
+ method=method,
+ )
+ return coords, profile, profileName, xLabel
+
+ currentData1 = item.getImageData1()
+ currentData2 = item.getImageData2()
+
+ yLabel = "%s" % str(method).capitalize()
+ coords, profile1, title, xLabel = createProfile2(currentData1)
+ title = title + "; width = %d" % lineWidth
+ _coords, profile2, _title, _xLabel = createProfile2(currentData2)
+
+ profile1.shape = -1
+ profile2.shape = -1
+
+ title = title.format(xlabel="width", ylabel="height")
+ xLabel = xLabel.format(xlabel="width", ylabel="height")
+ yLabel = yLabel.format(xlabel="width", ylabel="height")
+
+ data = core.CurvesProfileData(
+ coords=coords,
+ profiles=[
+ core.CurveProfileDesc(profile1, color=COLOR_A, name="profileA"),
+ core.CurveProfileDesc(profile2, color=COLOR_B, name="profileB"),
+ ],
+ title=title,
+ xLabel=xLabel,
+ yLabel=yLabel,
+ )
+ return data
+
+
+class ProfileImageDirectedLineROI(rois.ProfileImageDirectedLineROI):
+ """ROI for a compare image profile between 2 points.
+
+ The X profile of the line is displayed projected into the line itself,
+ using its scale and its orientation. It's the distance from the origin.
+ """
+
+ def computeProfile(self, item):
+ if not isinstance(item, _CompareImageItem):
+ raise TypeError("Unexpected class %s" % type(item))
+
+ from silx.image.bilinear import BilinearImage
+
+ origin = item.getOrigin()
+ scale = item.getScale()
+ method = self.getProfileMethod()
+ lineWidth = self.getProfileLineWidth()
+
+ roiInfo = self._getRoiInfo()
+ roiStart, roiEnd, _lineProjectionMode = roiInfo
+
+ startPt = (
+ (roiStart[1] - origin[1]) / scale[1],
+ (roiStart[0] - origin[0]) / scale[0],
+ )
+ endPt = ((roiEnd[1] - origin[1]) / scale[1], (roiEnd[0] - origin[0]) / scale[0])
+
+ if numpy.array_equal(startPt, endPt):
+ return None
+
+ def computeProfile(data):
+ bilinear = BilinearImage(data)
+ profile = bilinear.profile_line(
+ (startPt[0] - 0.5, startPt[1] - 0.5),
+ (endPt[0] - 0.5, endPt[1] - 0.5),
+ lineWidth,
+ method=method,
+ )
+ return profile
+
+ currentData1 = item.getImageData1()
+ currentData2 = item.getImageData2()
+ profile1 = computeProfile(currentData1)
+ profile2 = computeProfile(currentData2)
+
+ # Compute the line size
+ lineSize = numpy.sqrt(
+ (roiEnd[1] - roiStart[1]) ** 2 + (roiEnd[0] - roiStart[0]) ** 2
+ )
+ coords = numpy.linspace(
+ 0, lineSize, len(profile1), endpoint=True, dtype=numpy.float32
+ )
+
+ title = rois._lineProfileTitle(*roiStart, *roiEnd)
+ title = title + "; width = %d" % lineWidth
+ xLabel = "√({xlabel}²+{ylabel}²)"
+ yLabel = str(method).capitalize()
+
+ # Use the axis names from the original plot
+ profileManager = self.getProfileManager()
+ plot = profileManager.getPlotWidget()
+ xLabel = rois._relabelAxes(plot, xLabel)
+ title = rois._relabelAxes(plot, title)
+
+ data = core.CurvesProfileData(
+ coords=coords,
+ profiles=[
+ core.CurveProfileDesc(profile1, color=COLOR_A, name="profileA"),
+ core.CurveProfileDesc(profile2, color=COLOR_B, name="profileB"),
+ ],
+ title=title,
+ xLabel=xLabel,
+ yLabel=yLabel,
+ )
+ return data
diff --git a/src/silx/gui/plot/tools/compare/statusbar.py b/src/silx/gui/plot/tools/compare/statusbar.py
new file mode 100644
index 0000000..5e43a37
--- /dev/null
+++ b/src/silx/gui/plot/tools/compare/statusbar.py
@@ -0,0 +1,218 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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 tool bar helper.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "09/06/2023"
+
+
+import logging
+import weakref
+import numpy
+
+from silx.gui import qt
+
+
+_logger = logging.getLogger(__name__)
+
+
+class CompareImagesStatusBar(qt.QStatusBar):
+ """StatusBar containing specific information contained in a
+ :class:`CompareImages` widget
+
+ Use :meth:`setCompareWidget` to connect this toolbar to a specific
+ :class:`CompareImages` widget.
+
+ :param Union[qt.QWidget,None] parent: Parent of this widget.
+ """
+
+ def __init__(self, parent=None):
+ qt.QStatusBar.__init__(self, parent)
+ self.setSizeGripEnabled(False)
+ self.layout().setSpacing(0)
+ self.__compareWidget = None
+ self._label1 = qt.QLabel(self)
+ self._label1.setFrameShape(qt.QFrame.WinPanel)
+ self._label1.setFrameShadow(qt.QFrame.Sunken)
+ self._label2 = qt.QLabel(self)
+ self._label2.setFrameShape(qt.QFrame.WinPanel)
+ self._label2.setFrameShadow(qt.QFrame.Sunken)
+ self._transform = qt.QLabel(self)
+ self._transform.setFrameShape(qt.QFrame.WinPanel)
+ self._transform.setFrameShadow(qt.QFrame.Sunken)
+ self.addWidget(self._label1)
+ self.addWidget(self._label2)
+ self.addWidget(self._transform)
+ self._pos = None
+ self._updateStatusBar()
+
+ def setCompareWidget(self, widget):
+ """
+ Connect this tool bar to a specific :class:`CompareImages` widget.
+
+ :param Union[None,CompareImages] widget: The widget to connect with.
+ """
+ compareWidget = self.getCompareWidget()
+ if compareWidget is not None:
+ compareWidget.getPlot().sigPlotSignal.disconnect(self.__plotSignalReceived)
+ compareWidget.sigConfigurationChanged.disconnect(self.__dataChanged)
+ compareWidget = widget
+ if compareWidget is None:
+ self.__compareWidget = None
+ else:
+ self.__compareWidget = weakref.ref(compareWidget)
+ if compareWidget is not None:
+ compareWidget.getPlot().sigPlotSignal.connect(self.__plotSignalReceived)
+ compareWidget.sigConfigurationChanged.connect(self.__dataChanged)
+
+ def getCompareWidget(self):
+ """Returns the connected widget.
+
+ :rtype: CompareImages
+ """
+ if self.__compareWidget is None:
+ return None
+ else:
+ return self.__compareWidget()
+
+ def __plotSignalReceived(self, event):
+ """Called when old style signals at emmited from the plot."""
+ if event["event"] == "mouseMoved":
+ x, y = event["x"], event["y"]
+ self.__mouseMoved(x, y)
+
+ def __mouseMoved(self, x, y):
+ """Called when mouse move over the plot."""
+ self._pos = x, y
+ self._updateStatusBar()
+
+ def __dataChanged(self):
+ """Called when internal data from the connected widget changes."""
+ self._updateStatusBar()
+
+ def _formatData(self, data):
+ """Format pixel of an image.
+
+ It supports intensity, RGB, and RGBA.
+
+ :param Union[int,float,numpy.ndarray,str]: Value of a pixel
+ :rtype: str
+ """
+ if data is None:
+ return "No data"
+ if isinstance(data, (int, numpy.integer)):
+ return "%d" % data
+ if isinstance(data, (float, numpy.floating)):
+ return "%f" % data
+ if isinstance(data, numpy.ndarray):
+ # RGBA value
+ if data.shape == (3,):
+ return "R:%d G:%d B:%d" % (data[0], data[1], data[2])
+ elif data.shape == (4,):
+ return "R:%d G:%d B:%d A:%d" % (data[0], data[1], data[2], data[3])
+ _logger.debug("Unsupported data format %s. Cast it to string.", type(data))
+ return str(data)
+
+ def _updateStatusBar(self):
+ """Update the content of the status bar"""
+ widget = self.getCompareWidget()
+ if widget is None:
+ self._label1.setText("ImageA: NA")
+ self._label2.setText("ImageB: NA")
+ self._transform.setVisible(False)
+ else:
+ transform = widget.getTransformation()
+ self._transform.setVisible(transform is not None)
+ if transform is not None:
+ has_notable_translation = not numpy.isclose(
+ transform.tx, 0.0, atol=0.01
+ ) or not numpy.isclose(transform.ty, 0.0, atol=0.01)
+ has_notable_scale = not numpy.isclose(
+ transform.sx, 1.0, atol=0.01
+ ) or not numpy.isclose(transform.sy, 1.0, atol=0.01)
+ has_notable_rotation = not numpy.isclose(transform.rot, 0.0, atol=0.01)
+
+ strings = []
+ if has_notable_translation:
+ strings.append("Translation")
+ if has_notable_scale:
+ strings.append("Scale")
+ if has_notable_rotation:
+ strings.append("Rotation")
+ if strings == []:
+ has_translation = not numpy.isclose(
+ transform.tx, 0.0
+ ) or not numpy.isclose(transform.ty, 0.0)
+ has_scale = not numpy.isclose(
+ transform.sx, 1.0
+ ) or not numpy.isclose(transform.sy, 1.0)
+ has_rotation = not numpy.isclose(transform.rot, 0.0)
+ if has_translation or has_scale or has_rotation:
+ text = "No big changes"
+ else:
+ text = "No changes"
+ else:
+ text = "+".join(strings)
+ self._transform.setText("Align: " + text)
+
+ strings = []
+ if not numpy.isclose(transform.ty, 0.0):
+ strings.append("Translation x: %0.3fpx" % transform.tx)
+ if not numpy.isclose(transform.ty, 0.0):
+ strings.append("Translation y: %0.3fpx" % transform.ty)
+ if not numpy.isclose(transform.sx, 1.0):
+ strings.append("Scale x: %0.3f" % transform.sx)
+ if not numpy.isclose(transform.sy, 1.0):
+ strings.append("Scale y: %0.3f" % transform.sy)
+ if not numpy.isclose(transform.rot, 0.0):
+ strings.append(
+ "Rotation: %0.3fdeg" % (transform.rot * 180 / numpy.pi)
+ )
+ if strings == []:
+ text = "No transformation"
+ else:
+ text = "\n".join(strings)
+ self._transform.setToolTip(text)
+
+ if self._pos is None:
+ self._label1.setText("ImageA: NA")
+ self._label2.setText("ImageB: NA")
+ else:
+ data1, data2 = widget.getRawPixelData(self._pos[0], self._pos[1])
+ if isinstance(data1, str):
+ self._label1.setToolTip(data1)
+ text1 = "NA"
+ else:
+ self._label1.setToolTip("")
+ text1 = self._formatData(data1)
+ if isinstance(data2, str):
+ self._label2.setToolTip(data2)
+ text2 = "NA"
+ else:
+ self._label2.setToolTip("")
+ text2 = self._formatData(data2)
+ self._label1.setText("ImageA: %s" % text1)
+ self._label2.setText("ImageB: %s" % text2)
diff --git a/src/silx/gui/plot/tools/compare/toolbar.py b/src/silx/gui/plot/tools/compare/toolbar.py
new file mode 100644
index 0000000..a7f56ec
--- /dev/null
+++ b/src/silx/gui/plot/tools/compare/toolbar.py
@@ -0,0 +1,390 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# 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 tool bar helper.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "28/06/2018"
+
+
+import logging
+import weakref
+from typing import List, Optional
+
+from silx.gui import qt
+from silx.gui import icons
+from .core import AlignmentMode
+from .core import VisualizationMode
+from .core import sift
+
+
+_logger = logging.getLogger(__name__)
+
+
+class AlignmentModeToolButton(qt.QToolButton):
+ """ToolButton to select a AlignmentMode"""
+
+ sigSelected = qt.Signal(AlignmentMode)
+
+ def __init__(self, parent=None):
+ super(AlignmentModeToolButton, self).__init__(parent=parent)
+
+ menu = qt.QMenu(self)
+ self.setMenu(menu)
+
+ self.__group = qt.QActionGroup(self)
+ self.__group.setExclusive(True)
+ self.__group.triggered.connect(self.__selectionChanged)
+
+ icon = icons.getQIcon("compare-align-origin")
+ action = qt.QAction(icon, "Align images on their upper-left pixel", self)
+ action.setProperty("enum", AlignmentMode.ORIGIN)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ self.__originAlignAction = action
+ menu.addAction(action)
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-align-center")
+ action = qt.QAction(icon, "Center images", self)
+ action.setProperty("enum", AlignmentMode.CENTER)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ self.__centerAlignAction = action
+ menu.addAction(action)
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-align-stretch")
+ action = qt.QAction(icon, "Stretch the second image on the first one", self)
+ action.setProperty("enum", AlignmentMode.STRETCH)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ self.__stretchAlignAction = action
+ menu.addAction(action)
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-align-auto")
+ action = qt.QAction(icon, "Auto-alignment of the second image", self)
+ action.setProperty("enum", AlignmentMode.AUTO)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ self.__autoAlignAction = action
+ menu.addAction(action)
+ if sift is None:
+ action.setEnabled(False)
+ action.setToolTip("Sift module is not available")
+ self.__group.addAction(action)
+
+ def getActionFromMode(self, mode: AlignmentMode) -> Optional[qt.QAction]:
+ """Returns an action from it's mode"""
+ for action in self.__group.actions():
+ actionMode = action.property("enum")
+ if mode == actionMode:
+ return action
+ return None
+
+ def setVisibleModes(self, modes: List[AlignmentMode]):
+ """Make visible only a set of modes.
+
+ The order does not matter.
+ """
+ modes = set(modes)
+ for action in self.__group.actions():
+ mode = action.property("enum")
+ action.setVisible(mode in modes)
+
+ def __selectionChanged(self, selectedAction: qt.QAction):
+ """Called when user requesting changes of the alignment mode."""
+ self.__updateMenu()
+ mode = self.getSelected()
+ self.sigSelected.emit(mode)
+
+ def __updateMenu(self):
+ """Update the state of the action containing alignment menu."""
+ selectedAction = self.__group.checkedAction()
+ if selectedAction is not None:
+ self.setText(selectedAction.text())
+ self.setIcon(selectedAction.icon())
+ self.setToolTip(selectedAction.toolTip())
+ else:
+ self.setText("")
+ self.setIcon(qt.QIcon())
+ self.setToolTip("")
+
+ def getSelected(self) -> AlignmentMode:
+ action = self.__group.checkedAction()
+ if action is None:
+ return None
+ return action.property("enum")
+
+ def setSelected(self, mode: AlignmentMode):
+ action = self.getActionFromMode(mode)
+ old = self.__group.blockSignals(True)
+ if action is not None:
+ # Check this action
+ action.setChecked(True)
+ else:
+ action = self.__group.checkedAction()
+ if action is not None:
+ # Uncheck this action
+ action.setChecked(False)
+ self.__updateMenu()
+ self.__group.blockSignals(old)
+
+
+class VisualizationModeToolButton(qt.QToolButton):
+ """ToolButton to select a VisualisationMode"""
+
+ sigSelected = qt.Signal(VisualizationMode)
+
+ def __init__(self, parent=None):
+ super(VisualizationModeToolButton, self).__init__(parent=parent)
+
+ menu = qt.QMenu(self)
+ self.setMenu(menu)
+
+ self.__group = qt.QActionGroup(self)
+ self.__group.setExclusive(True)
+ self.__group.triggered.connect(self.__selectionChanged)
+
+ icon = icons.getQIcon("compare-mode-a")
+ action = qt.QAction(icon, "Display the first image only", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_A))
+ action.setProperty("enum", VisualizationMode.ONLY_A)
+ menu.addAction(action)
+ self.__aModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-b")
+ action = qt.QAction(icon, "Display the second image only", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_B))
+ action.setProperty("enum", VisualizationMode.ONLY_B)
+ menu.addAction(action)
+ self.__bModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-vline")
+ action = qt.QAction(icon, "Vertical compare mode", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_V))
+ action.setProperty("enum", VisualizationMode.VERTICAL_LINE)
+ menu.addAction(action)
+ self.__vlineModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-hline")
+ action = qt.QAction(icon, "Horizontal compare mode", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_H))
+ action.setProperty("enum", VisualizationMode.HORIZONTAL_LINE)
+ menu.addAction(action)
+ self.__hlineModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-rb-channel")
+ action = qt.QAction(icon, "Blue/red compare mode (additive mode)", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_C))
+ action.setProperty("enum", VisualizationMode.COMPOSITE_RED_BLUE_GRAY)
+ menu.addAction(action)
+ self.__brChannelModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-rbneg-channel")
+ action = qt.QAction(icon, "Yellow/cyan compare mode (subtractive mode)", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_Y))
+ action.setProperty("enum", VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG)
+ menu.addAction(action)
+ self.__ycChannelModeAction = action
+ self.__group.addAction(action)
+
+ icon = icons.getQIcon("compare-mode-a-minus-b")
+ action = qt.QAction(icon, "Raw A minus B compare mode", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
+ action.setProperty("enum", VisualizationMode.COMPOSITE_A_MINUS_B)
+ menu.addAction(action)
+ self.__ycChannelModeAction = action
+ self.__group.addAction(action)
+
+ def getActionFromMode(self, mode: VisualizationMode) -> Optional[qt.QAction]:
+ """Returns an action from it's mode"""
+ for action in self.__group.actions():
+ actionMode = action.property("enum")
+ if mode == actionMode:
+ return action
+ return None
+
+ def setVisibleModes(self, modes: List[VisualizationMode]):
+ """Make visible only a set of modes.
+
+ The order does not matter.
+ """
+ modes = set(modes)
+ for action in self.__group.actions():
+ mode = action.property("enum")
+ action.setVisible(mode in modes)
+
+ def __selectionChanged(self, selectedAction: qt.QAction):
+ """Called when user requesting changes of the visualization mode."""
+ self.__updateMenu()
+ mode = self.getSelected()
+ self.sigSelected.emit(mode)
+
+ def __updateMenu(self):
+ """Update the state of the action containing visualization menu."""
+ selectedAction = self.__group.checkedAction()
+ if selectedAction is not None:
+ self.setText(selectedAction.text())
+ self.setIcon(selectedAction.icon())
+ self.setToolTip(selectedAction.toolTip())
+ else:
+ self.setText("")
+ self.setIcon(qt.QIcon())
+ self.setToolTip("")
+
+ def getSelected(self) -> VisualizationMode:
+ action = self.__group.checkedAction()
+ if action is None:
+ return None
+ return action.property("enum")
+
+ def setSelected(self, mode: VisualizationMode):
+ action = self.getActionFromMode(mode)
+ old = self.__group.blockSignals(True)
+ if action is not None:
+ # Check this action
+ action.setChecked(True)
+ else:
+ action = self.__group.checkedAction()
+ if action is not None:
+ # Uncheck this action
+ action.setChecked(False)
+ self.__updateMenu()
+ self.__group.blockSignals(old)
+
+
+class CompareImagesToolBar(qt.QToolBar):
+ """ToolBar containing specific tools to custom the configuration of a
+ :class:`CompareImages` widget
+
+ Use :meth:`setCompareWidget` to connect this toolbar to a specific
+ :class:`CompareImages` widget.
+
+ :param Union[qt.QWidget,None] parent: Parent of this widget.
+ """
+
+ def __init__(self, parent=None):
+ qt.QToolBar.__init__(self, parent)
+ self.setWindowTitle("Compare images")
+
+ self.__compareWidget = None
+
+ self.__visualizationToolButton = VisualizationModeToolButton(self)
+ self.__visualizationToolButton.setPopupMode(qt.QToolButton.InstantPopup)
+ self.__visualizationToolButton.sigSelected.connect(self.__visualizationChanged)
+ self.addWidget(self.__visualizationToolButton)
+
+ self.__alignmentToolButton = AlignmentModeToolButton(self)
+ self.__alignmentToolButton.setPopupMode(qt.QToolButton.InstantPopup)
+ self.__alignmentToolButton.sigSelected.connect(self.__alignmentChanged)
+ self.addWidget(self.__alignmentToolButton)
+
+ icon = icons.getQIcon("compare-keypoints")
+ action = qt.QAction(icon, "Display/hide alignment keypoints", self)
+ action.setCheckable(True)
+ action.triggered.connect(self.__keypointVisibilityChanged)
+ self.addAction(action)
+ self.__displayKeypoints = action
+
+ def __visualizationChanged(self, mode: VisualizationMode):
+ widget = self.getCompareWidget()
+ if widget is not None:
+ widget.setVisualizationMode(mode)
+
+ def __alignmentChanged(self, mode: AlignmentMode):
+ widget = self.getCompareWidget()
+ if widget is not None:
+ widget.setAlignmentMode(mode)
+
+ def setCompareWidget(self, widget):
+ """
+ Connect this tool bar to a specific :class:`CompareImages` widget.
+
+ :param Union[None,CompareImages] widget: The widget to connect with.
+ """
+ compareWidget = self.getCompareWidget()
+ if compareWidget is not None:
+ compareWidget.sigConfigurationChanged.disconnect(
+ self.__updateSelectedActions
+ )
+ compareWidget = widget
+ self.setEnabled(compareWidget is not None)
+ if compareWidget is None:
+ self.__compareWidget = None
+ else:
+ self.__compareWidget = weakref.ref(compareWidget)
+ if compareWidget is not None:
+ widget.sigConfigurationChanged.connect(self.__updateSelectedActions)
+ self.__updateSelectedActions()
+
+ def getCompareWidget(self):
+ """Returns the connected widget.
+
+ :rtype: CompareImages
+ """
+ if self.__compareWidget is None:
+ return None
+ else:
+ return self.__compareWidget()
+
+ def __updateSelectedActions(self):
+ """
+ Update the state of this tool bar according to the state of the
+ connected :class:`CompareImages` widget.
+ """
+ widget = self.getCompareWidget()
+ if widget is None:
+ return
+ self.__visualizationToolButton.setSelected(widget.getVisualizationMode())
+ self.__alignmentToolButton.setSelected(widget.getAlignmentMode())
+ self.__displayKeypoints.setChecked(widget.getKeypointsVisible())
+
+ def __keypointVisibilityChanged(self):
+ """Called when action managing keypoints visibility changes"""
+ widget = self.getCompareWidget()
+ if widget is not None:
+ keypointsVisible = self.__displayKeypoints.isChecked()
+ widget.setKeypointsVisible(keypointsVisible)
diff --git a/src/silx/gui/plot/tools/menus.py b/src/silx/gui/plot/tools/menus.py
new file mode 100644
index 0000000..c748b6e
--- /dev/null
+++ b/src/silx/gui/plot/tools/menus.py
@@ -0,0 +1,93 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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 :class:`PlotWidget`-related QMenu.
+
+The following QMenu is available:
+
+- :class:`ZoomEnabledAxesMenu`
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "12/06/2023"
+
+
+import weakref
+from typing import Optional
+
+from silx.gui import qt
+
+from ..PlotWidget import PlotWidget
+
+
+class ZoomEnabledAxesMenu(qt.QMenu):
+ """Menu to toggle axes for zoom interaction"""
+
+ def __init__(self, plot: PlotWidget, parent: Optional[qt.QWidget] = None):
+ super().__init__(parent)
+ self.setTitle("Zoom axes")
+
+ assert isinstance(plot, PlotWidget)
+ self.__plotRef = weakref.ref(plot)
+
+ self.addSection("Enabled axes")
+ self.__xAxisAction = qt.QAction("X axis", parent=self)
+ self.__yAxisAction = qt.QAction("Y left axis", parent=self)
+ self.__y2AxisAction = qt.QAction("Y right axis", parent=self)
+
+ for action in (self.__xAxisAction, self.__yAxisAction, self.__y2AxisAction):
+ action.setCheckable(True)
+ action.setChecked(True)
+ action.triggered.connect(self._axesActionTriggered)
+ self.addAction(action)
+
+ # Listen to interaction configuration change
+ plot.interaction().sigChanged.connect(self._interactionChanged)
+ # Init the state
+ self._interactionChanged()
+
+ def getPlotWidget(self) -> Optional[PlotWidget]:
+ return self.__plotRef()
+
+ def _axesActionTriggered(self, checked=False):
+ plot = self.getPlotWidget()
+ if plot is None:
+ return
+
+ plot.interaction().setZoomEnabledAxes(
+ self.__xAxisAction.isChecked(),
+ self.__yAxisAction.isChecked(),
+ self.__y2AxisAction.isChecked(),
+ )
+
+ def _interactionChanged(self):
+ plot = self.getPlotWidget()
+ if plot is None:
+ return
+
+ enabledAxes = plot.interaction().getZoomEnabledAxes()
+ self.__xAxisAction.setChecked(enabledAxes.xaxis)
+ self.__yAxisAction.setChecked(enabledAxes.yaxis)
+ self.__y2AxisAction.setChecked(enabledAxes.y2axis)
diff --git a/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py b/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
index 44187ef..271adb8 100644
--- a/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
+++ b/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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,7 +29,6 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-from silx.utils import deprecation
from . import toolbar
@@ -39,16 +37,8 @@ class ScatterProfileToolBar(toolbar.ProfileToolBar):
:param parent: See :class:`QToolBar`.
:param plot: :class:`~silx.gui.plot.PlotWidget` on which to operate.
- :param str title: See :class:`QToolBar`.
"""
- def __init__(self, parent=None, plot=None, title=None):
+ def __init__(self, parent=None, plot=None):
super(ScatterProfileToolBar, self).__init__(parent, plot)
- if title is not None:
- deprecation.deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
self.setScheme("scatter")
diff --git a/src/silx/gui/plot/tools/profile/__init__.py b/src/silx/gui/plot/tools/profile/__init__.py
index d91191e..a72b5d2 100644
--- a/src/silx/gui/plot/tools/profile/__init__.py
+++ b/src/silx/gui/plot/tools/profile/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/tools/profile/core.py b/src/silx/gui/plot/tools/profile/core.py
index 200f5cf..194f459 100644
--- a/src/silx/gui/plot/tools/profile/core.py
+++ b/src/silx/gui/plot/tools/profile/core.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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,49 +24,63 @@
"""This module define core objects for profile tools.
"""
+from __future__ import annotations
+
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno", "V. Valls"]
__license__ = "MIT"
__date__ = "17/04/2020"
-import collections
+import typing
import numpy
import weakref
from silx.image.bilinear import BilinearImage
from silx.gui import qt
+from silx.gui import colors
+import silx.gui.plot.items
+
+
+class CurveProfileData(typing.NamedTuple):
+ coords: numpy.ndarray
+ profile: numpy.ndarray
+ title: str
+ xLabel: str
+ yLabel: str
+
+
+class RgbaProfileData(typing.NamedTuple):
+ coords: numpy.ndarray
+ profile: numpy.ndarray
+ profile_r: numpy.ndarray
+ profile_g: numpy.ndarray
+ profile_b: numpy.ndarray
+ profile_a: numpy.ndarray
+ title: str
+ xLabel: str
+ yLabel: str
+
+
+class ImageProfileData(typing.NamedTuple):
+ coords: numpy.ndarray
+ profile: numpy.ndarray
+ title: str
+ xLabel: str
+ yLabel: str
+ colormap: colors.Colormap
-CurveProfileData = collections.namedtuple(
- 'CurveProfileData', [
- "coords",
- "profile",
- "title",
- "xLabel",
- "yLabel",
- ])
-
-RgbaProfileData = collections.namedtuple(
- 'RgbaProfileData', [
- "coords",
- "profile",
- "profile_r",
- "profile_g",
- "profile_b",
- "profile_a",
- "title",
- "xLabel",
- "yLabel",
- ])
-
-ImageProfileData = collections.namedtuple(
- 'ImageProfileData', [
- 'coords',
- 'profile',
- 'title',
- 'xLabel',
- 'yLabel',
- 'colormap',
- ])
+class CurveProfileDesc(typing.NamedTuple):
+ profile: numpy.ndarray
+ name: typing.Optional[str] = None
+ color: typing.Optional[str] = None
+
+
+class CurvesProfileData(typing.NamedTuple):
+ coords: numpy.ndarray
+ profiles: typing.List[CurveProfileDesc]
+ title: str
+ xLabel: str
+ yLabel: str
class ProfileRoiMixIn:
@@ -108,7 +121,7 @@ class ProfileRoiMixIn:
def _setPlotItem(self, plotItem):
"""Specify the plot item to use with this profile
- :param `~silx.gui.plot.items.item.Item` plotItem: A plot item
+ :param `~silx.gui.plot.items.Item` plotItem: A plot item
"""
previousPlotItem = self.getPlotItem()
if previousPlotItem is plotItem:
@@ -119,7 +132,7 @@ class ProfileRoiMixIn:
def getPlotItem(self):
"""Returns the plot item used by this profile
- :rtype: `~silx.gui.plot.items.item.Item`
+ :rtype: `~silx.gui.plot.items.Item`
"""
if self.__plotItem is None:
return None
@@ -172,15 +185,18 @@ class ProfileRoiMixIn:
except ValueError:
pass
- def computeProfile(self, item):
+ def computeProfile(
+ self, item: silx.gui.plot.items.Item
+ ) -> typing.Union[
+ CurveProfileData, ImageProfileData, RgbaProfileData, CurvesProfileData
+ ]:
"""
Compute the profile which will be displayed.
This method is not called from the main Qt thread, but from a thread
pool.
- :param ~silx.gui.plot.items.Item item: A plot item
- :rtype: Union[CurveProfileData,ImageProfileData]
+ :param item: A plot item
"""
raise NotImplementedError()
@@ -202,7 +218,7 @@ def _alignedFullProfile(data, origin, scale, position, roiWidth, axis, method):
"""
assert axis in (0, 1)
assert len(data.shape) == 3
- assert method in ('mean', 'sum', 'none')
+ assert method in ("mean", "sum", "none")
# Convert from plot to image coords
imgPos = int((position - origin[1 - axis]) / scale[1 - axis])
@@ -216,31 +232,35 @@ def _alignedFullProfile(data, origin, scale, position, roiWidth, axis, method):
roiWidth = min(height, roiWidth) # Clip roi width to image size
# Get [start, end[ coords of the roi in the data
- start = int(int(imgPos) + 0.5 - roiWidth / 2.)
+ start = int(int(imgPos) + 0.5 - roiWidth / 2.0)
start = min(max(0, start), height - roiWidth)
end = start + roiWidth
- if method == 'none':
+ if method == "none":
profile = None
else:
if start < height and end > 0:
- if method == 'mean':
+ if method == "mean":
fct = numpy.mean
- elif method == 'sum':
+ elif method == "sum":
fct = numpy.sum
else:
- raise ValueError('method not managed')
- profile = fct(data[:, max(0, start):min(end, height), :], axis=1).astype(numpy.float32)
+ raise ValueError("method not managed")
+ profile = fct(data[:, max(0, start) : min(end, height), :], axis=1).astype(
+ numpy.float32
+ )
else:
profile = numpy.zeros((nimages, width), dtype=numpy.float32)
# Compute effective ROI in plot coords
- profileBounds = numpy.array(
- (0, width, width, 0),
- dtype=numpy.float32) * scale[axis] + origin[axis]
- roiBounds = numpy.array(
- (start, start, end, end),
- dtype=numpy.float32) * scale[1 - axis] + origin[1 - axis]
+ profileBounds = (
+ numpy.array((0, width, width, 0), dtype=numpy.float32) * scale[axis]
+ + origin[axis]
+ )
+ roiBounds = (
+ numpy.array((start, start, end, end), dtype=numpy.float32) * scale[1 - axis]
+ + origin[1 - axis]
+ )
if axis == 0: # Horizontal profile
area = profileBounds, roiBounds
@@ -273,7 +293,7 @@ def _alignedPartialProfile(data, rowRange, colRange, axis, method):
assert len(data.shape) == 3
assert rowRange[0] < rowRange[1]
assert colRange[0] < colRange[1]
- assert method in ('mean', 'sum')
+ assert method in ("mean", "sum")
nimages, height, width = data.shape
@@ -288,22 +308,23 @@ def _alignedPartialProfile(data, rowRange, colRange, axis, method):
colStart = min(max(0, colRange[0]), width)
colEnd = min(max(0, colRange[1]), width)
- if method == 'mean':
+ if method == "mean":
_fct = numpy.mean
- elif method == 'sum':
+ elif method == "sum":
_fct = numpy.sum
else:
- raise ValueError('method not managed')
+ raise ValueError("method not managed")
- imgProfile = _fct(data[:, rowStart:rowEnd, colStart:colEnd], axis=axis + 1,
- dtype=numpy.float32)
+ imgProfile = _fct(
+ data[:, rowStart:rowEnd, colStart:colEnd], axis=axis + 1, dtype=numpy.float32
+ )
# Profile including out of bound area
profile = numpy.zeros((nimages, profileLength), dtype=numpy.float32)
# Place imgProfile in full profile
- offset = - min(0, profileRange[0])
- profile[:, offset:offset + imgProfile.shape[1]] = imgProfile
+ offset = -min(0, profileRange[0])
+ profile[:, offset : offset + imgProfile.shape[1]] = imgProfile
return profile
@@ -347,14 +368,12 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
roiWidth = max(1, lineWidth)
roiStart, roiEnd, lineProjectionMode = roiInfo
- if lineProjectionMode == 'X': # Horizontal profile on the whole image
- profile, area = _alignedFullProfile(currentData3D,
- origin, scale,
- roiStart[1], roiWidth,
- axis=0,
- method=method)
+ if lineProjectionMode == "X": # Horizontal profile on the whole image
+ profile, area = _alignedFullProfile(
+ currentData3D, origin, scale, roiStart[1], roiWidth, axis=0, method=method
+ )
- if method == 'none':
+ if method == "none":
coords = None
else:
coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
@@ -362,19 +381,17 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
yMin, yMax = min(area[1]), max(area[1]) - 1
if roiWidth <= 1:
- profileName = '{ylabel} = %g' % yMin
+ profileName = "{ylabel} = %g" % yMin
else:
- profileName = '{ylabel} = [%g, %g]' % (yMin, yMax)
- xLabel = '{xlabel}'
+ profileName = "{ylabel} = [%g, %g]" % (yMin, yMax)
+ xLabel = "{xlabel}"
- elif lineProjectionMode == 'Y': # Vertical profile on the whole image
- profile, area = _alignedFullProfile(currentData3D,
- origin, scale,
- roiStart[0], roiWidth,
- axis=1,
- method=method)
+ elif lineProjectionMode == "Y": # Vertical profile on the whole image
+ profile, area = _alignedFullProfile(
+ currentData3D, origin, scale, roiStart[0], roiWidth, axis=1, method=method
+ )
- if method == 'none':
+ if method == "none":
coords = None
else:
coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
@@ -382,21 +399,20 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
xMin, xMax = min(area[0]), max(area[0]) - 1
if roiWidth <= 1:
- profileName = '{xlabel} = %g' % xMin
+ profileName = "{xlabel} = %g" % xMin
else:
- profileName = '{xlabel} = [%g, %g]' % (xMin, xMax)
- xLabel = '{ylabel}'
+ profileName = "{xlabel} = [%g, %g]" % (xMin, xMax)
+ xLabel = "{ylabel}"
else: # Free line profile
-
# Convert start and end points in image coords as (row, col)
- startPt = ((roiStart[1] - origin[1]) / scale[1],
- (roiStart[0] - origin[0]) / scale[0])
- endPt = ((roiEnd[1] - origin[1]) / scale[1],
- (roiEnd[0] - origin[0]) / scale[0])
+ startPt = (
+ (roiStart[1] - origin[1]) / scale[1],
+ (roiStart[0] - origin[0]) / scale[0],
+ )
+ endPt = ((roiEnd[1] - origin[1]) / scale[1], (roiEnd[0] - origin[0]) / scale[0])
- if (int(startPt[0]) == int(endPt[0]) or
- int(startPt[1]) == int(endPt[1])):
+ if int(startPt[0]) == int(endPt[0]) or int(startPt[1]) == int(endPt[1]):
# Profile is aligned with one of the axes
# Convert to int
@@ -408,62 +424,75 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
startPt, endPt = endPt, startPt
if startPt[0] == endPt[0]: # Row aligned
- rowRange = (int(startPt[0] + 0.5 - 0.5 * roiWidth),
- int(startPt[0] + 0.5 + 0.5 * roiWidth))
+ rowRange = (
+ int(startPt[0] + 0.5 - 0.5 * roiWidth),
+ int(startPt[0] + 0.5 + 0.5 * roiWidth),
+ )
colRange = startPt[1], endPt[1] + 1
- if method == 'none':
+ if method == "none":
profile = None
else:
- profile = _alignedPartialProfile(currentData3D,
- rowRange, colRange,
- axis=0,
- method=method)
+ profile = _alignedPartialProfile(
+ currentData3D, rowRange, colRange, axis=0, method=method
+ )
else: # Column aligned
rowRange = startPt[0], endPt[0] + 1
- colRange = (int(startPt[1] + 0.5 - 0.5 * roiWidth),
- int(startPt[1] + 0.5 + 0.5 * roiWidth))
- if method == 'none':
+ colRange = (
+ int(startPt[1] + 0.5 - 0.5 * roiWidth),
+ int(startPt[1] + 0.5 + 0.5 * roiWidth),
+ )
+ if method == "none":
profile = None
else:
- profile = _alignedPartialProfile(currentData3D,
- rowRange, colRange,
- axis=1,
- method=method)
+ profile = _alignedPartialProfile(
+ currentData3D, rowRange, colRange, axis=1, method=method
+ )
# Convert ranges to plot coords to draw ROI area
area = (
numpy.array(
(colRange[0], colRange[1], colRange[1], colRange[0]),
- dtype=numpy.float32) * scale[0] + origin[0],
+ dtype=numpy.float32,
+ )
+ * scale[0]
+ + origin[0],
numpy.array(
(rowRange[0], rowRange[0], rowRange[1], rowRange[1]),
- dtype=numpy.float32) * scale[1] + origin[1])
+ dtype=numpy.float32,
+ )
+ * scale[1]
+ + origin[1],
+ )
else: # General case: use bilinear interpolation
-
# Ensure startPt <= endPt
- if (startPt[1] > endPt[1] or (
- startPt[1] == endPt[1] and startPt[0] > endPt[0])):
+ if startPt[1] > endPt[1] or (
+ startPt[1] == endPt[1] and startPt[0] > endPt[0]
+ ):
startPt, endPt = endPt, startPt
- if method == 'none':
+ if method == "none":
profile = None
else:
profile = []
for slice_idx in range(currentData3D.shape[0]):
bilinear = BilinearImage(currentData3D[slice_idx, :, :])
- profile.append(bilinear.profile_line(
- (startPt[0] - 0.5, startPt[1] - 0.5),
- (endPt[0] - 0.5, endPt[1] - 0.5),
- roiWidth,
- method=method))
+ profile.append(
+ bilinear.profile_line(
+ (startPt[0] - 0.5, startPt[1] - 0.5),
+ (endPt[0] - 0.5, endPt[1] - 0.5),
+ roiWidth,
+ method=method,
+ )
+ )
profile = numpy.array(profile)
# Extend ROI with half a pixel on each end, and
# Convert back to plot coords (x, y)
- length = numpy.sqrt((endPt[0] - startPt[0]) ** 2 +
- (endPt[1] - startPt[1]) ** 2)
+ length = numpy.sqrt(
+ (endPt[0] - startPt[0]) ** 2 + (endPt[1] - startPt[1]) ** 2
+ )
dRow = (endPt[0] - startPt[0]) / length
dCol = (endPt[1] - startPt[1]) / length
@@ -475,16 +504,29 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
dRow, dCol = dCol, -dRow
area = (
- numpy.array((roiStartPt[1] - 0.5 * roiWidth * dCol,
- roiStartPt[1] + 0.5 * roiWidth * dCol,
- roiEndPt[1] + 0.5 * roiWidth * dCol,
- roiEndPt[1] - 0.5 * roiWidth * dCol),
- dtype=numpy.float32) * scale[0] + origin[0],
- numpy.array((roiStartPt[0] - 0.5 * roiWidth * dRow,
- roiStartPt[0] + 0.5 * roiWidth * dRow,
- roiEndPt[0] + 0.5 * roiWidth * dRow,
- roiEndPt[0] - 0.5 * roiWidth * dRow),
- dtype=numpy.float32) * scale[1] + origin[1])
+ numpy.array(
+ (
+ roiStartPt[1] - 0.5 * roiWidth * dCol,
+ roiStartPt[1] + 0.5 * roiWidth * dCol,
+ roiEndPt[1] + 0.5 * roiWidth * dCol,
+ roiEndPt[1] - 0.5 * roiWidth * dCol,
+ ),
+ dtype=numpy.float32,
+ )
+ * scale[0]
+ + origin[0],
+ numpy.array(
+ (
+ roiStartPt[0] - 0.5 * roiWidth * dRow,
+ roiStartPt[0] + 0.5 * roiWidth * dRow,
+ roiEndPt[0] + 0.5 * roiWidth * dRow,
+ roiEndPt[0] - 0.5 * roiWidth * dRow,
+ ),
+ dtype=numpy.float32,
+ )
+ * scale[1]
+ + origin[1],
+ )
# Convert start and end points back to plot coords
y0 = startPt[0] * scale[1] + origin[1]
@@ -493,33 +535,33 @@ def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
x1 = endPt[1] * scale[0] + origin[0]
if startPt[1] == endPt[1]:
- profileName = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1)
- if method == 'none':
+ profileName = "{xlabel} = %g; {ylabel} = [%g, %g]" % (x0, y0, y1)
+ if method == "none":
coords = None
else:
coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
coords = coords * scale[1] + y0
- xLabel = '{ylabel}'
+ xLabel = "{ylabel}"
elif startPt[0] == endPt[0]:
- profileName = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1)
- if method == 'none':
+ profileName = "{ylabel} = %g; {xlabel} = [%g, %g]" % (y0, x0, x1)
+ if method == "none":
coords = None
else:
coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
coords = coords * scale[0] + x0
- xLabel = '{xlabel}'
+ xLabel = "{xlabel}"
else:
m = (y1 - y0) / (x1 - x0)
b = y0 - m * x0
- profileName = '{ylabel} = %g * {xlabel} %+g' % (m, b)
- if method == 'none':
+ profileName = "{ylabel} = %g * {xlabel} %+g" % (m, b)
+ if method == "none":
coords = None
else:
- coords = numpy.linspace(x0, x1, len(profile[0]),
- endpoint=True,
- dtype=numpy.float32)
- xLabel = '{xlabel}'
+ coords = numpy.linspace(
+ x0, x1, len(profile[0]), endpoint=True, dtype=numpy.float32
+ )
+ xLabel = "{xlabel}"
return coords, profile, area, profileName, xLabel
diff --git a/src/silx/gui/plot/tools/profile/editors.py b/src/silx/gui/plot/tools/profile/editors.py
index 80e0452..d53f775 100644
--- a/src/silx/gui/plot/tools/profile/editors.py
+++ b/src/silx/gui/plot/tools/profile/editors.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
@@ -44,7 +43,6 @@ _logger = logging.getLogger(__name__)
class _NoProfileRoiEditor(qt.QWidget):
-
sigDataCommited = qt.Signal()
def setEditorData(self, roi):
@@ -55,7 +53,6 @@ class _NoProfileRoiEditor(qt.QWidget):
class _DefaultImageProfileRoiEditor(qt.QWidget):
-
sigDataCommited = qt.Signal()
def __init__(self, parent=None):
@@ -73,7 +70,7 @@ class _DefaultImageProfileRoiEditor(qt.QWidget):
self._methodsButton = ProfileOptionToolButton(parent=self, plot=None)
self._methodsButton.sigMethodChanged.connect(self._widgetChanged)
- label = qt.QLabel('W:')
+ label = qt.QLabel("W:")
label.setToolTip("Line width in pixels")
layout.addWidget(label)
layout.addWidget(self._lineWidth)
@@ -100,7 +97,6 @@ class _DefaultImageProfileRoiEditor(qt.QWidget):
class _DefaultImageStackProfileRoiEditor(_DefaultImageProfileRoiEditor):
-
def _initLayout(self, layout):
super(_DefaultImageStackProfileRoiEditor, self)._initLayout(layout)
self._profileDim = ProfileToolButton(parent=self, plot=None)
@@ -122,7 +118,6 @@ class _DefaultImageStackProfileRoiEditor(_DefaultImageProfileRoiEditor):
class _DefaultScatterProfileRoiEditor(qt.QWidget):
-
sigDataCommited = qt.Signal()
def __init__(self, parent=None):
@@ -135,7 +130,7 @@ class _DefaultScatterProfileRoiEditor(qt.QWidget):
layout = qt.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
- label = qt.QLabel('Samples:')
+ label = qt.QLabel("Samples:")
label.setToolTip("Number of sample points of the profile")
layout.addWidget(label)
layout.addWidget(self._nPoints)
@@ -161,6 +156,7 @@ class ProfileRoiEditorAction(qt.QWidgetAction):
:param qt.QWidget parent: Parent widget
"""
+
def __init__(self, parent=None):
super(ProfileRoiEditorAction, self).__init__(parent)
self.__roiManager = None
@@ -238,8 +234,7 @@ class ProfileRoiEditorAction(qt.QWidgetAction):
return self.__roi
def __roiPropertyChanged(self):
- """Handle changes on the property defining the ROI.
- """
+ """Handle changes on the property defining the ROI."""
self._updateWidgetValues()
def __setEditor(self, widget, editor):
@@ -252,7 +247,10 @@ class ProfileRoiEditorAction(qt.QWidgetAction):
return
layout = widget.layout()
if previousEditor is not None:
- previousEditor.sigDataCommited.disconnect(self._editorDataCommited)
+ try:
+ previousEditor.sigDataCommited.disconnect(self._editorDataCommited)
+ except (RuntimeError, TypeError):
+ pass
layout.removeWidget(previousEditor)
previousEditor.deleteLater()
if editor is not None:
@@ -263,16 +261,20 @@ class ProfileRoiEditorAction(qt.QWidgetAction):
"""Returns the editor class to use according to the ROI."""
if roi is None:
editorClass = _NoProfileRoiEditor
- elif isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
+ elif isinstance(
+ roi,
+ (rois._DefaultImageStackProfileRoiMixIn, rois.ProfileImageStackCrossROI),
+ ):
# Must be done before the default image ROI
# Cause ImageStack ROIs inherit from Image ROIs
editorClass = _DefaultImageStackProfileRoiEditor
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultImageProfileRoiMixIn, rois.ProfileImageCrossROI)
+ ):
editorClass = _DefaultImageProfileRoiEditor
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultScatterProfileRoiMixIn, rois.ProfileScatterCrossROI)
+ ):
editorClass = _DefaultScatterProfileRoiEditor
else:
# Unsupported
diff --git a/src/silx/gui/plot/tools/profile/manager.py b/src/silx/gui/plot/tools/profile/manager.py
index 4a22bc0..6f4ba35 100644
--- a/src/silx/gui/plot/tools/profile/manager.py
+++ b/src/silx/gui/plot/tools/profile/manager.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
@@ -65,12 +64,12 @@ class _RunnableComputeProfile(qt.QRunnable):
class _Signals(qt.QObject):
"""Signal holder"""
+
resultReady = qt.Signal(object, object)
runnerFinished = qt.Signal(object)
def __init__(self, threadPool, item, roi):
- """Constructor
- """
+ """Constructor"""
super(_RunnableComputeProfile, self).__init__()
self._signals = self._Signals()
self._signals.moveToThread(threadPool.thread())
@@ -115,8 +114,7 @@ class _RunnableComputeProfile(qt.QRunnable):
return self._signals.runnerFinished
def run(self):
- """Process the profile computation.
- """
+ """Process the profile computation."""
if not self._cancelled:
try:
profileData = self._roi.computeProfile(self._item)
@@ -142,7 +140,7 @@ class ProfileWindow(qt.QMainWindow):
def __init__(self, parent=None, backend=None):
qt.QMainWindow.__init__(self, parent=parent, flags=qt.Qt.Dialog)
- self.setWindowTitle('Profile window')
+ self.setWindowTitle("Profile window")
self._plot1D = None
self._plot2D = None
self._backend = backend
@@ -176,10 +174,13 @@ class ProfileWindow(qt.QMainWindow):
"""
# import here to avoid circular import
from ...PlotWindow import Plot1D
+
plot = Plot1D(parent=parent, backend=backend)
plot.setDataMargins(yMinMargin=0.1, yMaxMargin=0.1)
- plot.setGraphYLabel('Profile')
- plot.setGraphXLabel('')
+ plot.setGraphYLabel("Profile")
+ plot.setGraphXLabel("")
+ positionInfo = plot.getPositionInfoWidget()
+ positionInfo.setSnappingMode(positionInfo.SNAPPING_CURVE)
return plot
def createPlot2D(self, parent, backend):
@@ -193,6 +194,7 @@ class ProfileWindow(qt.QMainWindow):
"""
# import here to avoid circular import
from ...PlotWindow import Plot2D
+
return Plot2D(parent=parent, backend=backend)
def getPlot1D(self, init=True):
@@ -240,12 +242,12 @@ class ProfileWindow(qt.QMainWindow):
return
self.__color = colors.rgba(roi.getColor())
- def _setImageProfile(self, data):
+ def _setImageProfile(self, data: core.ImageProfileData):
"""
Setup the window to display a new profile data which is represented
by an image.
- :param core.ImageProfileData data: Computed data profile
+ :param data: Computed data profile
"""
plot = self.getPlot2D()
@@ -253,25 +255,26 @@ class ProfileWindow(qt.QMainWindow):
plot.setGraphTitle(data.title)
plot.getXAxis().setLabel(data.xLabel)
-
coords = data.coords
colormap = data.colormap
profileScale = (coords[-1] - coords[0]) / data.profile.shape[1], 1
- plot.addImage(data.profile,
- legend="profile",
- colormap=colormap,
- origin=(coords[0], 0),
- scale=profileScale)
+ plot.addImage(
+ data.profile,
+ legend="profile",
+ colormap=colormap,
+ origin=(coords[0], 0),
+ scale=profileScale,
+ )
plot.getYAxis().setLabel("Frame index (depth)")
self._showPlot2D()
- def _setCurveProfile(self, data):
+ def _setCurveProfile(self, data: core.CurveProfileData):
"""
Setup the window to display a new profile data which is represented
by a curve.
- :param core.CurveProfileData data: Computed data profile
+ :param data: Computed data profile
"""
plot = self.getPlot1D()
@@ -280,19 +283,16 @@ class ProfileWindow(qt.QMainWindow):
plot.getXAxis().setLabel(data.xLabel)
plot.getYAxis().setLabel(data.yLabel)
- plot.addCurve(data.coords,
- data.profile,
- legend="level",
- color=self.__color)
+ plot.addCurve(data.coords, data.profile, legend="level", color=self.__color)
self._showPlot1D()
- def _setRgbaProfile(self, data):
+ def _setRgbaProfile(self, data: core.RgbaProfileData):
"""
Setup the window to display a new profile data which is represented
by a curve.
- :param core.RgbaProfileData data: Computed data profile
+ :param data: Computed data profile
"""
plot = self.getPlot1D()
@@ -303,17 +303,33 @@ class ProfileWindow(qt.QMainWindow):
self._showPlot1D()
- plot.addCurve(data.coords, data.profile,
- legend="level", color="black")
- plot.addCurve(data.coords, data.profile_r,
- legend="red", color="red")
- plot.addCurve(data.coords, data.profile_g,
- legend="green", color="green")
- plot.addCurve(data.coords, data.profile_b,
- legend="blue", color="blue")
+ plot.addCurve(data.coords, data.profile, legend="level", color="black")
+ plot.addCurve(data.coords, data.profile_r, legend="red", color="red")
+ plot.addCurve(data.coords, data.profile_g, legend="green", color="green")
+ plot.addCurve(data.coords, data.profile_b, legend="blue", color="blue")
if data.profile_a is not None:
plot.addCurve(data.coords, data.profile_a, legend="alpha", color="gray")
+ def _setCurvesProfile(self, data: core.CurvesProfileData):
+ """
+ Setup the window to display a new profile data which is represented
+ by multiple curves.
+
+ :param data: Computed data profile
+ """
+ plot = self.getPlot1D()
+
+ plot.clear()
+ plot.setGraphTitle(data.title)
+ plot.getXAxis().setLabel(data.xLabel)
+ plot.getYAxis().setLabel(data.yLabel)
+
+ self._showPlot1D()
+
+ for i, desc in enumerate(data.profiles):
+ name = desc.name if desc.name is not None else f"profile{i}"
+ plot.addCurve(data.coords, desc.profile, legend=name, color=desc.color)
+
def clear(self):
"""Clear the window profile"""
plot = self.getPlot1D(init=False)
@@ -345,6 +361,8 @@ class ProfileWindow(qt.QMainWindow):
self._setRgbaProfile(data)
elif isinstance(data, core.CurveProfileData):
self._setCurveProfile(data)
+ elif isinstance(data, core.CurvesProfileData):
+ self._setCurvesProfile(data)
else:
raise TypeError("Unsupported type %s" % type(data))
@@ -358,10 +376,10 @@ class _ClearAction(qt.QAction):
def __init__(self, parent, profileManager):
super(_ClearAction, self).__init__(parent)
self.__profileManager = weakref.ref(profileManager)
- icon = icons.getQIcon('profile-clear')
+ icon = icons.getQIcon("profile-clear")
self.setIcon(icon)
- self.setText('Clear profile')
- self.setToolTip('Clear the profiles')
+ self.setText("Clear profile")
+ self.setToolTip("Clear the profiles")
self.setCheckable(False)
self.setEnabled(False)
self.triggered.connect(profileManager.clearProfile)
@@ -419,37 +437,47 @@ class _StoreLastParamBehavior(qt.QObject):
if previousRoi is roi:
return
if previousRoi is not None:
- previousRoi.sigProfilePropertyChanged.disconnect(self._profilePropertyChanged)
+ previousRoi.sigProfilePropertyChanged.disconnect(
+ self._profilePropertyChanged
+ )
self.__profileRoi = None if roi is None else weakref.ref(roi)
if roi is not None:
roi.sigProfilePropertyChanged.connect(self._profilePropertyChanged)
def _profilePropertyChanged(self):
- """Handle changes on the properties defining the profile ROI.
- """
+ """Handle changes on the properties defining the profile ROI."""
if self.__filter.locked():
return
roi = self.sender()
self.storeProperties(roi)
def storeProperties(self, roi):
- if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
+ if isinstance(
+ roi,
+ (rois._DefaultImageStackProfileRoiMixIn, rois.ProfileImageStackCrossROI),
+ ):
self.__properties["method"] = roi.getProfileMethod()
self.__properties["line-width"] = roi.getProfileLineWidth()
self.__properties["type"] = roi.getProfileType()
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultImageProfileRoiMixIn, rois.ProfileImageCrossROI)
+ ):
self.__properties["method"] = roi.getProfileMethod()
self.__properties["line-width"] = roi.getProfileLineWidth()
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultScatterProfileRoiMixIn, rois.ProfileScatterCrossROI)
+ ):
self.__properties["npoints"] = roi.getNPoints()
def restoreProperties(self, roi):
with self.__filter:
- if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
+ if isinstance(
+ roi,
+ (
+ rois._DefaultImageStackProfileRoiMixIn,
+ rois.ProfileImageStackCrossROI,
+ ),
+ ):
value = self.__properties.get("method", None)
if value is not None:
roi.setProfileMethod(value)
@@ -459,16 +487,18 @@ class _StoreLastParamBehavior(qt.QObject):
value = self.__properties.get("type", None)
if value is not None:
roi.setProfileType(value)
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultImageProfileRoiMixIn, rois.ProfileImageCrossROI)
+ ):
value = self.__properties.get("method", None)
if value is not None:
roi.setProfileMethod(value)
value = self.__properties.get("line-width", None)
if value is not None:
roi.setProfileLineWidth(value)
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
+ elif isinstance(
+ roi, (rois._DefaultScatterProfileRoiMixIn, rois.ProfileScatterCrossROI)
+ ):
value = self.__properties.get("npoints", None)
if value is not None:
roi.setNPoints(value)
@@ -481,12 +511,12 @@ class ProfileManager(qt.QObject):
:param plot: :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager`
on which to operate.
"""
+
def __init__(self, parent=None, plot=None, roiManager=None):
super(ProfileManager, self).__init__(parent)
assert isinstance(plot, PlotWidget)
- self._plotRef = weakref.ref(
- plot, WeakMethodProxy(self.__plotDestroyed))
+ self._plotRef = weakref.ref(plot, WeakMethodProxy(self.__plotDestroyed))
# Set-up interaction manager
if roiManager is None:
@@ -589,14 +619,16 @@ class ProfileManager(qt.QObject):
if hasattr(profileRoiClass, "ICON"):
action.setIcon(icons.getQIcon(profileRoiClass.ICON))
if hasattr(profileRoiClass, "NAME"):
+
def articulify(word):
"""Add an an/a article in the front of the word"""
- first = word[1] if word[0] == 'h' else word[0]
+ first = word[1] if word[0] == "h" else word[0]
if first in "aeiou":
return "an " + word
return "a " + word
- action.setText('Define %s' % articulify(profileRoiClass.NAME))
- action.setToolTip('Enables %s selection mode' % profileRoiClass.NAME)
+
+ action.setText("Define %s" % articulify(profileRoiClass.NAME))
+ action.setToolTip("Enables %s selection mode" % profileRoiClass.NAME)
action.setSingleShot(True)
return action
@@ -622,7 +654,7 @@ class ProfileManager(qt.QObject):
rois.ProfileImageLineROI,
rois.ProfileImageDirectedLineROI,
rois.ProfileImageCrossROI,
- ]
+ ]
return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
def createScatterActions(self, parent):
@@ -637,7 +669,7 @@ class ProfileManager(qt.QObject):
rois.ProfileScatterVerticalLineROI,
rois.ProfileScatterLineROI,
rois.ProfileScatterCrossROI,
- ]
+ ]
return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
def createScatterSliceActions(self, parent):
@@ -654,7 +686,7 @@ class ProfileManager(qt.QObject):
rois.ProfileScatterHorizontalSliceROI,
rois.ProfileScatterVerticalSliceROI,
rois.ProfileScatterCrossSliceROI,
- ]
+ ]
return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
def createImageStackActions(self, parent):
@@ -672,7 +704,7 @@ class ProfileManager(qt.QObject):
rois.ProfileImageStackVerticalLineROI,
rois.ProfileImageStackLineROI,
rois.ProfileImageStackCrossROI,
- ]
+ ]
return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
def createEditorAction(self, parent):
@@ -704,8 +736,7 @@ class ProfileManager(qt.QObject):
self.setPlotItem(item)
def setProfileWindowClass(self, profileWindowClass):
- """Set the class which will be instantiated to display profile result.
- """
+ """Set the class which will be instantiated to display profile result."""
self._profileWindowClass = profileWindowClass
def setActiveItemTracking(self, tracking):
@@ -797,7 +828,7 @@ class ProfileManager(qt.QObject):
roiManager.removeRoi(roi)
if not roiManager.isDrawing():
- # Clean the selected mode
+ # Clean the selected mode
roiManager.stop()
def hasPendingOperations(self):
@@ -808,8 +839,7 @@ class ProfileManager(qt.QObject):
return len(self.__reentrantResults) > 0 or len(self._pendingRunners) > 0
def requestUpdateAllProfile(self):
- """Request to update the profile of all the managed ROIs.
- """
+ """Request to update the profile of all the managed ROIs."""
for roi in self._rois:
self.requestUpdateProfile(roi)
@@ -867,7 +897,7 @@ class ProfileManager(qt.QObject):
if roi in self.__reentrantResults:
# Store the data to process it in the main loop
# And not a sub loop created by initProfileWindow
- # This also remove the duplicated requested
+ # This also remove the duplicated requested
self.__reentrantResults[roi] = profileData
return
@@ -917,7 +947,7 @@ class ProfileManager(qt.QObject):
:param ~silx.gui.plot.items.item.Item item: AN item
:rtype: qt.QColor
"""
- color = 'pink'
+ color = "pink"
if isinstance(item, items.ColormapMixIn):
colormap = item.getColormap()
name = colormap.getName()
@@ -947,12 +977,13 @@ class ProfileManager(qt.QObject):
roi.setColor(color)
def __itemChanged(self, changeType):
- """Handle item changes.
- """
- if changeType in (items.ItemChangedType.DATA,
- items.ItemChangedType.MASK,
- items.ItemChangedType.POSITION,
- items.ItemChangedType.SCALE):
+ """Handle item changes."""
+ if changeType in (
+ items.ItemChangedType.DATA,
+ items.ItemChangedType.MASK,
+ items.ItemChangedType.POSITION,
+ items.ItemChangedType.SCALE,
+ ):
self.requestUpdateAllProfile()
elif changeType == (items.ItemChangedType.COLORMAP):
self._updateRoiColors()
@@ -1039,7 +1070,7 @@ class ProfileManager(qt.QObject):
window = self.getPlotWidget().window()
winGeom = window.frameGeometry()
- if qt.BINDING in ("PySide2", "PyQt5"):
+ if qt.BINDING == "PyQt5":
qapp = qt.QApplication.instance()
desktop = qapp.desktop()
screenGeom = desktop.availableGeometry(window)
@@ -1069,7 +1100,6 @@ class ProfileManager(qt.QObject):
left = screenGeom.width() - profileGeom.width()
profileWindow.move(left, top)
-
def clearProfileWindow(self, profileWindow):
"""Called when a profile window is not anymore needed.
diff --git a/src/silx/gui/plot/tools/profile/rois.py b/src/silx/gui/plot/tools/profile/rois.py
index 9eef622..23f086a 100644
--- a/src/silx/gui/plot/tools/profile/rois.py
+++ b/src/silx/gui/plot/tools/profile/rois.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -76,13 +75,13 @@ def _lineProfileTitle(x0, y0, x1, y1):
:rtype: str
"""
if x0 == x1:
- title = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1)
+ title = "{xlabel} = %g; {ylabel} = [%g, %g]" % (x0, y0, y1)
elif y0 == y1:
- title = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1)
+ title = "{ylabel} = %g; {xlabel} = [%g, %g]" % (y0, x0, x1)
else:
m = (y1 - y0) / (x1 - x0)
b = y0 - m * x0
- title = '{ylabel} = %g * {xlabel} %+g' % (m, b)
+ title = "{ylabel} = %g * {xlabel} %+g" % (m, b)
return title
@@ -148,7 +147,8 @@ class _ImageProfileArea(items.Shape):
origin=origin,
scale=scale,
lineWidth=roi.getProfileLineWidth(),
- method="none")
+ method="none",
+ )
return area
@@ -215,8 +215,7 @@ class _SliceProfileArea(items.Shape):
class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
- """Provide common behavior for silx default image profile ROI.
- """
+ """Provide common behavior for silx default image profile ROI."""
ITEM_KIND = items.ImageBase
@@ -266,21 +265,21 @@ class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
def _getRoiInfo(self):
"""Wrapper to allow to reuse the previous Profile code.
-
+
It would be good to remove it at one point.
"""
if isinstance(self, roi_items.HorizontalLineROI):
- lineProjectionMode = 'X'
+ lineProjectionMode = "X"
y = self.getPosition()
roiStart = (0, y)
roiEnd = (1, y)
elif isinstance(self, roi_items.VerticalLineROI):
- lineProjectionMode = 'Y'
+ lineProjectionMode = "Y"
x = self.getPosition()
roiStart = (x, 0)
roiEnd = (x, 1)
elif isinstance(self, roi_items.LineROI):
- lineProjectionMode = 'D'
+ lineProjectionMode = "D"
roiStart, roiEnd = self.getEndPoints()
else:
assert False
@@ -295,15 +294,17 @@ class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
scale = item.getScale()
method = self.getProfileMethod()
lineWidth = self.getProfileLineWidth()
+ roiInfo = self._getRoiInfo()
def createProfile2(currentData):
coords, profile, _area, profileName, xLabel = core.createProfile(
- roiInfo=self._getRoiInfo(),
+ roiInfo=roiInfo,
currentData=currentData,
origin=origin,
scale=scale,
lineWidth=lineWidth,
- method=method)
+ method=method,
+ )
return coords, profile, profileName, xLabel
currentData = item.getValueData(copy=False)
@@ -349,61 +350,61 @@ class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
return data
-class ProfileImageHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultImageProfileRoiMixIn):
+class ProfileImageHorizontalLineROI(
+ roi_items.HorizontalLineROI, _DefaultImageProfileRoiMixIn
+):
"""ROI for an horizontal profile at a location of an image"""
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
+ ICON = "shape-horizontal"
+ NAME = "horizontal line profile"
def __init__(self, parent=None):
roi_items.HorizontalLineROI.__init__(self, parent=parent)
_DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultImageProfileRoiMixIn):
+class ProfileImageVerticalLineROI(
+ roi_items.VerticalLineROI, _DefaultImageProfileRoiMixIn
+):
"""ROI for a vertical profile at a location of an image"""
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
+ ICON = "shape-vertical"
+ NAME = "vertical line profile"
def __init__(self, parent=None):
roi_items.VerticalLineROI.__init__(self, parent=parent)
_DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageLineROI(roi_items.LineROI,
- _DefaultImageProfileRoiMixIn):
+class ProfileImageLineROI(roi_items.LineROI, _DefaultImageProfileRoiMixIn):
"""ROI for an image profile between 2 points.
The X profile of this ROI is the projecting into one of the x/y axes,
using its scale and its orientation.
"""
- ICON = 'shape-diagonal'
- NAME = 'line profile'
+ ICON = "shape-diagonal"
+ NAME = "line profile"
def __init__(self, parent=None):
roi_items.LineROI.__init__(self, parent=parent)
_DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageDirectedLineROI(roi_items.LineROI,
- _DefaultImageProfileRoiMixIn):
+class ProfileImageDirectedLineROI(roi_items.LineROI, _DefaultImageProfileRoiMixIn):
"""ROI for an image profile between 2 points.
The X profile of the line is displayed projected into the line itself,
using its scale and its orientation. It's the distance from the origin.
"""
- ICON = 'shape-diagonal-directed'
- NAME = 'directed line profile'
+ ICON = "shape-diagonal-directed"
+ NAME = "directed line profile"
def __init__(self, parent=None):
roi_items.LineROI.__init__(self, parent=parent)
_DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
- self._handleStart.setSymbol('o')
+ self._handleStart.setSymbol("o")
def computeProfile(self, item):
if not isinstance(item, items.ImageBase):
@@ -420,10 +421,11 @@ class ProfileImageDirectedLineROI(roi_items.LineROI,
roiInfo = self._getRoiInfo()
roiStart, roiEnd, _lineProjectionMode = roiInfo
- startPt = ((roiStart[1] - origin[1]) / scale[1],
- (roiStart[0] - origin[0]) / scale[0])
- endPt = ((roiEnd[1] - origin[1]) / scale[1],
- (roiEnd[0] - origin[0]) / scale[0])
+ startPt = (
+ (roiStart[1] - origin[1]) / scale[1],
+ (roiStart[0] - origin[0]) / scale[0],
+ )
+ endPt = ((roiEnd[1] - origin[1]) / scale[1], (roiEnd[0] - origin[0]) / scale[0])
if numpy.array_equal(startPt, endPt):
return None
@@ -433,14 +435,16 @@ class ProfileImageDirectedLineROI(roi_items.LineROI,
(startPt[0] - 0.5, startPt[1] - 0.5),
(endPt[0] - 0.5, endPt[1] - 0.5),
lineWidth,
- method=method)
+ method=method,
+ )
# Compute the line size
- lineSize = numpy.sqrt((roiEnd[1] - roiStart[1]) ** 2 +
- (roiEnd[0] - roiStart[0]) ** 2)
- coords = numpy.linspace(0, lineSize, len(profile),
- endpoint=True,
- dtype=numpy.float32)
+ lineSize = numpy.sqrt(
+ (roiEnd[1] - roiStart[1]) ** 2 + (roiEnd[0] - roiStart[0]) ** 2
+ )
+ coords = numpy.linspace(
+ 0, lineSize, len(profile), endpoint=True, dtype=numpy.float32
+ )
title = _lineProfileTitle(*roiStart, *roiEnd)
title = title + "; width = %d" % lineWidth
@@ -531,8 +535,7 @@ class _ProfileCrossROI(roi_items.HandleBasedROI, core.ProfileRoiMixIn):
def __updateLineProperty(self, event=None, checkVisibility=True):
if event == items.ItemChangedType.NAME:
self.__handleLabel.setText(self.getName())
- elif event in [items.ItemChangedType.COLOR,
- items.ItemChangedType.VISIBLE]:
+ elif event in [items.ItemChangedType.COLOR, items.ItemChangedType.VISIBLE]:
lines = []
if self.__vline:
lines.append(self.__vline)
@@ -659,8 +662,8 @@ class ProfileImageCrossROI(_ProfileCrossROI):
It is managed using 2 sub ROIs for vertical and horizontal.
"""
- ICON = 'shape-cross'
- NAME = 'cross profile'
+ ICON = "shape-cross"
+ NAME = "cross profile"
ITEM_KIND = items.ImageBase
def _createLines(self, parent):
@@ -693,8 +696,7 @@ class ProfileImageCrossROI(_ProfileCrossROI):
class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
- """Provide common behavior for silx default scatter profile ROI.
- """
+ """Provide common behavior for silx default scatter profile ROI."""
ITEM_KIND = items.Scatter
@@ -737,7 +739,7 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
:param float y1: Profile end point Y coord
:return: (points, values) profile data or None
"""
- future = scatter._getInterpolator()
+ future = scatter._getInterpolatorFuture()
try:
interpolator = future.result()
except CancelledError:
@@ -746,15 +748,14 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
return None # Cannot init an interpolator
nPoints = self.getNPoints()
- points = numpy.transpose((
- numpy.linspace(x0, x1, nPoints, endpoint=True),
- numpy.linspace(y0, y1, nPoints, endpoint=True)))
-
- values = interpolator(points)
+ x = numpy.linspace(x0, x1, nPoints, endpoint=True)
+ y = numpy.linspace(y0, y1, nPoints, endpoint=True)
+ values = interpolator(x, y)
if not numpy.any(numpy.isfinite(values)):
return None # Profile outside convex hull
+ points = numpy.transpose((x, y))
return points, values
def computeProfile(self, item):
@@ -779,7 +780,7 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
x0 = x1 = self.getPosition()
y0, y1 = plot.getYAxis().getLimits()
else:
- raise RuntimeError('Unsupported ROI for profile: {}'.format(self.__class__))
+ raise RuntimeError("Unsupported ROI for profile: {}".format(self.__class__))
if x1 < x0 or (x1 == x0 and y1 < y0):
# Invert points
@@ -793,13 +794,14 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
points = profile[0]
values = profile[1]
- if (numpy.abs(points[-1, 0] - points[0, 0]) >
- numpy.abs(points[-1, 1] - points[0, 1])):
+ if numpy.abs(points[-1, 0] - points[0, 0]) > numpy.abs(
+ points[-1, 1] - points[0, 1]
+ ):
xProfile = points[:, 0]
- xLabel = '{xlabel}'
+ xLabel = "{xlabel}"
else:
xProfile = points[:, 1]
- xLabel = '{ylabel}'
+ xLabel = "{ylabel}"
# Use the axis names from the original
profileManager = self.getProfileManager()
@@ -812,41 +814,42 @@ class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
profile=values,
title=title,
xLabel=xLabel,
- yLabel='Profile',
+ yLabel="Profile",
)
return data
-class ProfileScatterHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultScatterProfileRoiMixIn):
+class ProfileScatterHorizontalLineROI(
+ roi_items.HorizontalLineROI, _DefaultScatterProfileRoiMixIn
+):
"""ROI for an horizontal profile at a location of a scatter"""
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
+ ICON = "shape-horizontal"
+ NAME = "horizontal line profile"
def __init__(self, parent=None):
roi_items.HorizontalLineROI.__init__(self, parent=parent)
_DefaultScatterProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileScatterVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultScatterProfileRoiMixIn):
+class ProfileScatterVerticalLineROI(
+ roi_items.VerticalLineROI, _DefaultScatterProfileRoiMixIn
+):
"""ROI for an horizontal profile at a location of a scatter"""
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
+ ICON = "shape-vertical"
+ NAME = "vertical line profile"
def __init__(self, parent=None):
roi_items.VerticalLineROI.__init__(self, parent=parent)
_DefaultScatterProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileScatterLineROI(roi_items.LineROI,
- _DefaultScatterProfileRoiMixIn):
+class ProfileScatterLineROI(roi_items.LineROI, _DefaultScatterProfileRoiMixIn):
"""ROI for an horizontal profile at a location of a scatter"""
- ICON = 'shape-diagonal'
- NAME = 'line profile'
+ ICON = "shape-diagonal"
+ NAME = "line profile"
def __init__(self, parent=None):
roi_items.LineROI.__init__(self, parent=parent)
@@ -854,11 +857,10 @@ class ProfileScatterLineROI(roi_items.LineROI,
class ProfileScatterCrossROI(_ProfileCrossROI):
- """ROI to manage a cross of profiles for scatters.
- """
+ """ROI to manage a cross of profiles for scatters."""
- ICON = 'shape-cross'
- NAME = 'cross profile'
+ ICON = "shape-cross"
+ NAME = "cross profile"
ITEM_KIND = items.Scatter
def _createLines(self, parent):
@@ -910,7 +912,9 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
def _getSlice(self, item):
position = self.getPosition()
- bounds = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_BOUNDS)
+ bounds = item.getCurrentVisualizationParameter(
+ items.Scatter.VisualizationParameter.GRID_BOUNDS
+ )
if isinstance(self, roi_items.HorizontalLineROI):
axis = 1
elif isinstance(self, roi_items.VerticalLineROI):
@@ -921,21 +925,25 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
# ROI outside of the scatter bound
return None
- major_order = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_MAJOR_ORDER)
- assert major_order == 'row'
- max_grid_yy, max_grid_xx = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_SHAPE)
+ major_order = item.getCurrentVisualizationParameter(
+ items.Scatter.VisualizationParameter.GRID_MAJOR_ORDER
+ )
+ assert major_order == "row"
+ max_grid_yy, max_grid_xx = item.getCurrentVisualizationParameter(
+ items.Scatter.VisualizationParameter.GRID_SHAPE
+ )
xx, yy, _values, _xx_error, _yy_error = item.getData(copy=False)
if isinstance(self, roi_items.HorizontalLineROI):
axis = yy
max_grid_first = max_grid_yy
max_grid_second = max_grid_xx
- major_axis = major_order == 'column'
+ major_axis = major_order == "column"
elif isinstance(self, roi_items.VerticalLineROI):
axis = xx
max_grid_first = max_grid_xx
max_grid_second = max_grid_yy
- major_axis = major_order == 'row'
+ major_axis = major_order == "row"
else:
assert False
@@ -945,13 +953,19 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
if major_axis:
# slice in the middle of the scatter
- start = max_grid_second // 2 * max_grid_first
- vslice = axis[start:start + max_grid_second]
+ actual_size_grid_second = len(axis) // max_grid_first
+ start = actual_size_grid_second // 2 * max_grid_first
+ vslice = axis[start : start + max_grid_first]
+ if len(vslice) == 0:
+ return None
index = argnearest(vslice, position)
slicing = slice(index, None, max_grid_first)
else:
# slice in the middle of the scatter
- vslice = axis[max_grid_second // 2::max_grid_second]
+ actual_size_grid_second = len(axis) // max_grid_first
+ vslice = axis[actual_size_grid_second // 2 :: max_grid_second]
+ if len(vslice) == 0:
+ return None
index = argnearest(vslice, position)
start = index * max_grid_second
slicing = slice(start, start + max_grid_second)
@@ -994,28 +1008,30 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
return data
-class ProfileScatterHorizontalSliceROI(roi_items.HorizontalLineROI,
- _DefaultScatterProfileSliceRoiMixIn):
+class ProfileScatterHorizontalSliceROI(
+ roi_items.HorizontalLineROI, _DefaultScatterProfileSliceRoiMixIn
+):
"""ROI for an horizontal profile at a location of a scatter
using data slicing.
"""
- ICON = 'slice-horizontal'
- NAME = 'horizontal data slice profile'
+ ICON = "slice-horizontal"
+ NAME = "horizontal data slice profile"
def __init__(self, parent=None):
roi_items.HorizontalLineROI.__init__(self, parent=parent)
_DefaultScatterProfileSliceRoiMixIn.__init__(self, parent=parent)
-class ProfileScatterVerticalSliceROI(roi_items.VerticalLineROI,
- _DefaultScatterProfileSliceRoiMixIn):
+class ProfileScatterVerticalSliceROI(
+ roi_items.VerticalLineROI, _DefaultScatterProfileSliceRoiMixIn
+):
"""ROI for a vertical profile at a location of a scatter
using data slicing.
"""
- ICON = 'slice-vertical'
- NAME = 'vertical data slice profile'
+ ICON = "slice-vertical"
+ NAME = "vertical data slice profile"
def __init__(self, parent=None):
roi_items.VerticalLineROI.__init__(self, parent=parent)
@@ -1023,11 +1039,10 @@ class ProfileScatterVerticalSliceROI(roi_items.VerticalLineROI,
class ProfileScatterCrossSliceROI(_ProfileCrossROI):
- """ROI to manage a cross of slicing profiles on scatters.
- """
+ """ROI to manage a cross of slicing profiles on scatters."""
- ICON = 'slice-cross'
- NAME = 'cross data slice profile'
+ ICON = "slice-cross"
+ NAME = "cross data slice profile"
ITEM_KIND = items.Scatter
def _createLines(self, parent):
@@ -1037,7 +1052,6 @@ class ProfileScatterCrossSliceROI(_ProfileCrossROI):
class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn):
-
ITEM_KIND = items.ImageStack
def __init__(self, parent=None):
@@ -1068,65 +1082,71 @@ class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn):
assert kind == "2D"
+ currentData = numpy.array(item.getStackData(copy=False))
+ origin = item.getOrigin()
+ scale = item.getScale()
+ colormap = item.getColormap()
+ method = self.getProfileMethod()
+ roiInfo = self._getRoiInfo()
+
def createProfile2(currentData):
coords, profile, _area, profileName, xLabel = core.createProfile(
- roiInfo=self._getRoiInfo(),
+ roiInfo=roiInfo,
currentData=currentData,
origin=origin,
scale=scale,
lineWidth=self.getProfileLineWidth(),
- method=method)
+ method=method,
+ )
return coords, profile, profileName, xLabel
- currentData = numpy.array(item.getStackData(copy=False))
- origin = item.getOrigin()
- scale = item.getScale()
- colormap = item.getColormap()
- method = self.getProfileMethod()
-
coords, profile, profileName, xLabel = createProfile2(currentData)
+ profileManager = self.getProfileManager()
+ plot = profileManager.getPlotWidget()
+
data = core.ImageProfileData(
coords=coords,
profile=profile,
- title=profileName,
- xLabel=xLabel,
+ title=_relabelAxes(plot, profileName),
+ xLabel=_relabelAxes(plot, xLabel),
yLabel="Profile",
colormap=colormap,
)
return data
-class ProfileImageStackHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultImageStackProfileRoiMixIn):
+class ProfileImageStackHorizontalLineROI(
+ roi_items.HorizontalLineROI, _DefaultImageStackProfileRoiMixIn
+):
"""ROI for an horizontal profile at a location of a stack of images"""
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
+ ICON = "shape-horizontal"
+ NAME = "horizontal line profile"
def __init__(self, parent=None):
roi_items.HorizontalLineROI.__init__(self, parent=parent)
_DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageStackVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultImageStackProfileRoiMixIn):
+class ProfileImageStackVerticalLineROI(
+ roi_items.VerticalLineROI, _DefaultImageStackProfileRoiMixIn
+):
"""ROI for an vertical profile at a location of a stack of images"""
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
+ ICON = "shape-vertical"
+ NAME = "vertical line profile"
def __init__(self, parent=None):
roi_items.VerticalLineROI.__init__(self, parent=parent)
_DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent)
-class ProfileImageStackLineROI(roi_items.LineROI,
- _DefaultImageStackProfileRoiMixIn):
+class ProfileImageStackLineROI(roi_items.LineROI, _DefaultImageStackProfileRoiMixIn):
"""ROI for an vertical profile at a location of a stack of images"""
- ICON = 'shape-diagonal'
- NAME = 'line profile'
+ ICON = "shape-diagonal"
+ NAME = "line profile"
def __init__(self, parent=None):
roi_items.LineROI.__init__(self, parent=parent)
@@ -1136,8 +1156,8 @@ class ProfileImageStackLineROI(roi_items.LineROI,
class ProfileImageStackCrossROI(ProfileImageCrossROI):
"""ROI for an vertical profile at a location of a stack of images"""
- ICON = 'shape-cross'
- NAME = 'cross profile'
+ ICON = "shape-cross"
+ NAME = "cross profile"
ITEM_KIND = items.ImageStack
def _createLines(self, parent):
diff --git a/src/silx/gui/plot/tools/profile/toolbar.py b/src/silx/gui/plot/tools/profile/toolbar.py
index 4a9a195..d073717 100644
--- a/src/silx/gui/plot/tools/profile/toolbar.py
+++ b/src/silx/gui/plot/tools/profile/toolbar.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
@@ -45,10 +44,11 @@ _logger = logging.getLogger(__name__)
class ProfileToolBar(qt.QToolBar):
"""Tool bar to provide profile for a plot.
-
+
It is an helper class. For a dedicated application it would be better to
use an own tool bar in order in order have more flexibility.
"""
+
def __init__(self, parent=None, plot=None):
super(ProfileToolBar, self).__init__(parent=parent)
self.__scheme = None
diff --git a/src/silx/gui/plot/tools/roi.py b/src/silx/gui/plot/tools/roi.py
index e4be6a7..21b9409 100644
--- a/src/silx/gui/plot/tools/roi.py
+++ b/src/silx/gui/plot/tools/roi.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 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
@@ -35,6 +34,7 @@ import logging
import time
import weakref
import functools
+from typing import Optional
import numpy
@@ -43,6 +43,8 @@ from ...utils import blockSignals
from ...utils import LockReentrant
from .. import PlotWidget
from ..items import roi as roi_items
+from ..items import ItemChangedType
+from ..items.roi import RegionOfInterest
from ...colors import rgba
@@ -88,7 +90,7 @@ class CreateRoiModeAction(qt.QAction):
iconName = "add-shape-unknown"
if name is None:
name = roiClass.__name__
- text = 'Add %s' % name
+ text = "Add %s" % name
self.setIcon(icons.getQIcon(iconName))
self.setText(text)
self.setCheckable(True)
@@ -145,7 +147,9 @@ class CreateRoiModeAction(qt.QAction):
if roiManager is not None:
roiManager.sigInteractiveRoiCreated.disconnect(self.initRoi)
roiManager.sigInteractiveRoiFinalized.disconnect(self.__finalizeRoi)
- roiManager.sigInteractiveModeFinished.disconnect(self.__interactiveModeFinished)
+ roiManager.sigInteractiveModeFinished.disconnect(
+ self.__interactiveModeFinished
+ )
self.setChecked(False)
def initRoi(self, roi):
@@ -381,6 +385,7 @@ class RegionOfInterestManager(qt.QObject):
roi_items.VerticalLineROI,
roi_items.ArcROI,
roi_items.HorizontalRangeROI,
+ roi_items.BandROI,
)
def __init__(self, parent):
@@ -391,7 +396,8 @@ class RegionOfInterestManager(qt.QObject):
self._roiClass = None
self._source = None
- self._color = rgba('red')
+ self._lastHoveredMarkerLabel = None
+ self._color = rgba("red")
self._label = "__RegionOfInterestManager__%d" % id(self)
@@ -404,8 +410,7 @@ class RegionOfInterestManager(qt.QObject):
parent.sigPlotSignal.connect(self._plotSignals)
- parent.sigInteractiveModeChanged.connect(
- self._plotInteractiveModeChanged)
+ parent.sigInteractiveModeChanged.connect(self._plotInteractiveModeChanged)
parent.sigItemRemoved.connect(self._itemRemoved)
@@ -432,7 +437,7 @@ class RegionOfInterestManager(qt.QObject):
:raise ValueError: If kind is not supported
"""
if not issubclass(roiClass, roi_items.RegionOfInterest):
- raise ValueError('Unsupported ROI class %s' % roiClass)
+ raise ValueError("Unsupported ROI class %s" % roiClass)
action = self._modeActions.get(roiClass, None)
if action is None: # Lazy-loading
@@ -476,19 +481,21 @@ class RegionOfInterestManager(qt.QObject):
return # Should not happen
kind = roiClass.getFirstInteractionShape()
- if kind == 'point':
- if event['event'] == 'mouseClicked' and event['button'] == 'left':
- points = numpy.array([(event['x'], event['y'])],
- dtype=numpy.float64)
+ if kind == "point":
+ if event["event"] == "mouseClicked" and event["button"] == "left":
+ points = numpy.array([(event["x"], event["y"])], dtype=numpy.float64)
# Not an interactive creation
roi = self._createInteractiveRoi(roiClass, points=points)
roi.creationFinalized()
self.sigInteractiveRoiFinalized.emit(roi)
else: # other shapes
- if (event['event'] in ('drawingProgress', 'drawingFinished') and
- event['parameters']['label'] == self._label):
- points = numpy.array((event['xdata'], event['ydata']),
- dtype=numpy.float64).T
+ if (
+ event["event"] in ("drawingProgress", "drawingFinished")
+ and event["parameters"]["label"] == self._label
+ ):
+ points = numpy.array(
+ (event["xdata"], event["ydata"]), dtype=numpy.float64
+ ).T
if self._drawnROI is None: # Create new ROI
# NOTE: Set something before createRoi, so isDrawing is True
@@ -497,8 +504,8 @@ class RegionOfInterestManager(qt.QObject):
else:
self._drawnROI.setFirstShapePoints(points)
- if event['event'] == 'drawingFinished':
- if kind == 'polygon' and len(points) > 1:
+ if event["event"] == "drawingFinished":
+ if kind == "polygon" and len(points) > 1:
self._drawnROI.setFirstShapePoints(points[:-1])
roi = self._drawnROI
self._drawnROI = None # Stop drawing
@@ -521,7 +528,7 @@ class RegionOfInterestManager(qt.QObject):
return roi
return None
- def setCurrentRoi(self, roi):
+ def setCurrentRoi(self, roi: Optional[RegionOfInterest]):
"""Set the currently selected ROI, and emit a signal.
:param Union[RegionOfInterest,None] roi: The ROI to select
@@ -545,11 +552,8 @@ class RegionOfInterestManager(qt.QObject):
self._currentRoi.setHighlighted(True)
self.sigCurrentRoiChanged.emit(roi)
- def getCurrentRoi(self):
- """Returns the currently selected ROI, else None.
-
- :rtype: Union[RegionOfInterest,None]
- """
+ def getCurrentRoi(self) -> Optional[RegionOfInterest]:
+ """Returns the currently selected ROI, else None."""
return self._currentRoi
def _plotSignals(self, event):
@@ -568,6 +572,8 @@ class RegionOfInterestManager(qt.QObject):
plot = self.parent()
marker = plot._getMarkerAt(event["xpixel"], event["ypixel"])
roi = self.__getRoiFromMarker(marker)
+ elif event["event"] == "hover":
+ self._lastHoveredMarkerLabel = event["label"]
else:
return
@@ -585,7 +591,7 @@ class RegionOfInterestManager(qt.QObject):
else:
self.setCurrentRoi(None)
- def __updateMode(self, roi):
+ def __updateMode(self, roi: RegionOfInterest):
if isinstance(roi, roi_items.InteractionModeMixIn):
available = roi.availableInteractionModes()
mode = roi.getInteractionMode()
@@ -593,46 +599,50 @@ class RegionOfInterestManager(qt.QObject):
mode = available[(imode + 1) % len(available)]
roi.setInteractionMode(mode)
- def _feedContextMenu(self, menu):
+ def _feedContextMenu(self, menu: qt.QMenu):
"""Called when the default plot context menu is about to be displayed"""
roi = self.getCurrentRoi()
if roi is not None:
if roi.isEditable():
- # Filter by data position
- # FIXME: It would be better to use GUI coords for it
- plot = self.parent()
- pos = plot.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())
- data = plot.pixelToData(pos.x(), pos.y())
- if roi.contains(data):
- if isinstance(roi, roi_items.InteractionModeMixIn):
- self._contextMenuForInteractionMode(menu, roi)
-
- removeAction = qt.QAction(menu)
- removeAction.setText("Remove %s" % roi.getName())
- callback = functools.partial(self.removeRoi, roi)
- removeAction.triggered.connect(callback)
- menu.addAction(removeAction)
-
- def _contextMenuForInteractionMode(self, menu, roi):
- availableModes = roi.availableInteractionModes()
- currentMode = roi.getInteractionMode()
- submenu = qt.QMenu(menu)
- modeGroup = qt.QActionGroup(menu)
- modeGroup.setExclusive(True)
- for mode in availableModes:
- action = qt.QAction(menu)
- action.setText(mode.label)
- action.setToolTip(mode.description)
- action.setCheckable(True)
- if mode is currentMode:
- action.setChecked(True)
- else:
- callback = functools.partial(roi.setInteractionMode, mode)
- action.triggered.connect(callback)
- modeGroup.addAction(action)
- submenu.addAction(action)
- submenu.setTitle("%s interaction mode" % roi.getName())
- menu.addMenu(submenu)
+ if self._isMouseHoverRoi(roi):
+ roiMenu = self._createMenuForRoi(menu, roi)
+ menu.addMenu(roiMenu)
+
+ def _isMouseHoverRoi(self, roi: RegionOfInterest) -> bool:
+ """Check that the mouse hovers this roi"""
+ plot = self.parent()
+
+ if self._lastHoveredMarkerLabel is not None:
+ marker = plot._getMarker(self._lastHoveredMarkerLabel)
+ if marker is not None:
+ r = self.__getRoiFromMarker(marker)
+ if roi is r:
+ return True
+
+ # Filter by data position
+ # FIXME: It would be better to use GUI coords for it
+ pos = plot.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())
+ data = plot.pixelToData(pos.x(), pos.y())
+ return roi.contains(data)
+
+ def _createMenuForRoi(self, parent: qt.QWidget, roi: RegionOfInterest) -> qt.QMenu:
+ """Create a QMenu for the given RegionOfInterest"""
+ roiMenu = qt.QMenu(parent)
+ roiMenu.setTitle(roi.getName())
+
+ if isinstance(roi, roi_items.InteractionModeMixIn):
+ interactionMenu = roi.createMenuForInteractionMode(roiMenu)
+ roiMenu.addMenu(interactionMenu)
+
+ removeAction = qt.QAction(roiMenu)
+ removeAction.setText("Remove")
+ callback = functools.partial(self.removeRoi, roi)
+ removeAction.triggered.connect(callback)
+ roiMenu.addAction(removeAction)
+
+ roi.populateContextMenu(roiMenu)
+
+ return roiMenu
# RegionOfInterest API
@@ -654,8 +664,7 @@ class RegionOfInterestManager(qt.QObject):
"""
if self.getRois(): # Something to reset
for roi in self._rois:
- roi.sigRegionChanged.disconnect(
- self._regionOfInterestChanged)
+ roi.sigRegionChanged.disconnect(self._regionOfInterestChanged)
roi.setParent(None)
self._rois = []
self._roisUpdated()
@@ -715,8 +724,7 @@ class RegionOfInterestManager(qt.QObject):
"""
plot = self.parent()
if plot is None:
- raise RuntimeError(
- 'Cannot add ROI: PlotWidget no more available')
+ raise RuntimeError("Cannot add ROI: PlotWidget no more available")
roi.setParent(self)
@@ -739,11 +747,12 @@ class RegionOfInterestManager(qt.QObject):
:param roi_items.RegionOfInterest roi: The ROI to remove
:raise ValueError: When ROI does not belong to this object
"""
- if not (isinstance(roi, roi_items.RegionOfInterest) and
- roi.parent() is self and
- roi in self._rois):
- raise ValueError(
- 'RegionOfInterest does not belong to this instance')
+ if not (
+ isinstance(roi, roi_items.RegionOfInterest)
+ and roi.parent() is self
+ and roi in self._rois
+ ):
+ raise ValueError("RegionOfInterest does not belong to this instance")
roi.sigAboutToBeRemoved.emit()
self.sigRoiAboutToBeRemoved.emit(roi)
@@ -834,7 +843,7 @@ class RegionOfInterestManager(qt.QObject):
self.stop()
if not issubclass(roiClass, roi_items.RegionOfInterest):
- raise ValueError('Unsupported ROI class %s' % roiClass)
+ raise ValueError("Unsupported ROI class %s" % roiClass)
plot = self.parent()
if plot is None:
@@ -859,18 +868,20 @@ class RegionOfInterestManager(qt.QObject):
plot = self.parent()
firstInteractionShapeKind = roiClass.getFirstInteractionShape()
- if firstInteractionShapeKind == 'point':
- plot.setInteractiveMode(mode='select', source=self)
+ if firstInteractionShapeKind == "point":
+ plot.setInteractiveMode(mode="select", source=self)
else:
if roiClass.showFirstInteractionShape():
color = rgba(self.getColor())
else:
color = None
- plot.setInteractiveMode(mode='select-draw',
- source=self,
- shape=firstInteractionShapeKind,
- color=color,
- label=self._label)
+ plot.setInteractiveMode(
+ mode="draw",
+ source=self,
+ shape=firstInteractionShapeKind,
+ color=color,
+ label=self._label,
+ )
def __roiInteractiveModeEnded(self):
"""Handle end of ROI draw interactive mode"""
@@ -964,7 +975,7 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
super(InteractiveRegionOfInterestManager, self).__init__(parent)
self._maxROI = None
self.__timeoutEndTime = None
- self.__message = ''
+ self.__message = ""
self.__validationMode = self.ValidationMode.ENTER
self.__execClass = None
@@ -991,11 +1002,10 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
if max_ is not None:
max_ = int(max_)
if max_ <= 0:
- raise ValueError('Max limit must be strictly positive')
+ raise ValueError("Max limit must be strictly positive")
if len(self.getRois()) > max_:
- raise ValueError(
- 'Cannot set max limit: Already too many ROIs')
+ raise ValueError("Cannot set max limit: Already too many ROIs")
self._maxROI = max_
@@ -1013,19 +1023,19 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
class ValidationMode(enum.Enum):
"""Mode of validation to leave blocking :meth:`exec`"""
- AUTO = 'auto'
+ AUTO = "auto"
"""Automatically ends the interactive mode once
the user terminates the last ROI shape."""
- ENTER = 'enter'
+ ENTER = "enter"
"""Ends the interactive mode when the *Enter* key is pressed."""
- AUTO_ENTER = 'auto_enter'
+ AUTO_ENTER = "auto_enter"
"""Ends the interactive mode when reaching max ROIs or
when the *Enter* key is pressed.
"""
- NONE = 'none'
+ NONE = "none"
"""Do not provide the user a way to end the interactive mode.
The end of :meth:`exec` is done through :meth:`quit` or timeout.
@@ -1051,9 +1061,10 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
self.__validationMode = mode
if self.isExec():
- if (self.isMaxRois() and self.getValidationMode() in
- (self.ValidationMode.AUTO,
- self.ValidationMode.AUTO_ENTER)):
+ if self.isMaxRois() and self.getValidationMode() in (
+ self.ValidationMode.AUTO,
+ self.ValidationMode.AUTO_ENTER,
+ ):
self.quit()
self.__updateMessage()
@@ -1064,17 +1075,20 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
if event.type() == qt.QEvent.KeyPress:
key = event.key()
- if (key in (qt.Qt.Key_Return, qt.Qt.Key_Enter) and
- self.getValidationMode() in (
- self.ValidationMode.ENTER,
- self.ValidationMode.AUTO_ENTER)):
+ if key in (
+ qt.Qt.Key_Return,
+ qt.Qt.Key_Enter,
+ ) and self.getValidationMode() in (
+ self.ValidationMode.ENTER,
+ self.ValidationMode.AUTO_ENTER,
+ ):
# Stop on return key pressed
self.quit()
return True # Stop further handling of this keys
- if (key in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or (
- key == qt.Qt.Key_Z and
- event.modifiers() & qt.Qt.ControlModifier)):
+ if key in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or (
+ key == qt.Qt.Key_Z and event.modifiers() & qt.Qt.ControlModifier
+ ):
rois = self.getRois()
if rois: # Something to undo
self.removeRoi(rois[-1])
@@ -1096,8 +1110,7 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
return self.__message
else:
remaining = self.__timeoutEndTime - time.time()
- return self.__message + (' - %d seconds remaining' %
- max(1, int(remaining)))
+ return self.__message + (" - %d seconds remaining" % max(1, int(remaining)))
# Listen to ROI updates
@@ -1110,9 +1123,10 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
self.removeRoi(self.getRois()[-2])
self.__updateMessage()
- if (self.isMaxRois() and
- self.getValidationMode() in (self.ValidationMode.AUTO,
- self.ValidationMode.AUTO_ENTER)):
+ if self.isMaxRois() and self.getValidationMode() in (
+ self.ValidationMode.AUTO,
+ self.ValidationMode.AUTO_ENTER,
+ ):
self.quit()
def __aboutToBeRemoved(self, *args, **kwargs):
@@ -1131,10 +1145,10 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
def __updateMessage(self, nbrois=None):
"""Update message"""
if not self.isExec():
- message = 'Done'
+ message = "Done"
elif not self.isStarted():
- message = 'Use %s ROI edition mode' % self.__execClass
+ message = "Use %s ROI edition mode" % self.__execClass
else:
if nbrois is None:
@@ -1144,16 +1158,18 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
max_ = self.getMaxRois()
if max_ is None:
- message = 'Select %ss (%d selected)' % (name, nbrois)
+ message = "Select %ss (%d selected)" % (name, nbrois)
elif max_ <= 1:
- message = 'Select a %s' % name
+ message = "Select a %s" % name
else:
- message = 'Select %d/%d %ss' % (nbrois, max_, name)
+ message = "Select %d/%d %ss" % (nbrois, max_, name)
- if (self.getValidationMode() == self.ValidationMode.ENTER and
- self.isMaxRois()):
- message += ' - Press Enter to confirm'
+ if (
+ self.getValidationMode() == self.ValidationMode.ENTER
+ and self.isMaxRois()
+ ):
+ message += " - Press Enter to confirm"
if message != self.__message:
self.__message = message
@@ -1164,9 +1180,11 @@ class InteractiveRegionOfInterestManager(RegionOfInterestManager):
def __timeoutUpdate(self):
"""Handle update of timeout"""
- if (self.__timeoutEndTime is not None and
- (self.__timeoutEndTime - time.time()) > 0):
- self.sigMessageChanged.emit(self.getMessage())
+ if (
+ self.__timeoutEndTime is not None
+ and (self.__timeoutEndTime - time.time()) > 0
+ ):
+ self.sigMessageChanged.emit(self.getMessage())
else: # Stop interactive mode and message timer
timer = self.sender()
if timer is not None:
@@ -1234,7 +1252,7 @@ class _DeleteRegionOfInterestToolButton(qt.QToolButton):
def __init__(self, parent, roi):
super(_DeleteRegionOfInterestToolButton, self).__init__(parent)
- self.setIcon(icons.getQIcon('remove'))
+ self.setIcon(icons.getQIcon("remove"))
self.setToolTip("Remove this ROI")
self.__roiRef = roi if roi is None else weakref.ref(roi)
self.clicked.connect(self.__clicked)
@@ -1252,11 +1270,20 @@ class _DeleteRegionOfInterestToolButton(qt.QToolButton):
class RegionOfInterestTableWidget(qt.QTableWidget):
"""Widget displaying the ROIs of a :class:`RegionOfInterestManager`"""
+ # Columns indices of the different displayed information
+ (
+ _LABEL_VISIBLE_COL,
+ _EDITABLE_COL,
+ _KIND_COL,
+ _COORDINATES_COL,
+ _DELETE_COL,
+ ) = range(5)
+
def __init__(self, parent=None):
super(RegionOfInterestTableWidget, self).__init__(parent)
self._roiManagerRef = None
- headers = ['Label', 'Edit', 'Kind', 'Coordinates', '']
+ headers = ["Label", "Edit", "Kind", "Coordinates", ""]
self.setColumnCount(len(headers))
self.setHorizontalHeaderLabels(headers)
@@ -1278,21 +1305,17 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
self.itemChanged.connect(self.__itemChanged)
def __itemChanged(self, item):
- """Handle item updates"""
+ """Handle QTableWidget item updates"""
column = item.column()
- index = item.data(qt.Qt.UserRole)
-
- if index is not None:
- manager = self.getRegionOfInterestManager()
- roi = manager.getRois()[index]
- else:
+ roi = item.data(qt.Qt.UserRole)
+ if roi is None:
return
if column == 0:
# First collect information from item, then update ROI
- # Otherwise, this causes issues issues
+ # Otherwise, this causes issues
checked = item.checkState() == qt.Qt.Checked
- text= item.text()
+ text = item.text()
roi.setVisible(checked)
roi.setName(text)
elif column == 1:
@@ -1300,7 +1323,7 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
elif column in (2, 3, 4):
pass # TODO
else:
- logger.error('Unhandled column %d', column)
+ logger.error("Unhandled column %d", column)
def setRegionOfInterestManager(self, manager):
"""Set the :class:`RegionOfInterestManager` object to sync with
@@ -1312,7 +1335,13 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
previousManager = self.getRegionOfInterestManager()
if previousManager is not None:
- previousManager.sigRoiChanged.disconnect(self._sync)
+ previousManager.sigRoiAdded.disconnect(self.__roiAdded)
+ previousManager.sigRoiAboutToBeRemoved.disconnect(
+ self.__roiAboutToBeRemoved
+ )
+ for roi in previousManager.getRois():
+ self.__disconnectRoi(roi)
+
self.setRowCount(0)
self._roiManagerRef = weakref.ref(manager)
@@ -1320,7 +1349,10 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
self._sync()
if manager is not None:
- manager.sigRoiChanged.connect(self._sync)
+ for roi in manager.getRois():
+ self.__connectRoi(roi)
+ manager.sigRoiAdded.connect(self.__roiAdded)
+ manager.sigRoiAboutToBeRemoved.connect(self.__roiAboutToBeRemoved)
def _getReadableRoiDescription(self, roi):
"""Returns modelisation of a ROI as a readable sequence of values.
@@ -1345,6 +1377,75 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
logger.debug("Backtrace", exc_info=True)
return text
+ def __connectRoi(self, roi: RegionOfInterest):
+ """Start listening ROI signals"""
+ roi.sigItemChanged.connect(self.__roiItemChanged)
+ roi.sigRegionChanged.connect(self.__roiRegionChanged)
+
+ def __disconnectRoi(self, roi: RegionOfInterest):
+ """Stop listening ROI signals"""
+ roi.sigItemChanged.disconnect(self.__roiItemChanged)
+ roi.sigRegionChanged.disconnect(self.__roiRegionChanged)
+
+ def __getRoiRow(self, roi: RegionOfInterest) -> int:
+ """Returns row index of given region of interest
+
+ :raises ValueError: If region of interest is not in the list
+ """
+ manager = self.getRegionOfInterestManager()
+ if manager is None:
+ return
+ return manager.getRois().index(roi)
+
+ def __roiAdded(self, roi: RegionOfInterest):
+ """Handle new ROI added to the manager"""
+ self.__connectRoi(roi)
+ self._sync()
+
+ def __roiAboutToBeRemoved(self, roi: RegionOfInterest):
+ """Handle removing a ROI from the manager"""
+ self.__disconnectRoi(roi)
+ self.removeRow(self.__getRoiRow(roi))
+
+ def __roiItemChanged(self, event: ItemChangedType):
+ """Handle ROI sigItemChanged events"""
+ roi = self.sender()
+ if roi is None:
+ return
+
+ try:
+ row = self.__getRoiRow(roi)
+ except ValueError:
+ return
+
+ if event == ItemChangedType.VISIBLE:
+ item = self.item(row, self._LABEL_VISIBLE_COL)
+ item.setCheckState(qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked)
+ return
+
+ if event == ItemChangedType.NAME:
+ item = self.item(row, self._LABEL_VISIBLE_COL)
+ item.setText(roi.getName())
+ return
+
+ if event == ItemChangedType.EDITABLE:
+ item = self.item(row, self._EDITABLE_COL)
+ item.setCheckState(qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked)
+ return
+
+ def __roiRegionChanged(self):
+ """Handle change of ROI coordinates"""
+ roi = self.sender()
+ if roi is None:
+ return
+
+ item = self.item(self.__getRoiRow(roi), self._COORDINATES_COL)
+ if item is None:
+ return
+
+ text = self._getReadableRoiDescription(roi)
+ item.setText(text)
+
def _sync(self):
"""Update widget content according to ROI manger"""
manager = self.getRegionOfInterestManager()
@@ -1360,21 +1461,19 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
baseFlags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled
# Label and visible
- label = roi.getName()
- item = qt.QTableWidgetItem(label)
+ item = qt.QTableWidgetItem()
item.setFlags(baseFlags | qt.Qt.ItemIsEditable | qt.Qt.ItemIsUserCheckable)
- item.setData(qt.Qt.UserRole, index)
- item.setCheckState(
- qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked)
- self.setItem(index, 0, item)
+ item.setData(qt.Qt.UserRole, roi)
+ item.setText(roi.getName())
+ item.setCheckState(qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked)
+ self.setItem(index, self._LABEL_VISIBLE_COL, item)
# Editable
item = qt.QTableWidgetItem()
item.setFlags(baseFlags | qt.Qt.ItemIsUserCheckable)
- item.setData(qt.Qt.UserRole, index)
- item.setCheckState(
- qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked)
- self.setItem(index, 1, item)
+ item.setData(qt.Qt.UserRole, roi)
+ item.setCheckState(qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked)
+ self.setItem(index, self._EDITABLE_COL, item)
item.setTextAlignment(qt.Qt.AlignCenter)
item.setText(None)
@@ -1385,19 +1484,18 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
label = roi.__class__.__name__
item = qt.QTableWidgetItem(label.capitalize())
item.setFlags(baseFlags)
- self.setItem(index, 2, item)
+ self.setItem(index, self._KIND_COL, item)
+ # Coordinates
item = qt.QTableWidgetItem()
item.setFlags(baseFlags)
-
- # Coordinates
text = self._getReadableRoiDescription(roi)
item.setText(text)
- self.setItem(index, 3, item)
+ self.setItem(index, self._COORDINATES_COL, item)
# Delete
- delBtn = _DeleteRegionOfInterestToolButton(None, roi)
widget = qt.QWidget(self)
+ delBtn = _DeleteRegionOfInterestToolButton(widget, roi)
layout = qt.QHBoxLayout()
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(0)
@@ -1405,7 +1503,7 @@ class RegionOfInterestTableWidget(qt.QTableWidget):
layout.addStretch(1)
layout.addWidget(delBtn)
layout.addStretch(1)
- self.setCellWidget(index, 4, widget)
+ self.setCellWidget(index, self._DELETE_COL, widget)
def getRegionOfInterestManager(self):
"""Returns the :class:`RegionOfInterestManager` this widget supervise.
diff --git a/src/silx/gui/plot/tools/test/__init__.py b/src/silx/gui/plot/tools/test/__init__.py
index aa4a601..2e682d7 100644
--- a/src/silx/gui/plot/tools/test/__init__.py
+++ b/src/silx/gui/plot/tools/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py b/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py
index 37af10e..9f1a184 100644
--- a/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py
+++ b/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -27,8 +26,6 @@ __license__ = "MIT"
__date__ = "02/08/2018"
-import unittest
-
from silx.gui import qt
from silx.utils.testutils import ParametricTestCase
from silx.gui.utils.testutils import TestCaseQt
@@ -47,7 +44,7 @@ class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase):
self.legends.setPlotWidget(self.plot)
dock = qt.QDockWidget()
- dock.setWindowTitle('Curve Legends')
+ dock.setWindowTitle("Curve Legends")
dock.setWidget(self.legends)
self.plot.addTabbedDockWidget(dock)
@@ -69,9 +66,9 @@ class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase):
def testAddRemoveCurves(self):
"""Test CurveLegendsWidget while adding/removing curves"""
- self.plot.addCurve((0, 1), (1, 2), legend='a')
+ self.plot.addCurve((0, 1), (1, 2), legend="a")
self._assertNbLegends(1)
- self.plot.addCurve((0, 1), (2, 3), legend='b')
+ self.plot.addCurve((0, 1), (2, 3), legend="b")
self._assertNbLegends(2)
# Detached/attach
@@ -85,28 +82,35 @@ class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase):
self._assertNbLegends(0)
def testUpdateCurves(self):
- """Test CurveLegendsWidget while updating curves """
- self.plot.addCurve((0, 1), (1, 2), legend='a')
+ """Test CurveLegendsWidget while updating curves"""
+ self.plot.addCurve((0, 1), (1, 2), legend="a")
self._assertNbLegends(1)
- self.plot.addCurve((0, 1), (2, 3), legend='b')
+ self.plot.addCurve((0, 1), (2, 3), legend="b")
self._assertNbLegends(2)
# Activate curve
- self.plot.setActiveCurve('a')
+ self.plot.setActiveCurve("a")
self.qapp.processEvents()
- self.plot.setActiveCurve('b')
+ self.plot.setActiveCurve("b")
self.qapp.processEvents()
# Change curve style
- curve = self.plot.getCurve('a')
+ curve = self.plot.getCurve("a")
curve.setLineWidth(2)
- for linestyle in (':', '', '--', '-'):
+ for linestyle in (
+ ":",
+ "",
+ "--",
+ "-",
+ (0.0, (5.0, 5.0)),
+ (5.0, (10.0, 2.0, 2.0, 5.0)),
+ ):
with self.subTest(linestyle=linestyle):
curve.setLineStyle(linestyle)
self.qapp.processEvents()
self.qWait(1000)
- for symbol in ('o', 'd', '', 's'):
+ for symbol in ("o", "d", "", "s"):
with self.subTest(symbol=symbol):
curve.setSymbol(symbol)
self.qapp.processEvents()
diff --git a/src/silx/gui/plot/tools/test/testProfile.py b/src/silx/gui/plot/tools/test/testProfile.py
index 829f49e..61b95a6 100644
--- a/src/silx/gui/plot/tools/test/testProfile.py
+++ b/src/silx/gui/plot/tools/test/testProfile.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,14 +26,11 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-import unittest
import contextlib
import numpy
import logging
from silx.gui import qt
-from silx.utils import deprecation
-from silx.utils import testutils
from silx.gui.utils.testutils import TestCaseQt
from silx.utils.testutils import ParametricTestCase
@@ -50,7 +46,6 @@ _logger = logging.getLogger(__name__)
class TestRois(TestCaseQt):
-
def test_init(self):
"""Check that the constructor is not called twice"""
roi = rois.ProfileImageVerticalLineROI()
@@ -60,7 +55,6 @@ class TestRois(TestCaseQt):
class TestInteractions(TestCaseQt):
-
@contextlib.contextmanager
def defaultPlot(self):
try:
@@ -169,7 +163,7 @@ class TestInteractions(TestCaseQt):
self.assertEqual(len(profileRois), 3)
else:
self.assertEqual(len(profileRois), 1)
- # The first one should be the expected one
+ # The first one should be the expected one
roi = profileRois[0]
# Test that something was displayed
@@ -228,14 +222,14 @@ class TestInteractions(TestCaseQt):
if isinstance(editor, editors._NoProfileRoiEditor):
pass
elif isinstance(editor, editors._DefaultImageStackProfileRoiEditor):
- # GUI to ROI
+ # GUI to ROI
editor._lineWidth.setValue(2)
self.assertEqual(roi.getProfileLineWidth(), 2)
editor._methodsButton.setMethod("sum")
self.assertEqual(roi.getProfileMethod(), "sum")
editor._profileDim.setDimension(1)
self.assertEqual(roi.getProfileType(), "1D")
- # ROI to GUI
+ # ROI to GUI
roi.setProfileLineWidth(3)
self.assertEqual(editor._lineWidth.value(), 3)
roi.setProfileMethod("mean")
@@ -243,21 +237,21 @@ class TestInteractions(TestCaseQt):
roi.setProfileType("2D")
self.assertEqual(editor._profileDim.getDimension(), 2)
elif isinstance(editor, editors._DefaultImageProfileRoiEditor):
- # GUI to ROI
+ # GUI to ROI
editor._lineWidth.setValue(2)
self.assertEqual(roi.getProfileLineWidth(), 2)
editor._methodsButton.setMethod("sum")
self.assertEqual(roi.getProfileMethod(), "sum")
- # ROI to GUI
+ # ROI to GUI
roi.setProfileLineWidth(3)
self.assertEqual(editor._lineWidth.value(), 3)
roi.setProfileMethod("mean")
self.assertEqual(editor._methodsButton.getMethod(), "mean")
elif isinstance(editor, editors._DefaultScatterProfileRoiEditor):
- # GUI to ROI
+ # GUI to ROI
editor._nPoints.setValue(100)
self.assertEqual(roi.getNPoints(), 100)
- # ROI to GUI
+ # ROI to GUI
roi.setNPoints(200)
self.assertEqual(editor._nPoints.value(), 200)
else:
@@ -269,17 +263,32 @@ class TestInteractions(TestCaseQt):
(rois.ProfileImageVerticalLineROI, editors._DefaultImageProfileRoiEditor),
(rois.ProfileImageLineROI, editors._DefaultImageProfileRoiEditor),
(rois.ProfileImageCrossROI, editors._DefaultImageProfileRoiEditor),
- (rois.ProfileScatterHorizontalLineROI, editors._DefaultScatterProfileRoiEditor),
- (rois.ProfileScatterVerticalLineROI, editors._DefaultScatterProfileRoiEditor),
+ (
+ rois.ProfileScatterHorizontalLineROI,
+ editors._DefaultScatterProfileRoiEditor,
+ ),
+ (
+ rois.ProfileScatterVerticalLineROI,
+ editors._DefaultScatterProfileRoiEditor,
+ ),
(rois.ProfileScatterLineROI, editors._DefaultScatterProfileRoiEditor),
(rois.ProfileScatterCrossROI, editors._DefaultScatterProfileRoiEditor),
(rois.ProfileScatterHorizontalSliceROI, editors._NoProfileRoiEditor),
(rois.ProfileScatterVerticalSliceROI, editors._NoProfileRoiEditor),
(rois.ProfileScatterCrossSliceROI, editors._NoProfileRoiEditor),
- (rois.ProfileImageStackHorizontalLineROI, editors._DefaultImageStackProfileRoiEditor),
- (rois.ProfileImageStackVerticalLineROI, editors._DefaultImageStackProfileRoiEditor),
+ (
+ rois.ProfileImageStackHorizontalLineROI,
+ editors._DefaultImageStackProfileRoiEditor,
+ ),
+ (
+ rois.ProfileImageStackVerticalLineROI,
+ editors._DefaultImageStackProfileRoiEditor,
+ ),
(rois.ProfileImageStackLineROI, editors._DefaultImageStackProfileRoiEditor),
- (rois.ProfileImageStackCrossROI, editors._DefaultImageStackProfileRoiEditor),
+ (
+ rois.ProfileImageStackCrossROI,
+ editors._DefaultImageStackProfileRoiEditor,
+ ),
]
with self.defaultPlot() as plot:
profileManager = manager.ProfileManager(plot, plot)
@@ -289,7 +298,7 @@ class TestInteractions(TestCaseQt):
roi = roiClass()
roi._setProfileManager(profileManager)
try:
- # Force widget creation
+ # Force widget creation
menu = qt.QMenu(plot)
menu.addAction(editorAction)
widgets = editorAction.createdWidgets()
@@ -320,10 +329,8 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
self.mouseMove(self.plot) # Move to center
self.qapp.processEvents()
- deprecation.FORCE = True
def tearDown(self):
- deprecation.FORCE = False
self.qapp.processEvents()
profileManager = self.toolBar.getProfileManager()
profileManager.clearProfile()
@@ -339,7 +346,7 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
"""Test horizontal and vertical profile, without and with image"""
# Use Plot backend widget to submit mouse events
widget = self.plot.getWidgetHandle()
- for method in ('sum', 'mean'):
+ for method in ("sum", "mean"):
with self.subTest(method=method):
# 2 positions to use for mouse events
pos1 = widget.width() * 0.4, widget.height() * 0.4
@@ -354,8 +361,7 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1)
# with image
- self.plot.addImage(
- numpy.arange(100 * 100).reshape(100, -1))
+ self.plot.addImage(numpy.arange(100 * 100).reshape(100, -1))
self.mousePress(widget, qt.Qt.LeftButton, pos=pos1)
self.mouseMove(widget, pos=pos2)
self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2)
@@ -369,16 +375,14 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
if not manager.hasPendingOperations():
break
- @testutils.validate_logging(deprecation.depreclog.name, warning=4)
def testDiagonalProfile(self):
"""Test diagonal profile, without and with image"""
# Use Plot backend widget to submit mouse events
widget = self.plot.getWidgetHandle()
- self.plot.addImage(
- numpy.arange(100 * 100).reshape(100, -1))
+ self.plot.addImage(numpy.arange(100 * 100).reshape(100, -1))
- for method in ('sum', 'mean'):
+ for method in ("sum", "mean"):
with self.subTest(method=method):
# 2 positions to use for mouse events
pos1 = widget.width() * 0.4, widget.height() * 0.4
@@ -415,10 +419,12 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
if not manager.hasPendingOperations():
break
- curveItem = self.toolBar.getProfilePlot().getAllCurves()[0]
- if method == 'sum':
+ curveItem = (
+ roi.getProfileWindow().getCurrentPlotWidget().getAllCurves()[0]
+ )
+ if method == "sum":
self.assertTrue(curveItem.getData()[1].max() > 10000)
- elif method == 'mean':
+ elif method == "mean":
self.assertTrue(curveItem.getData()[1].max() < 10000)
# Remove the ROI so the profile window is also removed
@@ -427,77 +433,26 @@ class TestProfileToolBar(TestCaseQt, ParametricTestCase):
self.qWait(100)
-class TestDeprecatedProfileToolBar(TestCaseQt):
- """Tests old features of the ProfileToolBar widget."""
-
- def setUp(self):
- self.plot = None
- super(TestDeprecatedProfileToolBar, self).setUp()
-
- def tearDown(self):
- if self.plot is not None:
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.plot = None
- self.qWait()
-
- super(TestDeprecatedProfileToolBar, self).tearDown()
-
- @testutils.validate_logging(deprecation.depreclog.name, warning=2)
- def testCustomProfileWindow(self):
- from silx.gui.plot import ProfileMainWindow
-
- self.plot = PlotWindow()
- profileWindow = ProfileMainWindow.ProfileMainWindow(self.plot)
- toolBar = Profile.ProfileToolBar(parent=self.plot,
- plot=self.plot,
- profileWindow=profileWindow)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
- profileWindow.show()
- self.qWaitForWindowExposed(profileWindow)
- self.qapp.processEvents()
-
- self.plot.addImage(numpy.arange(10 * 10).reshape(10, -1))
- profile = rois.ProfileImageHorizontalLineROI()
- profile.setPosition(5)
- toolBar.getProfileManager().getRoiManager().addRoi(profile)
- toolBar.getProfileManager().getRoiManager().setCurrentRoi(profile)
-
- for _ in range(20):
- self.qWait(200)
- if not toolBar.getProfileManager().hasPendingOperations():
- break
-
- # There is a displayed profile
- self.assertIsNotNone(profileWindow.getProfile())
- self.assertIs(toolBar.getProfileMainWindow(), profileWindow)
-
- # There is nothing anymore but the window is still there
- toolBar.getProfileManager().clearProfile()
- self.qapp.processEvents()
- self.assertIsNone(profileWindow.getProfile())
-
-
class TestProfile3DToolBar(TestCaseQt):
- """Tests for Profile3DToolBar widget.
- """
+ """Tests for Profile3DToolBar widget."""
+
def setUp(self):
super(TestProfile3DToolBar, self).setUp()
self.plot = StackView()
self.plot.show()
self.qWaitForWindowExposed(self.plot)
- self.plot.setStack(numpy.array([
- [[0, 1, 2], [3, 4, 5]],
- [[6, 7, 8], [9, 10, 11]],
- [[12, 13, 14], [15, 16, 17]]
- ]))
- deprecation.FORCE = True
+ self.plot.setStack(
+ numpy.array(
+ [
+ [[0, 1, 2], [3, 4, 5]],
+ [[6, 7, 8], [9, 10, 11]],
+ [[12, 13, 14], [15, 16, 17]],
+ ]
+ )
+ )
def tearDown(self):
- deprecation.FORCE = False
profileManager = self.plot.getProfileToolbar().getProfileManager()
profileManager.clearProfile()
profileManager = None
@@ -507,7 +462,6 @@ class TestProfile3DToolBar(TestCaseQt):
super(TestProfile3DToolBar, self).tearDown()
- @testutils.validate_logging(deprecation.depreclog.name, warning=2)
def testMethodProfile2D(self):
"""Test that the profile can have a different method if we want to
compute then in 1D or in 2D"""
@@ -531,15 +485,13 @@ class TestProfile3DToolBar(TestCaseQt):
break
# check 2D 'mean' profile
- profilePlot = toolBar.getProfilePlot()
+ profilePlot = roi.getProfileWindow().getCurrentPlotWidget()
data = profilePlot.getAllImages()[0].getData()
expected = numpy.array([[1, 4], [7, 10], [13, 16]])
numpy.testing.assert_almost_equal(data, expected)
- @testutils.validate_logging(deprecation.depreclog.name, warning=2)
def testMethodSumLine(self):
- """Simple interaction test to make sure the sum is correctly computed
- """
+ """Simple interaction test to make sure the sum is correctly computed"""
toolBar = self.plot.getProfileToolbar()
toolBar.lineAction.trigger()
@@ -564,14 +516,13 @@ class TestProfile3DToolBar(TestCaseQt):
break
# check 2D 'sum' profile
- profilePlot = toolBar.getProfilePlot()
+ profilePlot = roi.getProfileWindow().getCurrentPlotWidget()
data = profilePlot.getAllImages()[0].getData()
expected = numpy.array([[3, 12], [21, 30], [39, 48]])
numpy.testing.assert_almost_equal(data, expected)
class TestGetProfilePlot(TestCaseQt):
-
def setUp(self):
self.plot = None
super(TestGetProfilePlot, self).setUp()
@@ -619,8 +570,7 @@ class TestGetProfilePlot(TestCaseQt):
self.plot.show()
self.qWaitForWindowExposed(self.plot)
- self.plot.setStack(numpy.array([[[0, 1], [2, 3]],
- [[4, 5], [6, 7]]]))
+ self.plot.setStack(numpy.array([[[0, 1], [2, 3]], [[4, 5], [6, 7]]]))
toolBar = self.plot.getProfileToolbar()
diff --git a/src/silx/gui/plot/tools/test/testROI.py b/src/silx/gui/plot/tools/test/testRoiCore.py
index 21697d1..e7f6d8a 100644
--- a/src/silx/gui/plot/tools/test/testROI.py
+++ b/src/silx/gui/plot/tools/test/testRoiCore.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
@@ -27,7 +26,6 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-import unittest
import numpy.testing
from silx.gui import qt
@@ -38,237 +36,6 @@ import silx.gui.plot.items.roi as roi_items
from silx.gui.plot.tools import roi
-class TestRoiItems(TestCaseQt):
-
- def testLine_geometry(self):
- item = roi_items.LineROI()
- startPoint = numpy.array([1, 2])
- endPoint = numpy.array([3, 4])
- item.setEndPoints(startPoint, endPoint)
- numpy.testing.assert_allclose(item.getEndPoints()[0], startPoint)
- numpy.testing.assert_allclose(item.getEndPoints()[1], endPoint)
-
- def testHLine_geometry(self):
- item = roi_items.HorizontalLineROI()
- item.setPosition(15)
- self.assertEqual(item.getPosition(), 15)
-
- def testVLine_geometry(self):
- item = roi_items.VerticalLineROI()
- item.setPosition(15)
- self.assertEqual(item.getPosition(), 15)
-
- def testPoint_geometry(self):
- point = numpy.array([1, 2])
- item = roi_items.PointROI()
- item.setPosition(point)
- numpy.testing.assert_allclose(item.getPosition(), point)
-
- def testRectangle_originGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- center = numpy.array([5, 10])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- numpy.testing.assert_allclose(item.getOrigin(), origin)
- numpy.testing.assert_allclose(item.getSize(), size)
- numpy.testing.assert_allclose(item.getCenter(), center)
-
- def testRectangle_centerGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- center = numpy.array([5, 10])
- item = roi_items.RectangleROI()
- item.setGeometry(center=center, size=size)
- numpy.testing.assert_allclose(item.getOrigin(), origin)
- numpy.testing.assert_allclose(item.getSize(), size)
- numpy.testing.assert_allclose(item.getCenter(), center)
-
- def testRectangle_setCenterGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- newCenter = numpy.array([0, 0])
- item.setCenter(newCenter)
- expectedOrigin = numpy.array([-5, -10])
- numpy.testing.assert_allclose(item.getOrigin(), expectedOrigin)
- numpy.testing.assert_allclose(item.getCenter(), newCenter)
- numpy.testing.assert_allclose(item.getSize(), size)
-
- def testRectangle_setOriginGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- newOrigin = numpy.array([10, 10])
- item.setOrigin(newOrigin)
- expectedCenter = numpy.array([15, 20])
- numpy.testing.assert_allclose(item.getOrigin(), newOrigin)
- numpy.testing.assert_allclose(item.getCenter(), expectedCenter)
- numpy.testing.assert_allclose(item.getSize(), size)
-
- def testCircle_geometry(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- numpy.testing.assert_allclose(item.getCenter(), center)
- numpy.testing.assert_allclose(item.getRadius(), radius)
-
- def testCircle_setCenter(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- newCenter = numpy.array([-10, 0])
- item.setCenter(newCenter)
- numpy.testing.assert_allclose(item.getCenter(), newCenter)
- numpy.testing.assert_allclose(item.getRadius(), radius)
-
- def testCircle_setRadius(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- newRadius = 5.1
- item.setRadius(newRadius)
- numpy.testing.assert_allclose(item.getCenter(), center)
- numpy.testing.assert_allclose(item.getRadius(), newRadius)
-
- def testCircle_contains(self):
- center = numpy.array([2, -1])
- radius = 1.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- self.assertTrue(item.contains([1, -1]))
- self.assertFalse(item.contains([0, 0]))
- self.assertTrue(item.contains([2, 0]))
- self.assertFalse(item.contains([3.01, -1]))
-
- def testEllipse_contains(self):
- center = numpy.array([-2, 0])
- item = roi_items.EllipseROI()
- item.setCenter(center)
- item.setOrientation(numpy.pi / 4.0)
- item.setMajorRadius(2)
- item.setMinorRadius(1)
- print(item.getMinorRadius(), item.getMajorRadius())
- self.assertFalse(item.contains([0, 0]))
- self.assertTrue(item.contains([-1, 1]))
- self.assertTrue(item.contains([-3, 0]))
- self.assertTrue(item.contains([-2, 0]))
- self.assertTrue(item.contains([-2, 1]))
- self.assertFalse(item.contains([-4, 1]))
-
- def testRectangle_isIn(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- self.assertTrue(item.contains(position=(0, 0)))
- self.assertTrue(item.contains(position=(2, 14)))
- self.assertFalse(item.contains(position=(14, 12)))
-
- def testPolygon_emptyGeometry(self):
- points = numpy.empty((0, 2))
- item = roi_items.PolygonROI()
- item.setPoints(points)
- numpy.testing.assert_allclose(item.getPoints(), points)
-
- def testPolygon_geometry(self):
- points = numpy.array([[10, 10], [12, 10], [50, 1]])
- item = roi_items.PolygonROI()
- item.setPoints(points)
- numpy.testing.assert_allclose(item.getPoints(), points)
-
- def testPolygon_isIn(self):
- points = numpy.array([[0, 0], [0, 10], [5, 10]])
- item = roi_items.PolygonROI()
- item.setPoints(points)
- self.assertTrue(item.contains((0, 0)))
- self.assertFalse(item.contains((6, 2)))
- self.assertFalse(item.contains((-2, 5)))
- self.assertFalse(item.contains((2, -1)))
- self.assertFalse(item.contains((8, 1)))
- self.assertTrue(item.contains((1, 8)))
-
- def testArc_getToSetGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]]))
- item.setGeometry(*item.getGeometry())
-
- def testArc_degenerated_point(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
-
- def testArc_degenerated_line(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
-
- def testArc_special_circle(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, 3 * numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0)
- self.assertTrue(item.isClosed())
-
- def testArc_special_donut(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0)
- self.assertTrue(item.isClosed())
-
- def testArc_clockwiseGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), startAngle)
- self.assertAlmostEqual(item.getEndAngle(), endAngle)
- self.assertAlmostEqual(item.isClosed(), False)
-
- def testArc_anticlockwiseGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, -numpy.pi * 0.5
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), startAngle)
- self.assertAlmostEqual(item.getEndAngle(), endAngle)
- self.assertAlmostEqual(item.isClosed(), False)
-
- def testHRange_geometry(self):
- item = roi_items.HorizontalRangeROI()
- vmin = 1
- vmax = 3
- item.setRange(vmin, vmax)
- self.assertAlmostEqual(item.getMin(), vmin)
- self.assertAlmostEqual(item.getMax(), vmax)
- self.assertAlmostEqual(item.getCenter(), 2)
-
-
class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
"""Tests for RegionOfInterestManager class"""
@@ -295,25 +62,44 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
def test(self):
"""Test ROI of different shapes"""
tests = ( # shape, points=[list of (x, y), list of (x, y)]
- (roi_items.PointROI, numpy.array(([(10., 15.)], [(20., 25.)]))),
- (roi_items.RectangleROI,
- numpy.array((((1., 10.), (11., 20.)),
- ((2., 3.), (12., 13.))))),
- (roi_items.PolygonROI,
- numpy.array((((0., 1.), (0., 10.), (10., 0.)),
- ((5., 6.), (5., 16.), (15., 6.))))),
- (roi_items.LineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.HorizontalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.VerticalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.HorizontalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
+ (roi_items.PointROI, numpy.array(([(10.0, 15.0)], [(20.0, 25.0)]))),
+ (
+ roi_items.RectangleROI,
+ numpy.array((((1.0, 10.0), (11.0, 20.0)), ((2.0, 3.0), (12.0, 13.0)))),
+ ),
+ (
+ roi_items.PolygonROI,
+ numpy.array(
+ (
+ ((0.0, 1.0), (0.0, 10.0), (10.0, 0.0)),
+ ((5.0, 6.0), (5.0, 16.0), (15.0, 6.0)),
+ )
+ ),
+ ),
+ (
+ roi_items.LineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.HorizontalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.VerticalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
+ (
+ roi_items.HorizontalLineROI,
+ numpy.array(
+ (((10.0, 20.0), (10.0, 30.0)), ((30.0, 40.0), (30.0, 50.0)))
+ ),
+ ),
)
for roiClass, points in tests:
@@ -448,7 +234,12 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
# Arc
item = roi_items.ArcROI()
center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
+ innerRadius, outerRadius, startAngle, endAngle = (
+ 1,
+ 100,
+ numpy.pi * 0.5,
+ numpy.pi,
+ )
item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
rois.append(item)
# Horizontal Range
@@ -488,12 +279,20 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
manager.removeRoi(item1)
self.assertIs(manager.getCurrentRoi(), None)
+ def testInitROIWithParent(self):
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.PointROI(manager)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+ manager.removeRoi(item)
+ self.qapp.processEvents()
+
def testMaxROI(self):
"""Test Max ROI"""
- origin1 = numpy.array([1., 10.])
- size1 = numpy.array([10., 10.])
- origin2 = numpy.array([2., 3.])
- size2 = numpy.array([10., 10.])
+ origin1 = numpy.array([1.0, 10.0])
+ size1 = numpy.array([10.0, 10.0])
+ origin2 = numpy.array([2.0, 3.0])
+ size2 = numpy.array([10.0, 10.0])
manager = roi.InteractiveRegionOfInterestManager(self.plot)
self.roiTableWidget.setRegionOfInterestManager(manager)
@@ -577,21 +376,22 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
manager.addRoi(item)
self.qapp.processEvents()
- # Drag the center
+ # Drag the center
widget = self.plot.getWidgetHandle()
mx, my = self.plot.dataToPixel(*center)
self.mouseMove(widget, pos=(mx, my))
self.mousePress(widget, qt.Qt.LeftButton, pos=(mx, my))
- self.mouseMove(widget, pos=(mx, my+25))
- self.mouseMove(widget, pos=(mx, my+50))
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my+50))
+ self.mouseMove(widget, pos=(mx, my + 25))
+ self.mouseMove(widget, pos=(mx, my + 50))
+ self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my + 50))
result = numpy.array(item.getEndPoints())
# x location is still the same
numpy.testing.assert_allclose(points[:, 0], result[:, 0], atol=0.5)
# size is still the same
- numpy.testing.assert_allclose(points[1] - points[0],
- result[1] - result[0], atol=0.5)
+ numpy.testing.assert_allclose(
+ points[1] - points[0], result[1] - result[0], atol=0.5
+ )
# But Y is not the same
self.assertNotEqual(points[0, 1], result[0, 1])
self.assertNotEqual(points[1, 1], result[1, 1])
@@ -662,7 +462,7 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
self.assertIs(item.getInteractionMode(), roi_items.ArcROI.ThreePointMode)
self.qWait(500)
- # Click on the center
+ # Click on the center
widget = self.plot.getWidgetHandle()
mx, my = self.plot.dataToPixel(*center)
@@ -680,3 +480,56 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
manager.clear()
self.qapp.processEvents()
+
+ def testBandRoiSwitchMode(self):
+ """Make sure we can switch mode by clicking on the ROI"""
+ xlimit = self.plot.getXAxis().getLimits()
+ ylimit = self.plot.getYAxis().getLimits()
+ xcenter = 0.5 * (xlimit[0] + xlimit[1])
+ ycenter = 0.5 * (ylimit[0] + ylimit[1])
+
+ # Create the line
+ manager = roi.RegionOfInterestManager(self.plot)
+ item = roi_items.BandROI()
+ item.setGeometry(
+ (xlimit[0], ycenter),
+ (xlimit[1], ycenter),
+ 20,
+ )
+ item.setEditable(True)
+ item.setSelectable(True)
+ manager.addRoi(item)
+ self.qapp.processEvents()
+
+ # Initial state
+ assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
+ self.qWait(500)
+
+ # Click on the center
+ widget = self.plot.getWidgetHandle()
+ mx, my = self.plot.dataToPixel(xcenter, ycenter)
+
+ # Select the ROI
+ self.mouseMove(widget, pos=(mx, my))
+ self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.qWait(500)
+ assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
+
+ # Change the mode
+ self.mouseMove(widget, pos=(mx, my))
+ self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.qWait(500)
+ assert item.getInteractionMode() is roi_items.BandROI.UnboundedMode
+
+ # Set available modes that exclude the current one
+ item.setAvailableInteractionModes([roi_items.BandROI.BoundedMode])
+ assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
+
+ # Clicking does not change the mode since there is only one
+ self.mouseMove(widget, pos=(mx, my))
+ self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
+ self.qWait(500)
+ assert item.getInteractionMode() is roi_items.BandROI.BoundedMode
+
+ manager.clear()
+ self.qapp.processEvents()
diff --git a/src/silx/gui/plot/tools/test/testRoiItems.py b/src/silx/gui/plot/tools/test/testRoiItems.py
new file mode 100644
index 0000000..9bd9690
--- /dev/null
+++ b/src/silx/gui/plot/tools/test/testRoiItems.py
@@ -0,0 +1,313 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "28/06/2018"
+
+
+import pytest
+import numpy.testing
+
+import silx.gui.plot.items.roi as roi_items
+
+
+def testLine_geometry(qapp):
+ item = roi_items.LineROI()
+ startPoint = numpy.array([1, 2])
+ endPoint = numpy.array([3, 4])
+ item.setEndPoints(startPoint, endPoint)
+ numpy.testing.assert_allclose(item.getEndPoints()[0], startPoint)
+ numpy.testing.assert_allclose(item.getEndPoints()[1], endPoint)
+
+
+def testHLine_geometry(qapp):
+ item = roi_items.HorizontalLineROI()
+ item.setPosition(15)
+ assert item.getPosition() == 15
+
+
+def testVLine_geometry(qapp):
+ item = roi_items.VerticalLineROI()
+ item.setPosition(15)
+ assert item.getPosition() == 15
+
+
+def testPoint_geometry(qapp):
+ point = numpy.array([1, 2])
+ item = roi_items.PointROI()
+ item.setPosition(point)
+ numpy.testing.assert_allclose(item.getPosition(), point)
+
+
+def testRectangle_originGeometry(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ center = numpy.array([5, 10])
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin, size=size)
+ numpy.testing.assert_allclose(item.getOrigin(), origin)
+ numpy.testing.assert_allclose(item.getSize(), size)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+
+
+def testRectangle_centerGeometry(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ center = numpy.array([5, 10])
+ item = roi_items.RectangleROI()
+ item.setGeometry(center=center, size=size)
+ numpy.testing.assert_allclose(item.getOrigin(), origin)
+ numpy.testing.assert_allclose(item.getSize(), size)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+
+
+def testRectangle_setCenterGeometry(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin, size=size)
+ newCenter = numpy.array([0, 0])
+ item.setCenter(newCenter)
+ expectedOrigin = numpy.array([-5, -10])
+ numpy.testing.assert_allclose(item.getOrigin(), expectedOrigin)
+ numpy.testing.assert_allclose(item.getCenter(), newCenter)
+ numpy.testing.assert_allclose(item.getSize(), size)
+
+
+def testRectangle_setOriginGeometry(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin, size=size)
+ newOrigin = numpy.array([10, 10])
+ item.setOrigin(newOrigin)
+ expectedCenter = numpy.array([15, 20])
+ numpy.testing.assert_allclose(item.getOrigin(), newOrigin)
+ numpy.testing.assert_allclose(item.getCenter(), expectedCenter)
+ numpy.testing.assert_allclose(item.getSize(), size)
+
+
+def testCircle_geometry(qapp):
+ center = numpy.array([0, 0])
+ radius = 10.0
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ numpy.testing.assert_allclose(item.getRadius(), radius)
+
+
+def testCircle_setCenter(qapp):
+ center = numpy.array([0, 0])
+ radius = 10.0
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ newCenter = numpy.array([-10, 0])
+ item.setCenter(newCenter)
+ numpy.testing.assert_allclose(item.getCenter(), newCenter)
+ numpy.testing.assert_allclose(item.getRadius(), radius)
+
+
+def testCircle_setRadius(qapp):
+ center = numpy.array([0, 0])
+ radius = 10.0
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ newRadius = 5.1
+ item.setRadius(newRadius)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ numpy.testing.assert_allclose(item.getRadius(), newRadius)
+
+
+def testCircle_contains(qapp):
+ center = numpy.array([2, -1])
+ radius = 1.0
+ item = roi_items.CircleROI()
+ item.setGeometry(center=center, radius=radius)
+ assert item.contains([1, -1])
+ assert not item.contains([0, 0])
+ assert item.contains([2, 0])
+ assert not item.contains([3.01, -1])
+
+
+def testEllipse_contains(qapp):
+ center = numpy.array([-2, 0])
+ item = roi_items.EllipseROI()
+ item.setCenter(center)
+ item.setOrientation(numpy.pi / 4.0)
+ item.setMajorRadius(2)
+ item.setMinorRadius(1)
+ print(item.getMinorRadius(), item.getMajorRadius())
+ assert not item.contains([0, 0])
+ assert item.contains([-1, 1])
+ assert item.contains([-3, 0])
+ assert item.contains([-2, 0])
+ assert item.contains([-2, 1])
+ assert not item.contains([-4, 1])
+
+
+def testRectangle_isIn(qapp):
+ origin = numpy.array([0, 0])
+ size = numpy.array([10, 20])
+ item = roi_items.RectangleROI()
+ item.setGeometry(origin=origin, size=size)
+ assert item.contains(position=(0, 0))
+ assert item.contains(position=(2, 14))
+ assert not item.contains(position=(14, 12))
+
+
+def testPolygon_emptyGeometry(qapp):
+ points = numpy.empty((0, 2))
+ item = roi_items.PolygonROI()
+ item.setPoints(points)
+ numpy.testing.assert_allclose(item.getPoints(), points)
+
+
+def testPolygon_geometry(qapp):
+ points = numpy.array([[10, 10], [12, 10], [50, 1]])
+ item = roi_items.PolygonROI()
+ item.setPoints(points)
+ numpy.testing.assert_allclose(item.getPoints(), points)
+
+
+def testPolygon_isIn(qapp):
+ points = numpy.array([[0, 0], [0, 10], [5, 10]])
+ item = roi_items.PolygonROI()
+ item.setPoints(points)
+ assert item.contains((0, 0))
+ assert not item.contains((6, 2))
+ assert not item.contains((-2, 5))
+ assert not item.contains((2, -1))
+ assert not item.contains((8, 1))
+ assert item.contains((1, 8))
+
+
+def testArc_getToSetGeometry(qapp):
+ """Test that we can use getGeometry as input to setGeometry"""
+ item = roi_items.ArcROI()
+ item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]]))
+ item.setGeometry(*item.getGeometry())
+
+
+def testArc_degenerated_point(qapp):
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+
+
+def testArc_degenerated_line(qapp):
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+
+
+def testArc_special_circle(qapp):
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, 3 * numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ assert item.getInnerRadius() == pytest.approx(innerRadius)
+ assert item.getOuterRadius() == pytest.approx(outerRadius)
+ assert item.getStartAngle() == pytest.approx(item.getEndAngle() - numpy.pi * 2.0)
+ assert item.isClosed()
+
+
+def testArc_special_donut(qapp):
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ assert item.getInnerRadius() == pytest.approx(innerRadius)
+ assert item.getOuterRadius() == pytest.approx(outerRadius)
+ assert item.getStartAngle() == pytest.approx(item.getEndAngle() - numpy.pi * 2.0)
+ assert item.isClosed()
+
+
+def testArc_clockwiseGeometry(qapp):
+ """Test that we can use getGeometry as input to setGeometry"""
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ assert item.getInnerRadius() == pytest.approx(innerRadius)
+ assert item.getOuterRadius() == pytest.approx(outerRadius)
+ assert item.getStartAngle() == pytest.approx(startAngle)
+ assert item.getEndAngle() == pytest.approx(endAngle)
+ assert not item.isClosed()
+
+
+def testArc_anticlockwiseGeometry(qapp):
+ """Test that we can use getGeometry as input to setGeometry"""
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = (
+ 1,
+ 100,
+ numpy.pi * 0.5,
+ -numpy.pi * 0.5,
+ )
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ numpy.testing.assert_allclose(item.getCenter(), center)
+ assert item.getInnerRadius() == pytest.approx(innerRadius)
+ assert item.getOuterRadius() == pytest.approx(outerRadius)
+ assert item.getStartAngle() == pytest.approx(startAngle)
+ assert item.getEndAngle() == pytest.approx(endAngle)
+ assert not item.isClosed()
+
+
+def testArc_position(qapp):
+ """Test validity of getPosition"""
+ item = roi_items.ArcROI()
+ center = numpy.array([10, 20])
+ innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
+ item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
+ assert item.getPosition(roi_items.ArcROI.Role.START) == pytest.approx((10.0, 70.5))
+ assert item.getPosition(roi_items.ArcROI.Role.STOP) == pytest.approx((-40.5, 20.0))
+ assert item.getPosition(roi_items.ArcROI.Role.MIDDLE) == pytest.approx(
+ (-25.71, 55.71), abs=0.1
+ )
+ assert item.getPosition(roi_items.ArcROI.Role.CENTER) == pytest.approx(
+ (10.0, 20), abs=0.1
+ )
+
+
+def testHRange_geometry(qapp):
+ item = roi_items.HorizontalRangeROI()
+ vmin = 1
+ vmax = 3
+ item.setRange(vmin, vmax)
+ assert item.getMin() == pytest.approx(vmin)
+ assert item.getMax() == pytest.approx(vmax)
+ assert item.getCenter() == pytest.approx(2)
+
+
+def testBand_getToSetGeometry(qapp):
+ """Test that we can use getGeometry as input to setGeometry"""
+ item = roi_items.BandROI()
+ item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]]))
+ item.setGeometry(*item.getGeometry())
diff --git a/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py b/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py
index 582a276..29c9ad0 100644
--- a/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py
+++ b/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -27,7 +26,6 @@ __license__ = "MIT"
__date__ = "28/06/2018"
-import unittest
import numpy
from silx.gui import qt
@@ -68,8 +66,9 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
# Add a scatter plot
self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
+ x=(0.0, 1.0, 1.0, 0.0), y=(0.0, 0.0, 1.0, 1.0), value=(0.0, 1.0, 2.0, 3.0)
+ )
+ self.plot.resetZoom(dataMargins=(0.1, 0.1, 0.1, 0.1))
self.qapp.processEvents()
# Set a ROI profile
@@ -108,8 +107,9 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
# Add a scatter plot
self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
+ x=(0.0, 1.0, 1.0, 0.0), y=(0.0, 0.0, 1.0, 1.0), value=(0.0, 1.0, 2.0, 3.0)
+ )
+ self.plot.resetZoom(dataMargins=(0.1, 0.1, 0.1, 0.1))
self.qapp.processEvents()
# Set a ROI profile
@@ -161,13 +161,14 @@ class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
# Add a scatter plot
self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
+ x=(0.0, 1.0, 1.0, 0.0), y=(0.0, 0.0, 1.0, 1.0), value=(0.0, 1.0, 2.0, 3.0)
+ )
+ self.plot.resetZoom(dataMargins=(0.1, 0.1, 0.1, 0.1))
self.qapp.processEvents()
# Set a ROI profile
roi = rois.ProfileScatterLineROI()
- roi.setEndPoints(numpy.array([0., 0.]), numpy.array([1., 1.]))
+ roi.setEndPoints(numpy.array([0.0, 0.0]), numpy.array([1.0, 1.0]))
roi.setNPoints(8)
roiManager.addRoi(roi)
diff --git a/src/silx/gui/plot/tools/test/testTools.py b/src/silx/gui/plot/tools/test/testTools.py
index 846f641..1212ead 100644
--- a/src/silx/gui/plot/tools/test/testTools.py
+++ b/src/silx/gui/plot/tools/test/testTools.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -30,11 +29,9 @@ __date__ = "02/03/2018"
import functools
-import unittest
import numpy
from silx.utils.testutils import LoggingValidator
-from silx.gui.utils.testutils import qWaitForWindowExposedAndActivate
from silx.gui import qt
from silx.gui.plot import PlotWindow
from silx.gui.plot import tools
@@ -83,28 +80,28 @@ class TestPositionInfo(PlotWidgetTestCase):
def testDefaultConverters(self):
"""Test PositionInfo with default converters"""
positionWidget = tools.PositionInfo(plot=self.plot)
- self._test(positionWidget, ('X', 'Y'))
+ self._test(positionWidget, ("X", "Y"))
def testCustomConverters(self):
"""Test PositionInfo with custom converters"""
converters = [
- ('Coords', lambda x, y: (int(x), int(y))),
- ('Radius', lambda x, y: numpy.sqrt(x * x + y * y)),
- ('Angle', lambda x, y: numpy.degrees(numpy.arctan2(y, x)))
+ ("Coords", lambda x, y: (int(x), int(y))),
+ ("Radius", lambda x, y: numpy.sqrt(x * x + y * y)),
+ ("Angle", lambda x, y: numpy.degrees(numpy.arctan2(y, x))),
]
- positionWidget = tools.PositionInfo(plot=self.plot,
- converters=converters)
- self._test(positionWidget, ('Coords', 'Radius', 'Angle'))
+ positionWidget = tools.PositionInfo(plot=self.plot, converters=converters)
+ self._test(positionWidget, ("Coords", "Radius", "Angle"))
def testFailingConverters(self):
"""Test PositionInfo with failing custom converters"""
+
def raiseException(x, y):
raise RuntimeError()
positionWidget = tools.PositionInfo(
- plot=self.plot,
- converters=[('Exception', raiseException)])
- self._test(positionWidget, ['Exception'], error=2)
+ plot=self.plot, converters=[("Exception", raiseException)]
+ )
+ self._test(positionWidget, ["Exception"], error=2)
def testUpdate(self):
"""Test :meth:`PositionInfo.updateInfo`"""
@@ -116,7 +113,8 @@ class TestPositionInfo(PlotWidgetTestCase):
positionWidget = tools.PositionInfo(
plot=self.plot,
- converters=[('Call count', functools.partial(update, calls))])
+ converters=[("Call count", functools.partial(update, calls))],
+ )
positionWidget.updateInfo()
self.assertEqual(len(calls), 1)
@@ -126,10 +124,12 @@ class TestPlotToolsToolbars(PlotWidgetTestCase):
"""Tests toolbars from silx.gui.plot.tools"""
def test(self):
- """"Add all toolbars"""
- for tbClass in (tools.InteractiveModeToolBar,
- tools.ImageToolBar,
- tools.CurveToolBar,
- tools.OutputToolBar):
+ """ "Add all toolbars"""
+ for tbClass in (
+ tools.InteractiveModeToolBar,
+ tools.ImageToolBar,
+ tools.CurveToolBar,
+ tools.OutputToolBar,
+ ):
tb = tbClass(parent=self.plot, plot=self.plot)
self.plot.addToolBar(tb)
diff --git a/src/silx/gui/plot/tools/toolbars.py b/src/silx/gui/plot/tools/toolbars.py
index 3df7d06..7f38f1c 100644
--- a/src/silx/gui/plot/tools/toolbars.py
+++ b/src/silx/gui/plot/tools/toolbars.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,7 +33,6 @@ from ... import qt
from .. import actions
from ..PlotWidget import PlotWidget
from .. import PlotToolButtons
-from ....utils.deprecation import deprecated
class InteractiveModeToolBar(qt.QToolBar):
@@ -45,17 +43,15 @@ class InteractiveModeToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Plot Interaction'):
+ def __init__(self, parent=None, plot=None, title="Plot Interaction"):
super(InteractiveModeToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
- self._zoomModeAction = actions.mode.ZoomModeAction(
- parent=self, plot=plot)
+ self._zoomModeAction = actions.mode.ZoomModeAction(parent=self, plot=plot)
self.addAction(self._zoomModeAction)
- self._panModeAction = actions.mode.PanModeAction(
- parent=self, plot=plot)
+ self._panModeAction = actions.mode.PanModeAction(parent=self, plot=plot)
self.addAction(self._panModeAction)
def getZoomModeAction(self):
@@ -81,7 +77,7 @@ class OutputToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Plot Output'):
+ def __init__(self, parent=None, plot=None, title="Plot Output"):
super(OutputToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
@@ -125,25 +121,25 @@ class ImageToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Image'):
+ def __init__(self, parent=None, plot=None, title="Image"):
super(ImageToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
+ self._resetZoomAction = actions.control.ResetZoomAction(parent=self, plot=plot)
self.addAction(self._resetZoomAction)
- self._colormapAction = actions.control.ColormapAction(
- parent=self, plot=plot)
+ self._colormapAction = actions.control.ColormapAction(parent=self, plot=plot)
self.addAction(self._colormapAction)
self._keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addWidget(self._keepDataAspectRatioButton)
self._yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addWidget(self._yAxisInvertedButton)
def getResetZoomAction(self):
@@ -183,37 +179,40 @@ class CurveToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Image'):
+ def __init__(self, parent=None, plot=None, title="Image"):
super(CurveToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
+ self._resetZoomAction = actions.control.ResetZoomAction(parent=self, plot=plot)
self.addAction(self._resetZoomAction)
self._xAxisAutoScaleAction = actions.control.XAxisAutoScaleAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._xAxisAutoScaleAction)
self._yAxisAutoScaleAction = actions.control.YAxisAutoScaleAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._yAxisAutoScaleAction)
self._xAxisLogarithmicAction = actions.control.XAxisLogarithmicAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._xAxisLogarithmicAction)
self._yAxisLogarithmicAction = actions.control.YAxisLogarithmicAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._yAxisLogarithmicAction)
- self._gridAction = actions.control.GridAction(
- parent=self, plot=plot)
+ self._gridAction = actions.control.GridAction(parent=self, plot=plot)
self.addAction(self._gridAction)
self._curveStyleAction = actions.control.CurveStyleAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._curveStyleAction)
def getResetZoomAction(self):
@@ -274,37 +273,38 @@ class ScatterToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, plot=None, title='Scatter Tools'):
+ def __init__(self, parent=None, plot=None, title="Scatter Tools"):
super(ScatterToolBar, self).__init__(title, parent)
assert isinstance(plot, PlotWidget)
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
+ self._resetZoomAction = actions.control.ResetZoomAction(parent=self, plot=plot)
self.addAction(self._resetZoomAction)
self._xAxisLogarithmicAction = actions.control.XAxisLogarithmicAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._xAxisLogarithmicAction)
self._yAxisLogarithmicAction = actions.control.YAxisLogarithmicAction(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addAction(self._yAxisLogarithmicAction)
self._keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=plot)
+ parent=self, plot=plot
+ )
self.addWidget(self._keepDataAspectRatioButton)
- self._gridAction = actions.control.GridAction(
- parent=self, plot=plot)
+ self._gridAction = actions.control.GridAction(parent=self, plot=plot)
self.addAction(self._gridAction)
- self._colormapAction = actions.control.ColormapAction(
- parent=self, plot=plot)
+ self._colormapAction = actions.control.ColormapAction(parent=self, plot=plot)
self.addAction(self._colormapAction)
- self._visualizationToolButton = \
- PlotToolButtons.ScatterVisualizationToolButton(parent=self, plot=plot)
+ self._visualizationToolButton = PlotToolButtons.ScatterVisualizationToolButton(
+ parent=self, plot=plot
+ )
self.addWidget(self._visualizationToolButton)
def getResetZoomAction(self):
@@ -355,8 +355,3 @@ class ScatterToolBar(qt.QToolBar):
:rtype: ScatterVisualizationToolButton
"""
return self._visualizationToolButton
-
- @deprecated(replacement='getScatterVisualizationToolButton',
- since_version='0.11.0')
- def getSymbolToolButton(self):
- return self.getScatterVisualizationToolButton()
diff --git a/src/silx/gui/plot/utils/__init__.py b/src/silx/gui/plot/utils/__init__.py
index 3187f6b..61e45b4 100644
--- a/src/silx/gui/plot/utils/__init__.py
+++ b/src/silx/gui/plot/utils/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot/utils/axis.py b/src/silx/gui/plot/utils/axis.py
index 5cf8ad9..4c6bcef 100644
--- a/src/silx/gui/plot/utils/axis.py
+++ b/src/silx/gui/plot/utils/axis.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -57,14 +56,16 @@ class SyncAxes(object):
.. versionadded:: 0.6
"""
- def __init__(self, axes,
- syncLimits=True,
- syncScale=True,
- syncDirection=True,
- syncCenter=False,
- syncZoom=False,
- filterHiddenPlots=False
- ):
+ def __init__(
+ self,
+ axes,
+ syncLimits=True,
+ syncScale=True,
+ syncDirection=True,
+ syncCenter=False,
+ syncZoom=False,
+ filterHiddenPlots=False,
+ ):
"""
Constructor
@@ -80,12 +81,13 @@ class SyncAxes(object):
"""
object.__init__(self)
- def implies(x, y): return bool(y ** x)
+ def implies(x, y):
+ return bool(y**x)
- assert(implies(syncZoom, not syncLimits))
- assert(implies(syncCenter, not syncLimits))
- assert(implies(syncLimits, not syncCenter))
- assert(implies(syncLimits, not syncZoom))
+ assert implies(syncZoom, not syncLimits)
+ assert implies(syncCenter, not syncLimits)
+ assert implies(syncLimits, not syncCenter)
+ assert implies(syncLimits, not syncZoom)
self.__filterHiddenPlots = filterHiddenPlots
self.__locked = False
@@ -314,7 +316,7 @@ class SyncAxes(object):
elif isinstance(axis, YAxis):
return bounds[3]
else:
- assert(False)
+ assert False
def __getLimitsFromCenter(self, axis, pos, pixelSize=None):
"""Returns the limits to apply to this axis to move the `pos` into the
diff --git a/src/silx/gui/plot/utils/intersections.py b/src/silx/gui/plot/utils/intersections.py
index 53f2546..faf6641 100644
--- a/src/silx/gui/plot/utils/intersections.py
+++ b/src/silx/gui/plot/utils/intersections.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
@@ -25,7 +24,9 @@
"""This module contains utils class for axes management.
"""
-__authors__ = ["H. Payno", ]
+__authors__ = [
+ "H. Payno",
+]
__license__ = "MIT"
__date__ = "18/05/2020"
@@ -60,11 +61,11 @@ def lines_intersection(line1_pt1, line1_pt2, line2_pt1, line2_pt2):
return None
return (
(num / denom.astype(float)) * dir_line2[0] + line2_pt1[0],
- (num / denom.astype(float)) * dir_line2[1] + line2_pt1[1])
+ (num / denom.astype(float)) * dir_line2[1] + line2_pt1[1],
+ )
-def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt,
- seg2_end_pt):
+def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt, seg2_end_pt):
"""
Compute intersection between two segments
@@ -75,10 +76,12 @@ def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt,
:return: numpy.array if an intersection exists, else None
:rtype: Union[None,numpy.array]
"""
- intersection = lines_intersection(line1_pt1=seg1_start_pt,
- line1_pt2=seg1_end_pt,
- line2_pt1=seg2_start_pt,
- line2_pt2=seg2_end_pt)
+ intersection = lines_intersection(
+ line1_pt1=seg1_start_pt,
+ line1_pt2=seg1_end_pt,
+ line2_pt1=seg2_start_pt,
+ line2_pt2=seg2_end_pt,
+ )
if intersection is not None:
max_x_seg1 = max(seg1_start_pt[0], seg1_end_pt[0])
max_x_seg2 = max(seg2_start_pt[0], seg2_end_pt[0])
@@ -94,8 +97,10 @@ def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt,
max_tmp_x = min(max_x_seg1, max_x_seg2)
min_tmp_y = max(min_y_seg1, min_y_seg2)
max_tmp_y = min(max_y_seg1, max_y_seg2)
- if (min_tmp_x <= intersection[0] <= max_tmp_x and
- min_tmp_y <= intersection[1] <= max_tmp_y):
+ if (
+ min_tmp_x <= intersection[0] <= max_tmp_x
+ and min_tmp_y <= intersection[1] <= max_tmp_y
+ ):
return intersection
else:
return None
diff --git a/src/silx/gui/plot3d/ParamTreeView.py b/src/silx/gui/plot3d/ParamTreeView.py
index 2593860..34ed1aa 100644
--- a/src/silx/gui/plot3d/ParamTreeView.py
+++ b/src/silx/gui/plot3d/ParamTreeView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -32,14 +31,14 @@ This module contains:
:class:`FloatEditor`, :class:`Vector3DEditor`,
:class:`Vector4DEditor`, :class:`IntSliderEditor`, :class:`BooleanEditor`
"""
-
-from __future__ import absolute_import
+from __future__ import annotations
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "05/12/2017"
+from collections.abc import Sequence
import numbers
import sys
@@ -52,25 +51,19 @@ class FloatEditor(_FloatEdit):
"""Editor widget for float.
:param parent: The widget's parent
- :param float value: The initial editor value
+ :param value: The initial editor value
"""
- valueChanged = qt.Signal(float)
- """Signal emitted when the float value has changed"""
-
- def __init__(self, parent=None, value=None):
+ def __init__(self, parent: qt.QWidget | None = None, value: float | None = None):
super(FloatEditor, self).__init__(parent, value)
self.setAlignment(qt.Qt.AlignLeft)
- self.editingFinished.connect(self._emit)
-
- def _emit(self):
- self.valueChanged.emit(self.value)
- value = qt.Property(float,
- fget=_FloatEdit.value,
- fset=_FloatEdit.setValue,
- user=True,
- notify=valueChanged)
+ valueProperty = qt.Property(
+ float,
+ fget=_FloatEdit.value,
+ fset=_FloatEdit.setValue,
+ user=True,
+ )
"""Qt user property of the float value this widget edits"""
@@ -81,59 +74,49 @@ class Vector3DEditor(qt.QWidget):
:param flags: The widgets's flags
"""
- valueChanged = qt.Signal(qt.QVector3D)
- """Signal emitted when the QVector3D value has changed"""
-
- def __init__(self, parent=None, flags=qt.Qt.Widget):
+ def __init__(
+ self,
+ parent: qt.QWidget | None = None,
+ flags: qt.Qt.WindowType = qt.Qt.Widget,
+ ):
super(Vector3DEditor, self).__init__(parent, flags)
layout = qt.QHBoxLayout(self)
# layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
- self.setLayout(layout)
- self._xEdit = _FloatEdit(parent=self, value=0.)
+
+ self._xEdit = _FloatEdit(parent=self, value=0.0)
self._xEdit.setAlignment(qt.Qt.AlignLeft)
- # self._xEdit.editingFinished.connect(self._emit)
- self._yEdit = _FloatEdit(parent=self, value=0.)
+ self._yEdit = _FloatEdit(parent=self, value=0.0)
self._yEdit.setAlignment(qt.Qt.AlignLeft)
- # self._yEdit.editingFinished.connect(self._emit)
- self._zEdit = _FloatEdit(parent=self, value=0.)
+ self._zEdit = _FloatEdit(parent=self, value=0.0)
self._zEdit.setAlignment(qt.Qt.AlignLeft)
- # self._zEdit.editingFinished.connect(self._emit)
- layout.addWidget(qt.QLabel('x:'))
+
+ layout.addWidget(qt.QLabel("x:"))
layout.addWidget(self._xEdit)
- layout.addWidget(qt.QLabel('y:'))
+ layout.addWidget(qt.QLabel("y:"))
layout.addWidget(self._yEdit)
- layout.addWidget(qt.QLabel('z:'))
+ layout.addWidget(qt.QLabel("z:"))
layout.addWidget(self._zEdit)
layout.addStretch(1)
- def _emit(self):
- vector = self.value
- self.valueChanged.emit(vector)
-
- def getValue(self):
- """Returns the QVector3D value of this widget
-
- :rtype: QVector3D
- """
+ def getValue(self) -> qt.QVector3D:
+ """Returns the QVector3D value of this widget"""
return qt.QVector3D(
- self._xEdit.value(), self._yEdit.value(), self._zEdit.value())
+ self._xEdit.value(), self._yEdit.value(), self._zEdit.value()
+ )
- def setValue(self, value):
- """Set the QVector3D value
-
- :param QVector3D value: The new value
- """
+ def setValue(self, value: qt.QVector3D):
+ """Set the QVector3D value"""
self._xEdit.setValue(value.x())
self._yEdit.setValue(value.y())
self._zEdit.setValue(value.z())
- self.valueChanged.emit(value)
- value = qt.Property(qt.QVector3D,
- fget=getValue,
- fset=setValue,
- user=True,
- notify=valueChanged)
+ value = qt.Property(
+ qt.QVector3D,
+ fget=getValue,
+ fset=setValue,
+ user=True,
+ )
"""Qt user property of the QVector3D value this widget edits"""
@@ -144,65 +127,57 @@ class Vector4DEditor(qt.QWidget):
:param flags: The widgets's flags
"""
- valueChanged = qt.Signal(qt.QVector4D)
- """Signal emitted when the QVector4D value has changed"""
-
- def __init__(self, parent=None, flags=qt.Qt.Widget):
+ def __init__(
+ self,
+ parent: qt.QWidget | None = None,
+ flags: qt.Qt.WindowType = qt.Qt.Widget,
+ ):
super(Vector4DEditor, self).__init__(parent, flags)
layout = qt.QHBoxLayout(self)
# layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
- self.setLayout(layout)
- self._xEdit = _FloatEdit(parent=self, value=0.)
+
+ self._xEdit = _FloatEdit(parent=self, value=0.0)
self._xEdit.setAlignment(qt.Qt.AlignLeft)
- # self._xEdit.editingFinished.connect(self._emit)
- self._yEdit = _FloatEdit(parent=self, value=0.)
+ self._yEdit = _FloatEdit(parent=self, value=0.0)
self._yEdit.setAlignment(qt.Qt.AlignLeft)
- # self._yEdit.editingFinished.connect(self._emit)
- self._zEdit = _FloatEdit(parent=self, value=0.)
+ self._zEdit = _FloatEdit(parent=self, value=0.0)
self._zEdit.setAlignment(qt.Qt.AlignLeft)
- # self._zEdit.editingFinished.connect(self._emit)
- self._wEdit = _FloatEdit(parent=self, value=0.)
+ self._wEdit = _FloatEdit(parent=self, value=0.0)
self._wEdit.setAlignment(qt.Qt.AlignLeft)
- # self._wEdit.editingFinished.connect(self._emit)
- layout.addWidget(qt.QLabel('x:'))
+
+ layout.addWidget(qt.QLabel("x:"))
layout.addWidget(self._xEdit)
- layout.addWidget(qt.QLabel('y:'))
+ layout.addWidget(qt.QLabel("y:"))
layout.addWidget(self._yEdit)
- layout.addWidget(qt.QLabel('z:'))
+ layout.addWidget(qt.QLabel("z:"))
layout.addWidget(self._zEdit)
- layout.addWidget(qt.QLabel('w:'))
+ layout.addWidget(qt.QLabel("w:"))
layout.addWidget(self._wEdit)
layout.addStretch(1)
- def _emit(self):
- vector = self.value
- self.valueChanged.emit(vector)
-
- def getValue(self):
- """Returns the QVector4D value of this widget
-
- :rtype: QVector4D
- """
- return qt.QVector4D(self._xEdit.value(), self._yEdit.value(),
- self._zEdit.value(), self._wEdit.value())
-
- def setValue(self, value):
- """Set the QVector4D value
-
- :param QVector4D value: The new value
- """
+ def getValue(self) -> qt.QVector4D:
+ """Returns the QVector4D value of this widget"""
+ return qt.QVector4D(
+ self._xEdit.value(),
+ self._yEdit.value(),
+ self._zEdit.value(),
+ self._wEdit.value(),
+ )
+
+ def setValue(self, value: qt.QVector4D):
+ """Set the QVector4D value"""
self._xEdit.setValue(value.x())
self._yEdit.setValue(value.y())
self._zEdit.setValue(value.z())
self._wEdit.setValue(value.w())
- self.valueChanged.emit(value)
- value = qt.Property(qt.QVector4D,
- fget=getValue,
- fset=setValue,
- user=True,
- notify=valueChanged)
+ value = qt.Property(
+ qt.QVector4D,
+ fget=getValue,
+ fset=setValue,
+ user=True,
+ )
"""Qt user property of the QVector4D value this widget edits"""
@@ -214,7 +189,7 @@ class IntSliderEditor(qt.QSlider):
:param parent: The widget's parent
"""
- def __init__(self, parent=None):
+ def __init__(self, parent: qt.QWidget | None = None):
super(IntSliderEditor, self).__init__(parent)
self.setOrientation(qt.Qt.Horizontal)
self.setSingleStep(1)
@@ -225,14 +200,39 @@ class IntSliderEditor(qt.QSlider):
class BooleanEditor(qt.QCheckBox):
"""Checkbox editor for bool.
- This is a QCheckBox with white background.
+ Wrap a QCheckBox to define a different user property with `clicked` signal.
:param parent: The widget's parent
"""
- def __init__(self, parent=None):
+ valueChanged = qt.Signal(bool)
+ """Signal emitted when value is changed by the user"""
+
+ def __init__(self, parent: qt.QWidget | None = None):
super(BooleanEditor, self).__init__(parent)
- self.setStyleSheet("background: white;")
+ self.setBackgroundRole(qt.QPalette.Base)
+ self.setAutoFillBackground(True)
+
+ layout = qt.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ self.__checkbox = qt.QCheckBox(self)
+ self.__checkbox.clicked.connect(self.valueChanged)
+ layout.addWidget(self.__checkbox)
+
+ def getValue(self) -> bool:
+ return self.__checkbox.isChecked()
+
+ def setValue(self, value: bool):
+ self.__checkbox.setChecked(value)
+
+ value = qt.Property(
+ bool,
+ fget=getValue,
+ fset=setValue,
+ user=True,
+ notify=valueChanged,
+ )
+ """Qt user property of the bool value this widget edits"""
class ParameterTreeDelegate(qt.QStyledItemDelegate):
@@ -251,77 +251,60 @@ class ParameterTreeDelegate(qt.QStyledItemDelegate):
}
"""Specific editors for different type of data"""
- def __init__(self, parent=None):
+ def __init__(self, parent: qt.QWidget | None = None):
super(ParameterTreeDelegate, self).__init__(parent)
- def paint(self, painter, option, index):
+ def paint(
+ self,
+ painter: qt.QPainter,
+ option: qt.QStyleOptionViewItem,
+ index: qt.QModelIndex,
+ ):
"""See :meth:`QStyledItemDelegate.paint`"""
data = index.data(qt.Qt.DisplayRole)
- if isinstance(data, (qt.QVector3D, qt.QVector4D)):
- if isinstance(data, qt.QVector3D):
- text = '(x: %g; y: %g; z: %g)' % (data.x(), data.y(), data.z())
- elif isinstance(data, qt.QVector4D):
- text = '(%g; %g; %g; %g)' % (data.x(), data.y(), data.z(), data.w())
- else:
- text = ''
-
- painter.save()
- painter.setRenderHint(qt.QPainter.Antialiasing, True)
-
- # Select palette color group
- colorGroup = qt.QPalette.Inactive
- if option.state & qt.QStyle.State_Active:
- colorGroup = qt.QPalette.Active
- if not option.state & qt.QStyle.State_Enabled:
- colorGroup = qt.QPalette.Disabled
-
- # Draw background if selected
- if option.state & qt.QStyle.State_Selected:
- brush = option.palette.brush(colorGroup,
- qt.QPalette.Highlight)
- painter.fillRect(option.rect, brush)
-
- # Draw text
- if option.state & qt.QStyle.State_Selected:
- colorRole = qt.QPalette.HighlightedText
- else:
- colorRole = qt.QPalette.WindowText
- color = option.palette.color(colorGroup, colorRole)
- painter.setPen(qt.QPen(color))
- painter.drawText(option.rect, qt.Qt.AlignLeft, text)
-
- painter.restore()
-
- # The following commented code does the same as QPainter based code
- # but it does not work with PySide
- # self.initStyleOption(option, index)
- # option.text = text
- # widget = option.widget
- # style = qt.QApplication.style() if not widget else widget.style()
- # style.drawControl(qt.QStyle.CE_ItemViewItem, option, painter, widget)
+ if not isinstance(data, (qt.QVector3D, qt.QVector4D)):
+ super(ParameterTreeDelegate, self).paint(painter, option, index)
+ return
+ if isinstance(data, qt.QVector3D):
+ text = "(x: %g; y: %g; z: %g)" % (data.x(), data.y(), data.z())
+ elif isinstance(data, qt.QVector4D):
+ text = "(%g; %g; %g; %g)" % (data.x(), data.y(), data.z(), data.w())
else:
- super(ParameterTreeDelegate, self).paint(painter, option, index)
+ text = ""
+
+ self.initStyleOption(option, index)
+ option.text = text
+ widget = option.widget
+ style = qt.QApplication.style() if not widget else widget.style()
+ style.drawControl(qt.QStyle.CE_ItemViewItem, option, painter, widget)
def _commit(self, *args):
"""Commit data to the model from editors"""
sender = self.sender()
self.commitData.emit(sender)
- def editorEvent(self, event, model, option, index):
+ def editorEvent(
+ self,
+ event: qt.QEvent,
+ model: qt.QAbstractItemModel,
+ option: qt.QStyleOptionViewItem,
+ index: qt.QModelIndex,
+ ):
"""See :meth:`QStyledItemDelegate.editorEvent`"""
- if (event.type() == qt.QEvent.MouseButtonPress and
- isinstance(index.data(qt.Qt.EditRole), qt.QColor)):
+ if event.type() == qt.QEvent.MouseButtonPress and isinstance(
+ index.data(qt.Qt.EditRole), qt.QColor
+ ):
initialColor = index.data(qt.Qt.EditRole)
- def callback(color):
+ def callback(color: qt.QColor):
theModel = index.model()
theModel.setData(index, color, qt.Qt.EditRole)
dialog = qt.QColorDialog(self.parent())
# dialog.setOption(qt.QColorDialog.ShowAlphaChannel, True)
- if sys.platform == 'darwin':
+ if sys.platform == "darwin":
# Use of native color dialog on macos might cause problems
dialog.setOption(qt.QColorDialog.DontUseNativeDialog, True)
dialog.setCurrentColor(initialColor)
@@ -333,9 +316,15 @@ class ParameterTreeDelegate(qt.QStyledItemDelegate):
return True
else:
return super(ParameterTreeDelegate, self).editorEvent(
- event, model, option, index)
-
- def createEditor(self, parent, option, index):
+ event, model, option, index
+ )
+
+ def createEditor(
+ self,
+ parent: qt.QWidget,
+ option: qt.QStyleOptionViewItem,
+ index: qt.QModelIndex,
+ ):
"""See :meth:`QStyledItemDelegate.createEditor`"""
data = index.data(qt.Qt.EditRole)
editorHint = index.data(qt.Qt.UserRole)
@@ -375,14 +364,8 @@ class ParameterTreeDelegate(qt.QStyledItemDelegate):
if userProperty.isValid() and userProperty.hasNotifySignal():
notifySignal = userProperty.notifySignal()
signature = notifySignal.methodSignature()
- if qt.BINDING == 'PySide2':
- signature = signature.data()
- else:
- signature = bytes(signature)
-
- if hasattr(signature, 'decode'): # For PySide with python3
- signature = signature.decode('ascii')
- signalName = signature.split('(')[0]
+ signature = bytes(signature).decode("ascii")
+ signalName = signature.split("(")[0]
signal = getattr(editor, signalName)
signal.connect(self._commit)
@@ -390,12 +373,18 @@ class ParameterTreeDelegate(qt.QStyledItemDelegate):
else: # Default handling for default types
return super(ParameterTreeDelegate, self).createEditor(
- parent, option, index)
+ parent, option, index
+ )
editor.setAutoFillBackground(True)
return editor
- def setModelData(self, editor, model, index):
+ def setModelData(
+ self,
+ editor: qt.QWidget,
+ model: qt.QAbstractItemModel,
+ index: qt.QModelIndex,
+ ):
"""See :meth:`QStyledItemDelegate.setModelData`"""
if isinstance(editor, tuple(self.EDITORS.values())):
# Special handling of Python classes
@@ -423,7 +412,7 @@ class ParamTreeView(qt.QTreeView):
:param parent: The widget's parent.
"""
- def __init__(self, parent=None):
+ def __init__(self, parent: qt.QWidget | None = None):
super(ParamTreeView, self).__init__(parent)
header = self.header()
@@ -438,65 +427,67 @@ class ParamTreeView(qt.QTreeView):
self.expanded.connect(self._expanded)
- self.setEditTriggers(qt.QAbstractItemView.CurrentChanged |
- qt.QAbstractItemView.DoubleClicked)
+ self.setEditTriggers(
+ qt.QAbstractItemView.CurrentChanged | qt.QAbstractItemView.DoubleClicked
+ )
self.__persistentEditors = set()
- def _openEditorForIndex(self, index):
+ def _openEditorForIndex(self, index: qt.QModelIndex):
"""Check if it has to open a persistent editor for a specific cell.
- :param QModelIndex index: The cell index
+ :param index: The cell index
"""
if index.flags() & qt.Qt.ItemIsEditable:
data = index.data(qt.Qt.EditRole)
editorHint = index.data(qt.Qt.UserRole)
- if (isinstance(data, bool) or
- callable(editorHint) or
- (isinstance(data, numbers.Number) and editorHint)):
+ if (
+ isinstance(data, bool)
+ or callable(editorHint)
+ or (isinstance(data, numbers.Number) and editorHint)
+ ):
self.openPersistentEditor(index)
self.__persistentEditors.add(index)
- def _openEditors(self, parent=qt.QModelIndex()):
+ def _openEditors(self, parent: qt.QModelIndex = qt.QModelIndex()):
"""Open persistent editors in a subtree starting at parent.
- :param QModelIndex parent: The root of the subtree to process.
+ :param parent: The root of the subtree to process.
"""
model = self.model()
if model is not None:
for index in visitQAbstractItemModel(model, parent):
self._openEditorForIndex(index)
- def setModel(self, model):
- """Set the model this TreeView is displaying
-
- :param QAbstractItemModel model:
- """
+ def setModel(self, model: qt.QAbstractItemModel):
+ """Set the model this TreeView is displaying"""
super(ParamTreeView, self).setModel(model)
self._openEditors()
- def rowsInserted(self, parent, start, end):
+ def rowsInserted(self, parent: qt.QModelIndex, start: int, end: int):
"""See :meth:`QTreeView.rowsInserted`"""
super(ParamTreeView, self).rowsInserted(parent, start, end)
model = self.model()
if model is not None:
- for row in range(start, end+1):
+ for row in range(start, end + 1):
self._openEditorForIndex(model.index(row, 1, parent))
self._openEditors(model.index(row, 0, parent))
- def _expanded(self, index):
+ def _expanded(self, index: qt.QModelIndex):
"""Handle QTreeView expanded signal"""
name = index.data(qt.Qt.DisplayRole)
- if name == 'Transform':
+ if name == "Transform":
rotateIndex = self.model().index(1, 0, index)
self.setExpanded(rotateIndex, True)
- def dataChanged(self, topLeft, bottomRight, roles=()):
+ def dataChanged(
+ self,
+ topLeft: qt.QModelIndex,
+ bottomRight: qt.QModelIndex,
+ roles: Sequence[int] = (),
+ ):
"""Handle model dataChanged signal eventually closing editors"""
- if roles: # Qt 5
- super(ParamTreeView, self).dataChanged(topLeft, bottomRight, roles)
- else: # Qt4 compatibility
- super(ParamTreeView, self).dataChanged(topLeft, bottomRight)
+ super(ParamTreeView, self).dataChanged(topLeft, bottomRight, roles)
if not roles or qt.Qt.UserRole in roles: # Check editorHint update
for row in range(topLeft.row(), bottomRight.row() + 1):
for column in range(topLeft.column(), bottomRight.column() + 1):
@@ -506,15 +497,15 @@ class ParamTreeView(qt.QTreeView):
self.closePersistentEditor(index)
self._openEditorForIndex(index)
- def _isPersistentEditorOpen(self, index):
- """Returns True if a persistent editor is opened for index
-
- :param QModelIndex index:
- :rtype: bool
- """
+ def _isPersistentEditorOpen(self, index: qt.QModelIndex) -> bool:
+ """Returns True if a persistent editor is opened for index"""
return index in self.__persistentEditors
- def selectionCommand(self, index, event=None):
+ def selectionCommand(
+ self,
+ index: qt.QModelIndex,
+ event: qt.QEvent | None = None,
+ ) -> qt.QItemSelectionModel.SelectionFlag:
"""Filter out selection of not selectable items"""
if index.flags() & qt.Qt.ItemIsSelectable:
return super(ParamTreeView, self).selectionCommand(index, event)
diff --git a/src/silx/gui/plot3d/Plot3DWidget.py b/src/silx/gui/plot3d/Plot3DWidget.py
index a90d34c..9a88fe3 100644
--- a/src/silx/gui/plot3d/Plot3DWidget.py
+++ b/src/silx/gui/plot3d/Plot3DWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module provides a Qt widget embedding an OpenGL scene."""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
@@ -69,10 +66,9 @@ class _OverviewViewport(scene.Viewport):
# Add a point to draw the background (in a group with depth mask)
backgroundPoint = primitives.ColorPoints(
- x=0., y=0., z=0.,
- color=(1., 1., 1., 0.5),
- size=self._SIZE)
- backgroundPoint.marker = 'o'
+ x=0.0, y=0.0, z=0.0, color=(1.0, 1.0, 1.0, 0.5), size=self._SIZE
+ )
+ backgroundPoint.marker = "o"
noDepthGroup = primitives.GroupNoDepth(mask=True, notest=True)
noDepthGroup.children.append(backgroundPoint)
self.scene.children.append(noDepthGroup)
@@ -89,11 +85,12 @@ class _OverviewViewport(scene.Viewport):
Sync the overview camera to point in the same direction
but from a sphere centered on origin.
"""
- position = -12. * source.extrinsic.direction
+ position = -12.0 * source.extrinsic.direction
self.camera.extrinsic.position = position
self.camera.extrinsic.setOrientation(
- source.extrinsic.direction, source.extrinsic.up)
+ source.extrinsic.direction, source.extrinsic.up
+ )
class Plot3DWidget(glu.OpenGLWidget):
@@ -119,13 +116,13 @@ class Plot3DWidget(glu.OpenGLWidget):
class FogMode(_Enum):
"""Different mode to render the scene with fog"""
- NONE = 'none'
+ NONE = "none"
"""No fog effect"""
- LINEAR = 'linear'
+ LINEAR = "linear"
"""Linear fog through the whole scene"""
- def __init__(self, parent=None, f=qt.Qt.WindowFlags()):
+ def __init__(self, parent=None, f=qt.Qt.Widget):
self._firstRender = True
super(Plot3DWidget, self).__init__(
@@ -134,7 +131,8 @@ class Plot3DWidget(glu.OpenGLWidget):
depthBufferSize=0,
stencilBufferSize=0,
version=(2, 1),
- f=f)
+ f=f,
+ )
self.setAutoFillBackground(False)
self.setMouseTracking(True)
@@ -148,22 +146,24 @@ class Plot3DWidget(glu.OpenGLWidget):
# Main viewport
self.viewport = scene.Viewport()
- self._sceneScale = transform.Scale(1., 1., 1.)
- self.viewport.scene.transforms = [self._sceneScale,
- transform.Translate(0., 0., 0.)]
+ self._sceneScale = transform.Scale(1.0, 1.0, 1.0)
+ self.viewport.scene.transforms = [
+ self._sceneScale,
+ transform.Translate(0.0, 0.0, 0.0),
+ ]
# Overview area
self.overview = _OverviewViewport(self.viewport.camera)
- self.setBackgroundColor((0.2, 0.2, 0.2, 1.))
+ self.setBackgroundColor((0.2, 0.2, 0.2, 1.0))
# Window describing on screen area to render
- self._window = scene.Window(mode='framebuffer')
+ self._window = scene.Window(mode="framebuffer")
self._window.viewports = [self.viewport, self.overview]
self._window.addListener(self._redraw)
self.eventHandler = None
- self.setInteractiveMode('rotate')
+ self.setInteractiveMode("rotate")
def __clickHandler(self, *args):
"""Handle interaction state machine click"""
@@ -183,31 +183,35 @@ class Plot3DWidget(glu.OpenGLWidget):
if mode is None:
self.eventHandler = None
- elif mode == 'rotate':
+ elif mode == "rotate":
self.eventHandler = interaction.RotateCameraControl(
self.viewport,
orbitAroundCenter=False,
- mode='position',
+ mode="position",
scaleTransform=self._sceneScale,
- selectCB=self.__clickHandler)
+ selectCB=self.__clickHandler,
+ )
- elif mode == 'pan':
+ elif mode == "pan":
self.eventHandler = interaction.PanCameraControl(
self.viewport,
orbitAroundCenter=False,
- mode='position',
+ mode="position",
scaleTransform=self._sceneScale,
- selectCB=self.__clickHandler)
+ selectCB=self.__clickHandler,
+ )
elif isinstance(mode, interaction.StateMachine):
self.eventHandler = mode
else:
- raise ValueError('Unsupported interactive mode %s', str(mode))
+ raise ValueError("Unsupported interactive mode %s", str(mode))
- if (self.eventHandler is not None and
- qt.QApplication.keyboardModifiers() & qt.Qt.ControlModifier):
- self.eventHandler.handleEvent('keyPress', qt.Qt.Key_Control)
+ if (
+ self.eventHandler is not None
+ and qt.QApplication.keyboardModifiers() & qt.Qt.ControlModifier
+ ):
+ self.eventHandler.handleEvent("keyPress", qt.Qt.Key_Control)
self.sigInteractiveModeChanged.emit()
@@ -219,9 +223,9 @@ class Plot3DWidget(glu.OpenGLWidget):
if self.eventHandler is None:
return None
if isinstance(self.eventHandler, interaction.RotateCameraControl):
- return 'rotate'
+ return "rotate"
elif isinstance(self.eventHandler, interaction.PanCameraControl):
- return 'pan'
+ return "pan"
else:
return None
@@ -230,13 +234,12 @@ class Plot3DWidget(glu.OpenGLWidget):
:param str projection: In 'perspective', 'orthographic'.
"""
- if projection == 'orthographic':
+ if projection == "orthographic":
projection = transform.Orthographic(size=self.viewport.size)
- elif projection == 'perspective':
- projection = transform.Perspective(fovy=30.,
- size=self.viewport.size)
+ elif projection == "perspective":
+ projection = transform.Perspective(fovy=30.0, size=self.viewport.size)
else:
- raise RuntimeError('Unsupported projection: %s' % projection)
+ raise RuntimeError("Unsupported projection: %s" % projection)
self.viewport.camera.intrinsic = projection
self.viewport.resetCamera()
@@ -248,11 +251,11 @@ class Plot3DWidget(glu.OpenGLWidget):
"""
projection = self.viewport.camera.intrinsic
if isinstance(projection, transform.Orthographic):
- return 'orthographic'
+ return "orthographic"
elif isinstance(projection, transform.Perspective):
- return 'perspective'
+ return "perspective"
else:
- raise RuntimeError('Unknown projection in use')
+ raise RuntimeError("Unknown projection in use")
def setBackgroundColor(self, color):
"""Set the background color of the OpenGL view.
@@ -264,7 +267,7 @@ class Plot3DWidget(glu.OpenGLWidget):
color = rgba(color)
if color != self.viewport.background:
self.viewport.background = color
- self.sigStyleChanged.emit('backgroundColor')
+ self.sigStyleChanged.emit("backgroundColor")
def getBackgroundColor(self):
"""Returns the RGBA background color (QColor)."""
@@ -279,7 +282,7 @@ class Plot3DWidget(glu.OpenGLWidget):
mode = self.FogMode.from_value(mode)
if mode != self.getFogMode():
self.viewport.fog.isOn = mode is self.FogMode.LINEAR
- self.sigStyleChanged.emit('fogMode')
+ self.sigStyleChanged.emit("fogMode")
def getFogMode(self):
"""Returns the kind of fog in use
@@ -310,13 +313,13 @@ class Plot3DWidget(glu.OpenGLWidget):
self._window.viewports = [self.viewport, self.overview]
else:
self._window.viewports = [self.viewport]
- self.sigStyleChanged.emit('orientationIndicatorVisible')
+ self.sigStyleChanged.emit("orientationIndicatorVisible")
def centerScene(self):
"""Position the center of the scene at the center of rotation."""
self.viewport.resetCamera()
- def resetZoom(self, face='front'):
+ def resetZoom(self, face="front"):
"""Reset the camera position to a default.
:param str face: The direction the camera is looking at:
@@ -347,7 +350,9 @@ class Plot3DWidget(glu.OpenGLWidget):
if self.viewport.dirty:
self.viewport.adjustCameraDepthExtent()
- self._window.render(self.context(), self.getDevicePixelRatio())
+ self._window.render(
+ self.context(), self.getDotsPerInch(), self.getDevicePixelRatio()
+ )
if self._firstRender: # TODO remove this ugly hack
self._firstRender = False
@@ -369,7 +374,7 @@ class Plot3DWidget(glu.OpenGLWidget):
:rtype: QImage
"""
if not self.isValid():
- _logger.error('OpenGL 2.1 not available, cannot save OpenGL image')
+ _logger.error("OpenGL 2.1 not available, cannot save OpenGL image")
height, width = self._window.shape
image = numpy.zeros((height, width, 3), dtype=numpy.uint8)
@@ -380,28 +385,25 @@ class Plot3DWidget(glu.OpenGLWidget):
return convertArrayToQImage(image)
def wheelEvent(self, event):
- if qt.BINDING == "PySide6":
- x, y = event.position().x(), event.position().y()
- else:
- x, y = event.x(), event.y()
+ x, y = qt.getMouseEventPosition(event)
xpixel = x * self.getDevicePixelRatio()
ypixel = y * self.getDevicePixelRatio()
- angle = event.angleDelta().y() / 8.
+ angle = event.angleDelta().y() / 8.0
event.accept()
if self.eventHandler is not None and angle != 0 and self.isValid():
self.makeCurrent()
- self.eventHandler.handleEvent('wheel', xpixel, ypixel, angle)
+ self.eventHandler.handleEvent("wheel", xpixel, ypixel, angle)
def keyPressEvent(self, event):
keyCode = event.key()
# No need to accept QKeyEvent
converter = {
- qt.Qt.Key_Left: 'left',
- qt.Qt.Key_Right: 'right',
- qt.Qt.Key_Up: 'up',
- qt.Qt.Key_Down: 'down'
+ qt.Qt.Key_Left: "left",
+ qt.Qt.Key_Right: "right",
+ qt.Qt.Key_Up: "up",
+ qt.Qt.Key_Down: "down",
}
direction = converter.get(keyCode, None)
if direction is not None:
@@ -413,10 +415,12 @@ class Plot3DWidget(glu.OpenGLWidget):
self.viewport.orbitCamera(direction)
else:
- if (keyCode == qt.Qt.Key_Control and
- self.eventHandler is not None and
- self.isValid()):
- self.eventHandler.handleEvent('keyPress', keyCode)
+ if (
+ keyCode == qt.Qt.Key_Control
+ and self.eventHandler is not None
+ and self.isValid()
+ ):
+ self.eventHandler.handleEvent("keyPress", keyCode)
# Key not handled, call base class implementation
super(Plot3DWidget, self).keyPressEvent(event)
@@ -424,40 +428,49 @@ class Plot3DWidget(glu.OpenGLWidget):
def keyReleaseEvent(self, event):
"""Catch Ctrl key release"""
keyCode = event.key()
- if (keyCode == qt.Qt.Key_Control and
- self.eventHandler is not None and
- self.isValid()):
- self.eventHandler.handleEvent('keyRelease', keyCode)
+ if (
+ keyCode == qt.Qt.Key_Control
+ and self.eventHandler is not None
+ and self.isValid()
+ ):
+ self.eventHandler.handleEvent("keyRelease", keyCode)
super(Plot3DWidget, self).keyReleaseEvent(event)
# Mouse events #
- _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'}
+ _MOUSE_BTNS = {
+ qt.Qt.LeftButton: "left",
+ qt.Qt.RightButton: "right",
+ qt.Qt.MiddleButton: "middle",
+ }
def mousePressEvent(self, event):
- xpixel = event.x() * self.getDevicePixelRatio()
- ypixel = event.y() * self.getDevicePixelRatio()
+ x, y = qt.getMouseEventPosition(event)
+ xpixel = x * self.getDevicePixelRatio()
+ ypixel = y * self.getDevicePixelRatio()
btn = self._MOUSE_BTNS[event.button()]
event.accept()
if self.eventHandler is not None and self.isValid():
self.makeCurrent()
- self.eventHandler.handleEvent('press', xpixel, ypixel, btn)
+ self.eventHandler.handleEvent("press", xpixel, ypixel, btn)
def mouseMoveEvent(self, event):
- xpixel = event.x() * self.getDevicePixelRatio()
- ypixel = event.y() * self.getDevicePixelRatio()
+ x, y = qt.getMouseEventPosition(event)
+ xpixel = x * self.getDevicePixelRatio()
+ ypixel = y * self.getDevicePixelRatio()
event.accept()
if self.eventHandler is not None and self.isValid():
self.makeCurrent()
- self.eventHandler.handleEvent('move', xpixel, ypixel)
+ self.eventHandler.handleEvent("move", xpixel, ypixel)
def mouseReleaseEvent(self, event):
- xpixel = event.x() * self.getDevicePixelRatio()
- ypixel = event.y() * self.getDevicePixelRatio()
+ x, y = qt.getMouseEventPosition(event)
+ xpixel = x * self.getDevicePixelRatio()
+ ypixel = y * self.getDevicePixelRatio()
btn = self._MOUSE_BTNS[event.button()]
event.accept()
if self.eventHandler is not None and self.isValid():
self.makeCurrent()
- self.eventHandler.handleEvent('release', xpixel, ypixel, btn)
+ self.eventHandler.handleEvent("release", xpixel, ypixel, btn)
diff --git a/src/silx/gui/plot3d/Plot3DWindow.py b/src/silx/gui/plot3d/Plot3DWindow.py
index 470b966..882f4cd 100644
--- a/src/silx/gui/plot3d/Plot3DWindow.py
+++ b/src/silx/gui/plot3d/Plot3DWindow.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides a QMainWindow with a 3D scene and associated toolbar.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "26/01/2017"
diff --git a/src/silx/gui/plot3d/SFViewParamTree.py b/src/silx/gui/plot3d/SFViewParamTree.py
index b269a6a..6eea5ae 100644
--- a/src/silx/gui/plot3d/SFViewParamTree.py
+++ b/src/silx/gui/plot3d/SFViewParamTree.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2021 European Synchrotron Radiation Facility
@@ -26,8 +25,6 @@
This module provides a tree widget to set/view parameters of a ScalarFieldView.
"""
-from __future__ import absolute_import
-
__authors__ = ["D. N."]
__license__ = "MIT"
__date__ = "24/04/2018"
@@ -51,7 +48,7 @@ _logger = logging.getLogger(__name__)
class ModelColumns(object):
NameColumn, ValueColumn, ColumnMax = range(3)
- ColumnNames = ['Name', 'Value']
+ ColumnNames = ["Name", "Value"]
class SubjectItem(qt.QStandardItem):
@@ -89,7 +86,6 @@ class SubjectItem(qt.QStandardItem):
"""
def __init__(self, subject, *args):
-
super(SubjectItem, self).__init__(*args)
self.setEditable(self.editable)
@@ -122,8 +118,7 @@ class SubjectItem(qt.QStandardItem):
@subject.setter
def subject(self, subject):
if self.__subject is not None:
- raise ValueError('Subject already set '
- ' (subject change not supported).')
+ raise ValueError("Subject already set " " (subject change not supported).")
if subject is None:
self.__subject = None
else:
@@ -139,9 +134,8 @@ class SubjectItem(qt.QStandardItem):
def gen_slot(_sigIdx):
def slotfn(*args, **kwargs):
- self._subjectChanged(signalIdx=_sigIdx,
- args=args,
- kwargs=kwargs)
+ self._subjectChanged(signalIdx=_sigIdx, args=args, kwargs=kwargs)
+
return slotfn
if self.__subject is not None:
@@ -296,8 +290,10 @@ class SubjectItem(qt.QStandardItem):
# View settings ###############################################################
+
class ColorItem(SubjectItem):
"""color item."""
+
editable = True
persistent = True
@@ -306,8 +302,7 @@ class ColorItem(SubjectItem):
editor.color = self.getColor()
# Wrapping call in lambda is a workaround for PySide with Python 3
- editor.sigColorChanged.connect(
- lambda color: self._editorSlot(color))
+ editor.sigColorChanged.connect(lambda color: self._editorSlot(color))
return editor
def _editorSlot(self, color):
@@ -326,7 +321,7 @@ class ColorItem(SubjectItem):
class BackgroundColorItem(ColorItem):
- itemName = 'Background'
+ itemName = "Background"
def setColor(self, color):
self.subject.setBackgroundColor(color)
@@ -336,7 +331,7 @@ class BackgroundColorItem(ColorItem):
class ForegroundColorItem(ColorItem):
- itemName = 'Foreground'
+ itemName = "Foreground"
def setColor(self, color):
self.subject.setForegroundColor(color)
@@ -346,7 +341,7 @@ class ForegroundColorItem(ColorItem):
class HighlightColorItem(ColorItem):
- itemName = 'Highlight'
+ itemName = "Highlight"
def setColor(self, color):
self.subject.setHighlightColor(color)
@@ -357,6 +352,7 @@ class HighlightColorItem(ColorItem):
class _LightDirectionAngleBaseItem(SubjectItem):
"""Base class for directional light angle item."""
+
editable = True
persistent = True
@@ -383,8 +379,7 @@ class _LightDirectionAngleBaseItem(SubjectItem):
editor.setValue(int(self._pullData()))
# Wrapping call in lambda is a workaround for PySide with Python 3
- editor.valueChanged.connect(
- lambda value: self._pushData(value))
+ editor.valueChanged.connect(lambda value: self._pushData(value))
return editor
@@ -405,10 +400,10 @@ class LightAzimuthAngleItem(_LightDirectionAngleBaseItem):
return self.subject.sigAzimuthAngleChanged
def _pullData(self):
- return self.subject.getAzimuthAngle()
+ return self.subject.getAzimuthAngle()
def _pushData(self, value, role=qt.Qt.UserRole):
- self.subject.setAzimuthAngle(value)
+ self.subject.setAzimuthAngle(value)
class LightAltitudeAngleItem(_LightDirectionAngleBaseItem):
@@ -418,15 +413,14 @@ class LightAltitudeAngleItem(_LightDirectionAngleBaseItem):
return self.subject.sigAltitudeAngleChanged
def _pullData(self):
- return self.subject.getAltitudeAngle()
+ return self.subject.getAltitudeAngle()
def _pushData(self, value, role=qt.Qt.UserRole):
- self.subject.setAltitudeAngle(value)
+ self.subject.setAltitudeAngle(value)
class _DirectionalLightProxy(qt.QObject):
- """Proxy to handle directional light with angles rather than vector.
- """
+ """Proxy to handle directional light with angles rather than vector."""
sigAzimuthAngleChanged = qt.Signal()
"""Signal sent when the azimuth angle has changed."""
@@ -438,8 +432,8 @@ class _DirectionalLightProxy(qt.QObject):
super(_DirectionalLightProxy, self).__init__()
self._light = light
light.addListener(self._directionUpdated)
- self._azimuth = 0.
- self._altitude = 0.
+ self._azimuth = 0.0
+ self._altitude = 0.0
def getAzimuthAngle(self):
"""Returns the signed angle in the horizontal plane.
@@ -485,14 +479,16 @@ class _DirectionalLightProxy(qt.QObject):
"""Handle light direction update in the scene"""
# Invert direction to manipulate the 'source' pointing to
# the center of the viewport
- x, y, z = - self._light.direction
+ x, y, z = -self._light.direction
# Horizontal plane is plane xz
azimuth = numpy.degrees(numpy.arctan2(x, z))
- altitude = numpy.degrees(numpy.pi/2. - numpy.arccos(y))
+ altitude = numpy.degrees(numpy.pi / 2.0 - numpy.arccos(y))
- if (abs(azimuth - self.getAzimuthAngle()) > 0.01 and
- abs(abs(altitude) - 90.) >= 0.001): # Do not update when at zenith
+ if (
+ abs(azimuth - self.getAzimuthAngle()) > 0.01
+ and abs(abs(altitude) - 90.0) >= 0.001
+ ): # Do not update when at zenith
self.setAzimuthAngle(azimuth)
if abs(altitude - self.getAltitudeAngle()) > 0.01:
@@ -501,10 +497,10 @@ class _DirectionalLightProxy(qt.QObject):
def _updateLight(self):
"""Update light direction in the scene"""
azimuth = numpy.radians(self._azimuth)
- delta = numpy.pi/2. - numpy.radians(self._altitude)
- z = - numpy.sin(delta) * numpy.cos(azimuth)
- x = - numpy.sin(delta) * numpy.sin(azimuth)
- y = - numpy.cos(delta)
+ delta = numpy.pi / 2.0 - numpy.radians(self._altitude)
+ z = -numpy.sin(delta) * numpy.cos(azimuth)
+ x = -numpy.sin(delta) * numpy.sin(azimuth)
+ y = -numpy.cos(delta)
self._light.direction = x, y, z
@@ -513,20 +509,18 @@ class DirectionalLightGroup(SubjectItem):
Root Item for the directional light
"""
- def __init__(self,subject, *args):
- self._light = _DirectionalLightProxy(
- subject.getPlot3DWidget().viewport.light)
+ def __init__(self, subject, *args):
+ self._light = _DirectionalLightProxy(subject.getPlot3DWidget().viewport.light)
super(DirectionalLightGroup, self).__init__(subject, *args)
def _init(self):
-
- nameItem = qt.QStandardItem('Azimuth')
+ nameItem = qt.QStandardItem("Azimuth")
nameItem.setEditable(False)
valueItem = LightAzimuthAngleItem(self._light)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Altitude')
+ nameItem = qt.QStandardItem("Altitude")
nameItem.setEditable(False)
valueItem = LightAltitudeAngleItem(self._light)
self.appendRow([nameItem, valueItem])
@@ -537,7 +531,8 @@ class BoundingBoxItem(SubjectItem):
Item is checkable.
"""
- itemName = 'Bounding Box'
+
+ itemName = "Bounding Box"
def _init(self):
visible = self.subject.isBoundingBoxVisible()
@@ -545,7 +540,7 @@ class BoundingBoxItem(SubjectItem):
self.setCheckState(qt.Qt.Checked if visible else qt.Qt.Unchecked)
def leftClicked(self):
- checked = (self.checkState() == qt.Qt.Checked)
+ checked = self.checkState() == qt.Qt.Checked
if checked != self.subject.isBoundingBoxVisible():
self.subject.setBoundingBoxVisible(checked)
@@ -555,7 +550,8 @@ class OrientationIndicatorItem(SubjectItem):
Item is checkable.
"""
- itemName = 'Axes indicator'
+
+ itemName = "Axes indicator"
def _init(self):
plot3d = self.subject.getPlot3DWidget()
@@ -565,7 +561,7 @@ class OrientationIndicatorItem(SubjectItem):
def leftClicked(self):
plot3d = self.subject.getPlot3DWidget()
- checked = (self.checkState() == qt.Qt.Checked)
+ checked = self.checkState() == qt.Qt.Checked
if checked != plot3d.isOrientationIndicatorVisible():
plot3d.setOrientationIndicatorVisible(checked)
@@ -574,28 +570,30 @@ class ViewSettingsItem(qt.QStandardItem):
"""Viewport settings"""
def __init__(self, subject, *args):
-
super(ViewSettingsItem, self).__init__(*args)
self.setEditable(False)
- classes = (BackgroundColorItem,
- ForegroundColorItem,
- HighlightColorItem,
- BoundingBoxItem,
- OrientationIndicatorItem)
+ classes = (
+ BackgroundColorItem,
+ ForegroundColorItem,
+ HighlightColorItem,
+ BoundingBoxItem,
+ OrientationIndicatorItem,
+ )
for cls in classes:
titleItem = qt.QStandardItem(cls.itemName)
titleItem.setEditable(False)
self.appendRow([titleItem, cls(subject)])
- nameItem = DirectionalLightGroup(subject, 'Light Direction')
+ nameItem = DirectionalLightGroup(subject, "Light Direction")
valueItem = qt.QStandardItem()
self.appendRow([nameItem, valueItem])
# Data information ############################################################
+
class DataChangedItem(SubjectItem):
"""
Base class for items listening to ScalarFieldView.sigDataChanged
@@ -612,42 +610,41 @@ class DataChangedItem(SubjectItem):
class DataTypeItem(DataChangedItem):
- itemName = 'dtype'
+ itemName = "dtype"
def _pullData(self):
data = self.subject.getData(copy=False)
- return ((data is not None) and str(data.dtype)) or 'N/A'
+ return ((data is not None) and str(data.dtype)) or "N/A"
class DataShapeItem(DataChangedItem):
- itemName = 'size'
+ itemName = "size"
def _pullData(self):
data = self.subject.getData(copy=False)
if data is None:
- return 'N/A'
+ return "N/A"
else:
return str(list(reversed(data.shape)))
class OffsetItem(DataChangedItem):
- itemName = 'offset'
+ itemName = "offset"
def _pullData(self):
offset = self.subject.getTranslation()
- return ((offset is not None) and str(offset)) or 'N/A'
+ return ((offset is not None) and str(offset)) or "N/A"
class ScaleItem(DataChangedItem):
- itemName = 'scale'
+ itemName = "scale"
def _pullData(self):
scale = self.subject.getScale()
- return ((scale is not None) and str(scale)) or 'N/A'
+ return ((scale is not None) and str(scale)) or "N/A"
class MatrixItem(DataChangedItem):
-
def __init__(self, subject, row, *args):
self.__row = row
super(MatrixItem, self).__init__(subject, *args)
@@ -658,9 +655,7 @@ class MatrixItem(DataChangedItem):
class DataSetItem(qt.QStandardItem):
-
def __init__(self, subject, *args):
-
super(DataSetItem, self).__init__(*args)
self.setEditable(False)
@@ -671,7 +666,7 @@ class DataSetItem(qt.QStandardItem):
titleItem.setEditable(False)
self.appendRow([titleItem, klass(subject)])
- matrixItem = qt.QStandardItem('matrix')
+ matrixItem = qt.QStandardItem("matrix")
matrixItem.setEditable(False)
valueItem = qt.QStandardItem()
self.appendRow([matrixItem, valueItem])
@@ -689,6 +684,7 @@ class DataSetItem(qt.QStandardItem):
# Isosurface ##################################################################
+
class IsoSurfaceRootItem(SubjectItem):
"""
Root (i.e : column index 0) Isosurface item.
@@ -700,8 +696,7 @@ class IsoSurfaceRootItem(SubjectItem):
def getSignals(self):
subject = self.subject
- return [subject.sigColorChanged,
- subject.sigVisibilityChanged]
+ return [subject.sigColorChanged, subject.sigVisibilityChanged]
def _subjectChanged(self, signalIdx=None, args=None, kwargs=None):
if signalIdx == 0:
@@ -720,17 +715,18 @@ class IsoSurfaceRootItem(SubjectItem):
self.setData(color, qt.Qt.DecorationRole)
self.setCheckState((visible and qt.Qt.Checked) or qt.Qt.Unchecked)
- nameItem = qt.QStandardItem('Level')
- sliderItem = IsoSurfaceLevelSlider(self.subject,
- self._isoLevelSliderNormalization)
+ nameItem = qt.QStandardItem("Level")
+ sliderItem = IsoSurfaceLevelSlider(
+ self.subject, self._isoLevelSliderNormalization
+ )
self.appendRow([nameItem, sliderItem])
- nameItem = qt.QStandardItem('Color')
+ nameItem = qt.QStandardItem("Color")
nameItem.setEditable(False)
valueItem = IsoSurfaceColorItem(self.subject)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Opacity')
+ nameItem = qt.QStandardItem("Opacity")
nameItem.setTextAlignment(qt.Qt.AlignLeft | qt.Qt.AlignTop)
nameItem.setEditable(False)
valueItem = IsoSurfaceAlphaItem(self.subject)
@@ -744,10 +740,12 @@ class IsoSurfaceRootItem(SubjectItem):
def queryRemove(self, view=None):
buttons = qt.QMessageBox.Ok | qt.QMessageBox.Cancel
- ans = qt.QMessageBox.question(view,
- 'Remove isosurface',
- 'Remove the selected iso-surface?',
- buttons=buttons)
+ ans = qt.QMessageBox.question(
+ view,
+ "Remove isosurface",
+ "Remove the selected iso-surface?",
+ buttons=buttons,
+ )
if ans == qt.QMessageBox.Ok:
sfview = self.subject.parent()
if sfview:
@@ -756,7 +754,7 @@ class IsoSurfaceRootItem(SubjectItem):
return False
def leftClicked(self):
- checked = (self.checkState() == qt.Qt.Checked)
+ checked = self.checkState() == qt.Qt.Checked
visible = self.subject.isVisible()
if checked != visible:
self.subject.setVisible(checked)
@@ -766,12 +764,12 @@ class IsoSurfaceLevelItem(SubjectItem):
"""
Base class for the isosurface level items.
"""
+
editable = True
def getSignals(self):
subject = self.subject
- return [subject.sigLevelChanged,
- subject.sigVisibilityChanged]
+ return [subject.sigLevelChanged, subject.sigVisibilityChanged]
def getEditor(self, parent, option, index):
return FloatEdit(parent)
@@ -799,15 +797,14 @@ class _IsoLevelSlider(qt.QSlider):
super(_IsoLevelSlider, self).__init__(parent=parent)
self.subject = subject
- if normalization == 'arcsinh':
+ if normalization == "arcsinh":
self.__norm = numpy.arcsinh
self.__invNorm = numpy.sinh
- elif normalization == 'linear':
+ elif normalization == "linear":
self.__norm = lambda x: x
self.__invNorm = lambda x: x
else:
- raise ValueError(
- "Unsupported normalization %s", normalization)
+ raise ValueError("Unsupported normalization %s", normalization)
self.sliderReleased.connect(self.__sliderReleased)
@@ -848,6 +845,7 @@ class IsoSurfaceLevelSlider(IsoSurfaceLevelItem):
"""
Isosurface level item with a slider editor.
"""
+
nTicks = 1000
persistent = True
@@ -877,6 +875,7 @@ class IsoSurfaceColorItem(SubjectItem):
"""
Isosurface color item.
"""
+
editable = True
persistent = True
@@ -889,8 +888,7 @@ class IsoSurfaceColorItem(SubjectItem):
color.setAlpha(255)
editor.color = color
# Wrapping call in lambda is a workaround for PySide with Python 3
- editor.sigColorChanged.connect(
- lambda color: self.__editorChanged(color))
+ editor.sigColorChanged.connect(lambda color: self.__editorChanged(color))
return editor
def __editorChanged(self, color):
@@ -906,6 +904,7 @@ class QColorEditor(qt.QWidget):
"""
QColor editor.
"""
+
sigColorChanged = qt.Signal(object)
color = property(lambda self: qt.QColor(self.__color))
@@ -941,7 +940,7 @@ class QColorEditor(qt.QWidget):
def __showColorDialog(self):
dialog = qt.QColorDialog(parent=self)
- if sys.platform == 'darwin':
+ if sys.platform == "darwin":
# Use of native color dialog on macos might cause problems
dialog.setOption(qt.QColorDialog.DontUseNativeDialog, True)
@@ -967,6 +966,7 @@ class IsoSurfaceAlphaItem(SubjectItem):
"""
Isosurface alpha item.
"""
+
editable = True
persistent = True
@@ -986,8 +986,7 @@ class IsoSurfaceAlphaItem(SubjectItem):
editor.setValue(color.alpha())
# Wrapping call in lambda is a workaround for PySide with Python 3
- editor.valueChanged.connect(
- lambda value: self.__editorChanged(value))
+ editor.valueChanged.connect(lambda value: self.__editorChanged(value))
return editor
@@ -1013,9 +1012,9 @@ class IsoSurfaceAlphaLegendItem(SubjectItem):
layout = qt.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
- layout.addWidget(qt.QLabel('0'))
+ layout.addWidget(qt.QLabel("0"))
layout.addStretch(1)
- layout.addWidget(qt.QLabel('1'))
+ layout.addWidget(qt.QLabel("1"))
editor = qt.QWidget(parent)
editor.setLayout(layout)
@@ -1036,7 +1035,6 @@ class IsoSurfaceCount(SubjectItem):
class IsoSurfaceAddRemoveWidget(qt.QWidget):
-
sigViewTask = qt.Signal(str)
"""Signal for the tree view to perform some task"""
@@ -1048,13 +1046,13 @@ class IsoSurfaceAddRemoveWidget(qt.QWidget):
layout.setSpacing(0)
addBtn = qt.QToolButton(self)
- addBtn.setText('+')
+ addBtn.setText("+")
addBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
layout.addWidget(addBtn)
addBtn.clicked.connect(self.__addClicked)
removeBtn = qt.QToolButton(self)
- removeBtn.setText('-')
+ removeBtn.setText("-")
removeBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
layout.addWidget(removeBtn)
removeBtn.clicked.connect(self.__removeClicked)
@@ -1069,17 +1067,17 @@ class IsoSurfaceAddRemoveWidget(qt.QWidget):
if dataRange is None:
dataRange = [0, 1]
- sfview.addIsosurface(
- numpy.mean((dataRange[0], dataRange[-1])), '#0000FF')
+ sfview.addIsosurface(numpy.mean((dataRange[0], dataRange[-1])), "#0000FF")
def __removeClicked(self):
- self.sigViewTask.emit('remove_iso')
+ self.sigViewTask.emit("remove_iso")
class IsoSurfaceAddRemoveItem(SubjectItem):
"""
Item displaying a simple QToolButton allowing to add an isosurface.
"""
+
persistent = True
def getEditor(self, parent, option, index):
@@ -1104,30 +1102,30 @@ class IsoSurfaceGroup(SubjectItem):
if len(args) >= 1:
isosurface = args[0]
if not isinstance(isosurface, Isosurface):
- raise ValueError('Expected an isosurface instance.')
+ raise ValueError("Expected an isosurface instance.")
self.__addIsosurface(isosurface)
else:
- raise ValueError('Expected an isosurface instance.')
+ raise ValueError("Expected an isosurface instance.")
elif signalIdx == 1:
if len(args) >= 1:
isosurface = args[0]
if not isinstance(isosurface, Isosurface):
- raise ValueError('Expected an isosurface instance.')
+ raise ValueError("Expected an isosurface instance.")
self.__removeIsosurface(isosurface)
else:
- raise ValueError('Expected an isosurface instance.')
+ raise ValueError("Expected an isosurface instance.")
def __addIsosurface(self, isosurface):
valueItem = IsoSurfaceRootItem(
- subject=isosurface,
- normalization=self._isoLevelSliderNormalization)
+ subject=isosurface, normalization=self._isoLevelSliderNormalization
+ )
nameItem = IsoSurfaceLevelItem(subject=isosurface)
self.insertRow(max(0, self.rowCount() - 1), [valueItem, nameItem])
def __removeIsosurface(self, isosurface):
for row in range(self.rowCount()):
child = self.child(row)
- subject = getattr(child, 'subject', None)
+ subject = getattr(child, "subject", None)
if subject == isosurface:
self.takeRow(row)
break
@@ -1146,6 +1144,7 @@ class IsoSurfaceGroup(SubjectItem):
# Cutting Plane ###############################################################
+
class ColormapBase(SubjectItem):
"""
Mixin class for colormap items.
@@ -1160,6 +1159,7 @@ class PlaneMinRangeItem(ColormapBase):
colormap minVal item.
Editor is a QLineEdit with a QDoubleValidator
"""
+
editable = True
def _pullData(self):
@@ -1200,6 +1200,7 @@ class PlaneMaxRangeItem(ColormapBase):
colormap maxVal item.
Editor is a QLineEdit with a QDoubleValidator
"""
+
editable = True
def _pullData(self):
@@ -1236,27 +1237,39 @@ class PlaneOrientationItem(SubjectItem):
Plane orientation item.
Editor is a QComboBox.
"""
+
editable = True
_PLANE_ACTIONS = (
- ('3d-plane-normal-x', 'Plane 0',
- 'Set plane perpendicular to red axis', (1., 0., 0.)),
- ('3d-plane-normal-y', 'Plane 1',
- 'Set plane perpendicular to green axis', (0., 1., 0.)),
- ('3d-plane-normal-z', 'Plane 2',
- 'Set plane perpendicular to blue axis', (0., 0., 1.)),
+ (
+ "3d-plane-normal-x",
+ "Plane 0",
+ "Set plane perpendicular to red axis",
+ (1.0, 0.0, 0.0),
+ ),
+ (
+ "3d-plane-normal-y",
+ "Plane 1",
+ "Set plane perpendicular to green axis",
+ (0.0, 1.0, 0.0),
+ ),
+ (
+ "3d-plane-normal-z",
+ "Plane 2",
+ "Set plane perpendicular to blue axis",
+ (0.0, 0.0, 1.0),
+ ),
)
def getSignals(self):
return [self.subject.getCutPlanes()[0].sigPlaneChanged]
def _pullData(self):
- currentNormal = self.subject.getCutPlanes()[0].getNormal(
- coordinates='scene')
+ currentNormal = self.subject.getCutPlanes()[0].getNormal(coordinates="scene")
for _, text, _, normal in self._PLANE_ACTIONS:
if numpy.allclose(normal, currentNormal):
return text
- return ''
+ return ""
def getEditor(self, parent, option, index):
editor = qt.QComboBox(parent)
@@ -1265,13 +1278,14 @@ class PlaneOrientationItem(SubjectItem):
# Wrapping call in lambda is a workaround for PySide with Python 3
editor.currentIndexChanged[int].connect(
- lambda index: self.__editorChanged(index))
+ lambda index: self.__editorChanged(index)
+ )
return editor
def __editorChanged(self, index):
normal = self._PLANE_ACTIONS[index][3]
plane = self.subject.getCutPlanes()[0]
- plane.setNormal(normal, coordinates='scene')
+ plane.setNormal(normal, coordinates="scene")
plane.moveToCenter()
def setEditorData(self, editor):
@@ -1298,7 +1312,8 @@ class PlaneInterpolationItem(SubjectItem):
interpolation = self.subject.getCutPlanes()[0].getInterpolation()
self.setCheckable(True)
self.setCheckState(
- qt.Qt.Checked if interpolation == 'linear' else qt.Qt.Unchecked)
+ qt.Qt.Checked if interpolation == "linear" else qt.Qt.Unchecked
+ )
self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)
def getSignals(self):
@@ -1306,7 +1321,7 @@ class PlaneInterpolationItem(SubjectItem):
def leftClicked(self):
checked = self.checkState() == qt.Qt.Checked
- self._setInterpolation('linear' if checked else 'nearest')
+ self._setInterpolation("linear" if checked else "nearest")
def _pullData(self):
interpolation = self.subject.getCutPlanes()[0].getInterpolation()
@@ -1326,8 +1341,7 @@ class PlaneDisplayBelowMinItem(SubjectItem):
def _init(self):
display = self.subject.getCutPlanes()[0].getDisplayValuesBelowMin()
self.setCheckable(True)
- self.setCheckState(
- qt.Qt.Checked if display else qt.Qt.Unchecked)
+ self.setCheckState(qt.Qt.Checked if display else qt.Qt.Unchecked)
self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)
def getSignals(self):
@@ -1351,12 +1365,21 @@ class PlaneColormapItem(ColormapBase):
colormap name item.
Editor is a QComboBox
"""
+
editable = True
- listValues = ['gray', 'reversed gray',
- 'temperature', 'red',
- 'green', 'blue',
- 'viridis', 'magma', 'inferno', 'plasma']
+ listValues = [
+ "gray",
+ "reversed gray",
+ "temperature",
+ "red",
+ "green",
+ "blue",
+ "viridis",
+ "magma",
+ "inferno",
+ "plasma",
+ ]
def getEditor(self, parent, option, index):
editor = qt.QComboBox(parent)
@@ -1364,7 +1387,8 @@ class PlaneColormapItem(ColormapBase):
# Wrapping call in lambda is a workaround for PySide with Python 3
editor.currentIndexChanged[int].connect(
- lambda index: self.__editorChanged(index))
+ lambda index: self.__editorChanged(index)
+ )
return editor
@@ -1378,7 +1402,7 @@ class PlaneColormapItem(ColormapBase):
try:
index = self.listValues.index(colormapName)
except ValueError:
- _logger.error('Unsupported colormap: %s', colormapName)
+ _logger.error("Unsupported colormap: %s", colormapName)
else:
editor.setCurrentIndex(index)
return True
@@ -1400,12 +1424,13 @@ class PlaneAutoScaleItem(ColormapBase):
def _init(self):
colorMap = self.subject.getCutPlanes()[0].getColormap()
self.setCheckable(True)
- self.setCheckState((colorMap.isAutoscale() and qt.Qt.Checked)
- or qt.Qt.Unchecked)
+ self.setCheckState(
+ (colorMap.isAutoscale() and qt.Qt.Checked) or qt.Qt.Unchecked
+ )
self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)
def leftClicked(self):
- checked = (self.checkState() == qt.Qt.Checked)
+ checked = self.checkState() == qt.Qt.Checked
self._setAutoScale(checked)
def _setAutoScale(self, auto):
@@ -1427,9 +1452,9 @@ class PlaneAutoScaleItem(ColormapBase):
auto = self.subject.getCutPlanes()[0].getColormap().isAutoscale()
self._setAutoScale(auto)
if auto:
- data = 'Auto'
+ data = "Auto"
else:
- data = 'User'
+ data = "User"
return data
@@ -1438,6 +1463,7 @@ class NormalizationNode(ColormapBase):
colormap normalization item.
Item is a QComboBox.
"""
+
editable = True
listValues = list(Colormap.NORMALIZATIONS)
@@ -1447,17 +1473,20 @@ class NormalizationNode(ColormapBase):
# Wrapping call in lambda is a workaround for PySide with Python 3
editor.currentIndexChanged[int].connect(
- lambda index: self.__editorChanged(index))
+ lambda index: self.__editorChanged(index)
+ )
return editor
def __editorChanged(self, index):
colorMap = self.subject.getCutPlanes()[0].getColormap()
normalization = self.listValues[index]
- self.subject.getCutPlanes()[0].setColormap(name=colorMap.getName(),
- norm=normalization,
- vmin=colorMap.getVMin(),
- vmax=colorMap.getVMax())
+ self.subject.getCutPlanes()[0].setColormap(
+ name=colorMap.getName(),
+ norm=normalization,
+ vmin=colorMap.getVMin(),
+ vmax=colorMap.getVMax(),
+ )
def setEditorData(self, editor):
normalization = self.subject.getCutPlanes()[0].getColormap().getNormalization()
@@ -1477,48 +1506,49 @@ class PlaneGroup(SubjectItem):
"""
Root Item for the plane items.
"""
+
def _init(self):
valueItem = qt.QStandardItem()
valueItem.setEditable(False)
- nameItem = PlaneVisibleItem(self.subject, 'Visible')
+ nameItem = PlaneVisibleItem(self.subject, "Visible")
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Colormap')
+ nameItem = qt.QStandardItem("Colormap")
nameItem.setEditable(False)
valueItem = PlaneColormapItem(self.subject)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Normalization')
+ nameItem = qt.QStandardItem("Normalization")
nameItem.setEditable(False)
valueItem = NormalizationNode(self.subject)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Orientation')
+ nameItem = qt.QStandardItem("Orientation")
nameItem.setEditable(False)
valueItem = PlaneOrientationItem(self.subject)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Interpolation')
+ nameItem = qt.QStandardItem("Interpolation")
nameItem.setEditable(False)
valueItem = PlaneInterpolationItem(self.subject)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Autoscale')
+ nameItem = qt.QStandardItem("Autoscale")
nameItem.setEditable(False)
valueItem = PlaneAutoScaleItem(self.subject)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Min')
+ nameItem = qt.QStandardItem("Min")
nameItem.setEditable(False)
valueItem = PlaneMinRangeItem(self.subject)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Max')
+ nameItem = qt.QStandardItem("Max")
nameItem.setEditable(False)
valueItem = PlaneMaxRangeItem(self.subject)
self.appendRow([nameItem, valueItem])
- nameItem = qt.QStandardItem('Values<=Min')
+ nameItem = qt.QStandardItem("Values<=Min")
nameItem.setEditable(False)
valueItem = PlaneDisplayBelowMinItem(self.subject)
self.appendRow([nameItem, valueItem])
@@ -1529,15 +1559,15 @@ class PlaneVisibleItem(SubjectItem):
Plane visibility item.
Item is checkable.
"""
+
def _init(self):
plane = self.subject.getCutPlanes()[0]
self.setCheckable(True)
- self.setCheckState((plane.isVisible() and qt.Qt.Checked)
- or qt.Qt.Unchecked)
+ self.setCheckState((plane.isVisible() and qt.Qt.Checked) or qt.Qt.Unchecked)
def leftClicked(self):
plane = self.subject.getCutPlanes()[0]
- checked = (self.checkState() == qt.Qt.Checked)
+ checked = self.checkState() == qt.Qt.Checked
if checked != plane.isVisible():
plane.setVisible(checked)
if plane.isVisible():
@@ -1546,6 +1576,7 @@ class PlaneVisibleItem(SubjectItem):
# Tree ########################################################################
+
class ItemDelegate(qt.QStyledItemDelegate):
"""
Delegate for the QTreeView filled with SubjectItems.
@@ -1563,13 +1594,11 @@ class ItemDelegate(qt.QStyledItemDelegate):
editor = item.getEditor(parent, option, index)
if editor:
editor.setAutoFillBackground(True)
- if hasattr(editor, 'sigViewTask'):
+ if hasattr(editor, "sigViewTask"):
editor.sigViewTask.connect(self.__viewTask)
return editor
- editor = super(ItemDelegate, self).createEditor(parent,
- option,
- index)
+ editor = super(ItemDelegate, self).createEditor(parent, option, index)
return editor
def updateEditorGeometry(self, editor, option, index):
@@ -1600,7 +1629,7 @@ class TreeView(qt.QTreeView):
def __init__(self, parent=None):
super(TreeView, self).__init__(parent)
self.__openedIndex = None
- self._isoLevelSliderNormalization = 'linear'
+ self._isoLevelSliderNormalization = "linear"
self.setIconSize(qt.QSize(16, 16))
@@ -1623,26 +1652,30 @@ class TreeView(qt.QTreeView):
"""
model = qt.QStandardItemModel()
model.setColumnCount(ModelColumns.ColumnMax)
- model.setHorizontalHeaderLabels(['Name', 'Value'])
+ model.setHorizontalHeaderLabels(["Name", "Value"])
item = qt.QStandardItem()
item.setEditable(False)
- model.appendRow([ViewSettingsItem(sfView, 'Style'), item])
+ model.appendRow([ViewSettingsItem(sfView, "Style"), item])
item = qt.QStandardItem()
item.setEditable(False)
- model.appendRow([DataSetItem(sfView, 'Data'), item])
+ model.appendRow([DataSetItem(sfView, "Data"), item])
item = IsoSurfaceCount(sfView)
item.setEditable(False)
- model.appendRow([IsoSurfaceGroup(sfView,
- self._isoLevelSliderNormalization,
- 'Isosurfaces'),
- item])
+ model.appendRow(
+ [
+ IsoSurfaceGroup(
+ sfView, self._isoLevelSliderNormalization, "Isosurfaces"
+ ),
+ item,
+ ]
+ )
item = qt.QStandardItem()
item.setEditable(False)
- model.appendRow([PlaneGroup(sfView, 'Cutting Plane'), item])
+ model.appendRow([PlaneGroup(sfView, "Cutting Plane"), item])
self.setModel(model)
@@ -1688,21 +1721,24 @@ class TreeView(qt.QTreeView):
meth = self.closePersistentEditor
curParent = parent
- children = [model.index(row, 0, curParent)
- for row in range(model.rowCount(curParent))]
+ children = [
+ model.index(row, 0, curParent) for row in range(model.rowCount(curParent))
+ ]
columnCount = model.columnCount()
while len(children) > 0:
curParent = children.pop(-1)
- children.extend([model.index(row, 0, curParent)
- for row in range(model.rowCount(curParent))])
+ children.extend(
+ [
+ model.index(row, 0, curParent)
+ for row in range(model.rowCount(curParent))
+ ]
+ )
for colIdx in range(columnCount):
- sibling = model.sibling(curParent.row(),
- colIdx,
- curParent)
+ sibling = model.sibling(curParent.row(), colIdx, curParent)
item = model.itemFromIndex(sibling)
if isinstance(item, SubjectItem) and item.persistent:
meth(sibling)
@@ -1784,9 +1820,8 @@ class TreeView(qt.QTreeView):
parentItem.removeRow(iso.row())
else:
qt.QMessageBox.information(
- self,
- 'Remove isosurface',
- 'Select an iso-surface to remove it')
+ self, "Remove isosurface", "Select an iso-surface to remove it"
+ )
def __clicked(self, index):
"""
@@ -1800,7 +1835,7 @@ class TreeView(qt.QTreeView):
item.leftClicked()
def __delegateEvent(self, task):
- if task == 'remove_iso':
+ if task == "remove_iso":
self.__removeIsosurfaces()
def setIsoLevelSliderNormalization(self, normalization):
@@ -1810,5 +1845,5 @@ class TreeView(qt.QTreeView):
:param str normalization: Either 'linear' or 'arcsinh'
"""
- assert normalization in ('linear', 'arcsinh')
+ assert normalization in ("linear", "arcsinh")
self._isoLevelSliderNormalization = normalization
diff --git a/src/silx/gui/plot3d/ScalarFieldView.py b/src/silx/gui/plot3d/ScalarFieldView.py
index b2bb254..e1d34fd 100644
--- a/src/silx/gui/plot3d/ScalarFieldView.py
+++ b/src/silx/gui/plot3d/ScalarFieldView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2020 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ It supports iso-surfaces, a cutting plane and the definition of
a region of interest.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "14/06/2018"
@@ -79,9 +76,9 @@ class Isosurface(qt.QObject):
def __init__(self, parent):
super(Isosurface, self).__init__(parent=parent)
- self._level = float('nan')
+ self._level = float("nan")
self._autoLevelFunction = None
- self._color = rgba('#FFD700FF')
+ self._color = rgba("#FFD700FF")
self._data = None
self._group = scene.Group()
@@ -94,7 +91,7 @@ class Isosurface(qt.QObject):
if data is None:
self._data = None
else:
- self._data = numpy.array(data, copy=copy, order='C')
+ self._data = numpy.array(data, copy=copy, order="C")
self._update()
@@ -170,7 +167,7 @@ class Isosurface(qt.QObject):
if color != self._color:
self._color = color
if len(self._group.children) != 0:
- self._group.children[0].setAttribute('color', self._color)
+ self._group.children[0].setAttribute("color", self._color)
self.sigColorChanged.emit()
def _update(self):
@@ -179,7 +176,7 @@ class Isosurface(qt.QObject):
if self._data is None:
if self.isAutoLevel():
- self._level = float('nan')
+ self._level = float("nan")
else:
if self.isAutoLevel():
@@ -194,12 +191,12 @@ class Isosurface(qt.QObject):
"Error while executing iso level function %s.%s",
module,
name,
- exc_info=True)
- level = float('nan')
+ exc_info=True,
+ )
+ level = float("nan")
else:
- _logger.info(
- 'Computed iso-level in %f s.', time.time() - st)
+ _logger.info("Computed iso-level in %f s.", time.time() - st)
if level != self._level:
self._level = level
@@ -209,19 +206,19 @@ class Isosurface(qt.QObject):
return
st = time.time()
- vertices, normals, indices = MarchingCubes(
- self._data,
- isolevel=self._level)
- _logger.info('Computed iso-surface in %f s.', time.time() - st)
+ vertices, normals, indices = MarchingCubes(self._data, isolevel=self._level)
+ _logger.info("Computed iso-surface in %f s.", time.time() - st)
if len(vertices) == 0:
return
else:
- mesh = primitives.Mesh3D(vertices,
- colors=self._color,
- normals=normals,
- mode='triangles',
- indices=indices)
+ mesh = primitives.Mesh3D(
+ vertices,
+ colors=self._color,
+ normals=normals,
+ mode="triangles",
+ indices=indices,
+ )
self._group.children = [mesh]
@@ -236,9 +233,9 @@ class SelectedRegion(object):
:param scale: Scale from array to data coordinates (sx, sy, sz)
"""
- def __init__(self, arrayRange, dataBBox,
- translation=(0., 0., 0.),
- scale=(1., 1., 1.)):
+ def __init__(
+ self, arrayRange, dataBBox, translation=(0.0, 0.0, 0.0), scale=(1.0, 1.0, 1.0)
+ ):
self._arrayRange = numpy.array(arrayRange, copy=True, dtype=numpy.int64)
assert self._arrayRange.shape == (3, 2)
assert numpy.all(self._arrayRange[:, 1] >= self._arrayRange[:, 0])
@@ -264,9 +261,11 @@ class SelectedRegion(object):
:return: A numpy array with (zslice, yslice, zslice)
:rtype: numpy.ndarray
"""
- return (slice(*self._arrayRange[0]),
- slice(*self._arrayRange[1]),
- slice(*self._arrayRange[2]))
+ return (
+ slice(*self._arrayRange[0]),
+ slice(*self._arrayRange[1]),
+ slice(*self._arrayRange[2]),
+ )
def getDataRange(self):
"""Range in the data coordinates of the selection: 3x2 array of float
@@ -351,12 +350,13 @@ class CutPlane(qt.QObject):
# Plane with texture on the data bounding box
self._dataPlane = cutplane.CutPlane(normal=(0, 1, 0))
self._dataPlane.strokeVisible = False
- self._dataPlane.alpha = 1.
+ self._dataPlane.alpha = 1.0
self._dataPlane.visible = self._visible
self._dataPlane.plane.addListener(self._planePositionChanged)
self._colormap = Colormap(
- name='gray', normalization='linear', vmin=None, vmax=None)
+ name="gray", normalization="linear", vmin=None, vmax=None
+ )
self.getColormap().sigChanged.connect(self._colormapChanged)
self._updateSceneColormap()
@@ -372,8 +372,8 @@ class CutPlane(qt.QObject):
bounds = self._planeStroke.parent.bounds(dataBounds=True)
if bounds is not None:
self._planeStroke.plane.point = numpy.clip(
- self._planeStroke.plane.point,
- a_min=bounds[0], a_max=bounds[1])
+ self._planeStroke.plane.point, a_min=bounds[0], a_max=bounds[1]
+ )
@staticmethod
def _syncPlanes(master, slave):
@@ -382,14 +382,12 @@ class CutPlane(qt.QObject):
:param PlaneInGroup master: Reference PlaneInGroup
:param PlaneInGroup slave: PlaneInGroup to align
"""
- masterToSlave = transform.StaticTransformList([
- slave.objectToSceneTransform.inverse(),
- master.objectToSceneTransform])
-
- point = masterToSlave.transformPoint(
- master.plane.point)
- normal = masterToSlave.transformNormal(
- master.plane.normal)
+ masterToSlave = transform.StaticTransformList(
+ [slave.objectToSceneTransform.inverse(), master.objectToSceneTransform]
+ )
+
+ point = masterToSlave.transformPoint(master.plane.point)
+ normal = masterToSlave.transformNormal(master.plane.normal)
slave.plane.setPlane(point, normal)
def _sfViewDataChanged(self):
@@ -410,8 +408,7 @@ class CutPlane(qt.QObject):
def _sfViewTransformChanged(self):
"""Handle transform changed in the ScalarFieldView"""
self._keepPlaneInBBox()
- self._syncPlanes(master=self._planeStroke,
- slave=self._dataPlane)
+ self._syncPlanes(master=self._planeStroke, slave=self._dataPlane)
self.sigPlaneChanged.emit()
def _planeChanged(self, source, *args, **kwargs):
@@ -426,14 +423,11 @@ class CutPlane(qt.QObject):
if self.__syncPlane:
self.__syncPlane = False
if source is self._planeStroke.plane:
- self._syncPlanes(master=self._planeStroke,
- slave=self._dataPlane)
+ self._syncPlanes(master=self._planeStroke, slave=self._dataPlane)
elif source is self._dataPlane.plane:
- self._syncPlanes(master=self._dataPlane,
- slave=self._planeStroke)
+ self._syncPlanes(master=self._dataPlane, slave=self._planeStroke)
else:
- _logger.error('Received an unknown object %s',
- str(source))
+ _logger.error("Received an unknown object %s", str(source))
if self._planeStroke.visible or self._dataPlane.visible:
self.sigPlaneChanged.emit()
@@ -450,7 +444,7 @@ class CutPlane(qt.QObject):
"""Returns whether the cut plane is defined or not (bool)"""
return self._planeStroke.isValid
- def _plane(self, coordinates='array'):
+ def _plane(self, coordinates="array"):
"""Returns the scene plane to set.
:param str coordinates: The coordinate system to use:
@@ -458,15 +452,14 @@ class CutPlane(qt.QObject):
:rtype: Plane
:raise ValueError: If coordinates is not correct
"""
- if coordinates == 'scene':
+ if coordinates == "scene":
return self._planeStroke.plane
- elif coordinates == 'array':
+ elif coordinates == "array":
return self._dataPlane.plane
else:
- raise ValueError(
- 'Unsupported coordinates: %s' % str(coordinates))
+ raise ValueError("Unsupported coordinates: %s" % str(coordinates))
- def getNormal(self, coordinates='array'):
+ def getNormal(self, coordinates="array"):
"""Returns the normal of the plane (as a unit vector)
:param str coordinates: The coordinate system to use:
@@ -477,7 +470,7 @@ class CutPlane(qt.QObject):
"""
return self._plane(coordinates).normal
- def setNormal(self, normal, coordinates='array'):
+ def setNormal(self, normal, coordinates="array"):
"""Set the normal of the plane.
:param normal: 3-tuple of float: nx, ny, nz
@@ -487,7 +480,7 @@ class CutPlane(qt.QObject):
"""
self._plane(coordinates).normal = normal
- def getPoint(self, coordinates='array'):
+ def getPoint(self, coordinates="array"):
"""Returns a point on the plane.
:param str coordinates: The coordinate system to use:
@@ -498,7 +491,7 @@ class CutPlane(qt.QObject):
"""
return self._plane(coordinates).point
- def setPoint(self, point, constraint=True, coordinates='array'):
+ def setPoint(self, point, constraint=True, coordinates="array"):
"""Set a point contained in the plane.
Warning: The plane might not intersect the bounding box of the data.
@@ -514,7 +507,7 @@ class CutPlane(qt.QObject):
if constraint:
self._keepPlaneInBBox()
- def getParameters(self, coordinates='array'):
+ def getParameters(self, coordinates="array"):
"""Returns the plane equation parameters: a*x + b*y + c*z + d = 0
:param str coordinates: The coordinate system to use:
@@ -525,7 +518,7 @@ class CutPlane(qt.QObject):
"""
return self._plane(coordinates).parameters
- def setParameters(self, parameters, constraint=True, coordinates='array'):
+ def setParameters(self, parameters, constraint=True, coordinates="array"):
"""Set the plane equation parameters: a*x + b*y + c*z + d = 0
Warning: The plane might not intersect the bounding box of the data.
@@ -647,11 +640,7 @@ class CutPlane(qt.QObject):
"""
return self._colormap
- def setColormap(self,
- name='gray',
- norm=None,
- vmin=None,
- vmax=None):
+ def setColormap(self, name="gray", norm=None, vmin=None, vmax=None):
"""Set the colormap to use.
By either providing a :class:`Colormap` object or
@@ -665,8 +654,9 @@ class CutPlane(qt.QObject):
:param float vmin: The minimum value of the range or None for autoscale
:param float vmax: The maximum value of the range or None for autoscale
"""
- _logger.debug('setColormap %s %s (%s, %s)',
- name, str(norm), str(vmin), str(vmax))
+ _logger.debug(
+ "setColormap %s %s (%s, %s)", name, str(norm), str(vmin), str(vmax)
+ )
self._colormap.sigChanged.disconnect(self._colormapChanged)
@@ -675,9 +665,10 @@ class CutPlane(qt.QObject):
self._colormap = name
else:
if norm is None:
- norm = 'linear'
+ norm = "linear"
self._colormap = Colormap(
- name=name, normalization=norm, vmin=vmin, vmax=vmax)
+ name=name, normalization=norm, vmin=vmin, vmax=vmax
+ )
self._colormap.sigChanged.connect(self._colormapChanged)
self._colormapChanged()
@@ -721,12 +712,12 @@ class _CutPlaneImage(object):
self._isValid = False
self._data = numpy.zeros((0, 0), dtype=numpy.float32)
self._index = 0
- self._xLabel = ''
- self._yLabel = ''
- self._normalLabel = ''
- self._scale = float('nan'), float('nan')
- self._translation = float('nan'), float('nan')
- self._position = float('nan')
+ self._xLabel = ""
+ self._yLabel = ""
+ self._normalLabel = ""
+ self._scale = float("nan"), float("nan")
+ self._translation = float("nan"), float("nan")
+ self._position = float("nan")
sfView = cutPlane.parent()
if not sfView or not cutPlane.isValid():
@@ -738,10 +729,10 @@ class _CutPlaneImage(object):
_logger.info("No data available")
return
- normal = cutPlane.getNormal(coordinates='array')
- point = cutPlane.getPoint(coordinates='array')
+ normal = cutPlane.getNormal(coordinates="array")
+ point = cutPlane.getPoint(coordinates="array")
- if numpy.linalg.norm(numpy.cross(normal, (1., 0., 0.))) < 0.0017:
+ if numpy.linalg.norm(numpy.cross(normal, (1.0, 0.0, 0.0))) < 0.0017:
if not 0 <= point[0] <= data.shape[2]:
_logger.info("Plane outside dataset")
return
@@ -749,7 +740,7 @@ class _CutPlaneImage(object):
slice_ = data[:, :, index]
xAxisIndex, yAxisIndex, normalAxisIndex = 1, 2, 0 # y, z, x
- elif numpy.linalg.norm(numpy.cross(normal, (0., 1., 0.))) < 0.0017:
+ elif numpy.linalg.norm(numpy.cross(normal, (0.0, 1.0, 0.0))) < 0.0017:
if not 0 <= point[1] <= data.shape[1]:
_logger.info("Plane outside dataset")
return
@@ -757,7 +748,7 @@ class _CutPlaneImage(object):
slice_ = numpy.transpose(data[:, index, :])
xAxisIndex, yAxisIndex, normalAxisIndex = 2, 0, 1 # z, x, y
- elif numpy.linalg.norm(numpy.cross(normal, (0., 0., 1.))) < 0.0017:
+ elif numpy.linalg.norm(numpy.cross(normal, (0.0, 0.0, 1.0))) < 0.0017:
if not 0 <= point[2] <= data.shape[0]:
_logger.info("Plane outside dataset")
return
@@ -765,8 +756,9 @@ class _CutPlaneImage(object):
slice_ = data[index, :, :]
xAxisIndex, yAxisIndex, normalAxisIndex = 0, 1, 2 # x, y, z
else:
- _logger.warning('Unsupported normal: (%f, %f, %f)',
- normal[0], normal[1], normal[2])
+ _logger.warning(
+ "Unsupported normal: (%f, %f, %f)", normal[0], normal[1], normal[2]
+ )
return
# Store cut plane image info
@@ -777,8 +769,11 @@ class _CutPlaneImage(object):
# Only store extra information when no transform matrix is set
# Otherwise this information can be meaningless
- if numpy.all(numpy.equal(sfView.getTransformMatrix(),
- numpy.identity(3, dtype=numpy.float32))):
+ if numpy.all(
+ numpy.equal(
+ sfView.getTransformMatrix(), numpy.identity(3, dtype=numpy.float32)
+ )
+ ):
labels = sfView.getAxesLabels()
self._xLabel = labels[xAxisIndex]
self._yLabel = labels[yAxisIndex]
@@ -790,8 +785,9 @@ class _CutPlaneImage(object):
translation = sfView.getTranslation()
self._translation = translation[xAxisIndex], translation[yAxisIndex]
- self._position = float(index * scale[normalAxisIndex] +
- translation[normalAxisIndex])
+ self._position = float(
+ index * scale[normalAxisIndex] + translation[normalAxisIndex]
+ )
def isValid(self):
"""Returns True if the cut plane image is defined (bool)"""
@@ -863,7 +859,8 @@ class ScalarFieldView(Plot3DWindow):
def __init__(self, parent=None):
super(ScalarFieldView, self).__init__(parent)
self._colormap = Colormap(
- name='gray', normalization='linear', vmin=None, vmax=None)
+ name="gray", normalization="linear", vmin=None, vmax=None
+ )
self._selectedRange = None
# Store iso-surfaces
@@ -872,35 +869,37 @@ class ScalarFieldView(Plot3DWindow):
# Transformations
self._dataScale = transform.Scale()
self._dataTranslate = transform.Translate()
- self._dataTransform = transform.Matrix() # default to identity
+ self._dataTransform = transform.Matrix() # default to identity
- self._foregroundColor = 1., 1., 1., 1.
- self._highlightColor = 0.7, 0.7, 0., 1.
+ self._foregroundColor = 1.0, 1.0, 1.0, 1.0
+ self._highlightColor = 0.7, 0.7, 0.0, 1.0
self._data = None
self._dataRange = None
self._group = primitives.BoundedGroup()
self._group.transforms = [
- self._dataTranslate, self._dataTransform, self._dataScale]
+ self._dataTranslate,
+ self._dataTransform,
+ self._dataScale,
+ ]
self._bbox = axes.LabelledAxes()
self._bbox.children = [self._group]
- self._outerScale = transform.Scale(1., 1., 1.)
+ self._outerScale = transform.Scale(1.0, 1.0, 1.0)
self._bbox.transforms = [self._outerScale]
self.getPlot3DWidget().viewport.scene.children.append(self._bbox)
self._selectionBox = primitives.Box()
self._selectionBox.strokeSmooth = False
- self._selectionBox.strokeWidth = 1.
+ self._selectionBox.strokeWidth = 1.0
# self._selectionBox.fillColor = 1., 1., 1., 0.3
# self._selectionBox.fillCulling = 'back'
self._selectionBox.visible = False
self._group.children.append(self._selectionBox)
self._cutPlane = CutPlane(sfView=self)
- self._cutPlane.sigVisibilityChanged.connect(
- self._planeVisibilityChanged)
+ self._cutPlane.sigVisibilityChanged.connect(self._planeVisibilityChanged)
planeStroke, dataPlane = self._cutPlane._get3DPrimitives()
self._bbox.children.append(planeStroke)
self._group.children.append(dataPlane)
@@ -908,13 +907,16 @@ class ScalarFieldView(Plot3DWindow):
self._isogroup = primitives.GroupDepthOffset()
self._isogroup.transforms = [
# Convert from z, y, x from marching cubes to x, y, z
- transform.Matrix((
- (0., 0., 1., 0.),
- (0., 1., 0., 0.),
- (1., 0., 0., 0.),
- (0., 0., 0., 1.))),
+ transform.Matrix(
+ (
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, 1.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ )
+ ),
# Offset to match cutting plane coords
- transform.Translate(0.5, 0.5, 0.5)
+ transform.Translate(0.5, 0.5, 0.5),
]
self._group.children.append(self._isogroup)
@@ -934,7 +936,7 @@ class ScalarFieldView(Plot3DWindow):
stream = qt.QDataStream(ioDevice)
- stream.writeString('<ScalarFieldView>')
+ stream.writeString("<ScalarFieldView>")
isoSurfaces = self.getIsosurfaces()
@@ -943,7 +945,7 @@ class ScalarFieldView(Plot3DWindow):
# TODO : delegate the serialization to the serialized items
# isosurfaces
if nIsoSurfaces:
- tagIn = '<IsoSurfaces nIso={0}>'.format(nIsoSurfaces)
+ tagIn = "<IsoSurfaces nIso={0}>".format(nIsoSurfaces)
stream.writeString(tagIn)
for surface in isoSurfaces:
@@ -954,16 +956,16 @@ class ScalarFieldView(Plot3DWindow):
stream.writeDouble(level)
stream.writeBool(visible)
- stream.writeString('</IsoSurfaces>')
+ stream.writeString("</IsoSurfaces>")
- stream.writeString('<Style>')
+ stream.writeString("<Style>")
background = self.getBackgroundColor()
foreground = self.getForegroundColor()
highlight = self.getHighlightColor()
stream << background << foreground << highlight
- stream.writeString('</Style>')
+ stream.writeString("</Style>")
- stream.writeString('</ScalarFieldView>')
+ stream.writeString("</ScalarFieldView>")
def loadConfig(self, ioDevice):
"""
@@ -975,14 +977,13 @@ class ScalarFieldView(Plot3DWindow):
tagStack = deque()
- tagInRegex = re.compile('<(?P<itemId>[^ /]*) *'
- '(?P<args>.*)>')
+ tagInRegex = re.compile("<(?P<itemId>[^ /]*) *" "(?P<args>.*)>")
- tagOutRegex = re.compile('</(?P<itemId>[^ ]*)>')
+ tagOutRegex = re.compile("</(?P<itemId>[^ ]*)>")
- tagRootInRegex = re.compile('<ScalarFieldView>')
+ tagRootInRegex = re.compile("<ScalarFieldView>")
- isoSurfaceArgsRegex = re.compile('nIso=(?P<nIso>[0-9]*)')
+ isoSurfaceArgsRegex = re.compile("nIso=(?P<nIso>[0-9]*)")
stream = qt.QDataStream(ioDevice)
@@ -991,26 +992,27 @@ class ScalarFieldView(Plot3DWindow):
if tagMatch is None:
# TODO : explicit error
- raise ValueError('Unknown data.')
+ raise ValueError("Unknown data.")
- itemId = 'ScalarFieldView'
+ itemId = "ScalarFieldView"
tagStack.append(itemId)
while True:
-
tag = stream.readString()
tagMatch = tagOutRegex.match(tag)
if tagMatch:
- closeId = tagMatch.groupdict()['itemId']
+ closeId = tagMatch.groupdict()["itemId"]
if closeId != itemId:
# TODO : explicit error
- raise ValueError('Unexpected closing tag {0} '
- '(expected {1})'
- ''.format(closeId, itemId))
+ raise ValueError(
+ "Unexpected closing tag {0} "
+ "(expected {1})"
+ "".format(closeId, itemId)
+ )
- if itemId == 'ScalarFieldView':
+ if itemId == "ScalarFieldView":
# reached end
break
else:
@@ -1022,23 +1024,24 @@ class ScalarFieldView(Plot3DWindow):
if tagMatch is None:
# TODO : explicit error
- raise ValueError('Unknown data.')
+ raise ValueError("Unknown data.")
tagStack.append(itemId)
matchDict = tagMatch.groupdict()
- itemId = matchDict['itemId']
+ itemId = matchDict["itemId"]
# TODO : delegate the deserialization to the serialized items
- if itemId == 'IsoSurfaces':
- argsMatch = isoSurfaceArgsRegex.match(matchDict['args'])
+ if itemId == "IsoSurfaces":
+ argsMatch = isoSurfaceArgsRegex.match(matchDict["args"])
if not argsMatch:
# TODO : explicit error
- raise ValueError('Failed to parse args "{0}".'
- ''.format(matchDict['args']))
+ raise ValueError(
+ 'Failed to parse args "{0}".' "".format(matchDict["args"])
+ )
argsDict = argsMatch.groupdict()
- nIso = int(argsDict['nIso'])
+ nIso = int(argsDict["nIso"])
if nIso:
for surface in self.getIsosurfaces():
self.removeIsosurface(surface)
@@ -1049,7 +1052,7 @@ class ScalarFieldView(Plot3DWindow):
visible = stream.readBool()
surface = self.addIsosurface(level, color=color)
surface.setVisible(visible)
- elif itemId == 'Style':
+ elif itemId == "Style":
background = qt.QColor()
foreground = qt.QColor()
highlight = qt.QColor()
@@ -1058,22 +1061,23 @@ class ScalarFieldView(Plot3DWindow):
self.setForegroundColor(foreground)
self.setHighlightColor(highlight)
else:
- raise ValueError('Unknown entry tag {0}.'
- ''.format(itemId))
+ raise ValueError("Unknown entry tag {0}." "".format(itemId))
def _initPanPlaneAction(self):
"""Creates and init the pan plane action"""
self._panPlaneAction = qt.QAction(self)
- self._panPlaneAction.setIcon(icons.getQIcon('3d-plane-pan'))
- self._panPlaneAction.setText('Pan plane')
+ self._panPlaneAction.setIcon(icons.getQIcon("3d-plane-pan"))
+ self._panPlaneAction.setText("Pan plane")
self._panPlaneAction.setCheckable(True)
self._panPlaneAction.setToolTip(
- 'Pan the cutting plane. Press <b>Ctrl</b> to rotate the scene.')
+ "Pan the cutting plane. Press <b>Ctrl</b> to rotate the scene."
+ )
self._panPlaneAction.setEnabled(False)
self._panPlaneAction.triggered[bool].connect(self._planeActionTriggered)
self.getPlot3DWidget().sigInteractiveModeChanged.connect(
- self._interactiveModeChanged)
+ self._interactiveModeChanged
+ )
toolbar = self.findChild(InteractiveModeToolBar)
if toolbar is not None:
@@ -1081,10 +1085,10 @@ class ScalarFieldView(Plot3DWindow):
def _planeActionTriggered(self, checked=False):
self._panPlaneAction.setChecked(True)
- self.setInteractiveMode('plane')
+ self.setInteractiveMode("plane")
def _interactiveModeChanged(self):
- self._panPlaneAction.setChecked(self.getInteractiveMode() == 'plane')
+ self._panPlaneAction.setChecked(self.getInteractiveMode() == "plane")
self._updateColors()
def _planeVisibilityChanged(self, visible):
@@ -1092,9 +1096,9 @@ class ScalarFieldView(Plot3DWindow):
if visible != self._panPlaneAction.isEnabled():
self._panPlaneAction.setEnabled(visible)
if visible:
- self.setInteractiveMode('plane')
+ self.setInteractiveMode("plane")
elif self._panPlaneAction.isChecked():
- self.setInteractiveMode('rotate')
+ self.setInteractiveMode("rotate")
def setInteractiveMode(self, mode):
"""Choose the current interaction.
@@ -1105,23 +1109,24 @@ class ScalarFieldView(Plot3DWindow):
return
sceneScale = self.getPlot3DWidget().viewport.scene.transforms[0]
- if mode == 'plane':
+ if mode == "plane":
mode = interaction.PanPlaneZoomOnWheelControl(
self.getPlot3DWidget().viewport,
self._cutPlane._get3DPrimitives()[0],
- mode='position',
+ mode="position",
orbitAroundCenter=False,
- scaleTransform=sceneScale)
+ scaleTransform=sceneScale,
+ )
self.getPlot3DWidget().setInteractiveMode(mode)
self._updateColors()
def getInteractiveMode(self):
- """Returns the current interaction mode, see :meth:`setInteractiveMode`
- """
- if isinstance(self.getPlot3DWidget().eventHandler,
- interaction.PanPlaneZoomOnWheelControl):
- return 'plane'
+ """Returns the current interaction mode, see :meth:`setInteractiveMode`"""
+ if isinstance(
+ self.getPlot3DWidget().eventHandler, interaction.PanPlaneZoomOnWheelControl
+ ):
+ return "plane"
else:
return self.getPlot3DWidget().getInteractiveMode()
@@ -1146,7 +1151,7 @@ class ScalarFieldView(Plot3DWindow):
self.centerScene()
else:
- data = numpy.array(data, copy=copy, dtype=numpy.float32, order='C')
+ data = numpy.array(data, copy=copy, dtype=numpy.float32, order="C")
assert data.ndim == 3
assert min(data.shape) >= 2
@@ -1163,7 +1168,7 @@ class ScalarFieldView(Plot3DWindow):
if dataRange is not None:
min_positive = dataRange.min_positive
if min_positive is None:
- min_positive = float('nan')
+ min_positive = float("nan")
dataRange = dataRange.minimum, min_positive, dataRange.maximum
self._dataRange = dataRange
@@ -1206,7 +1211,7 @@ class ScalarFieldView(Plot3DWindow):
# Transformations
- def setOuterScale(self, sx=1., sy=1., sz=1.):
+ def setOuterScale(self, sx=1.0, sy=1.0, sz=1.0):
"""Set the scale to apply to the whole scene including the axes.
This is useful when axis lengths in data space are really different.
@@ -1225,7 +1230,7 @@ class ScalarFieldView(Plot3DWindow):
"""
return self._outerScale.scale
- def setScale(self, sx=1., sy=1., sz=1.):
+ def setScale(self, sx=1.0, sy=1.0, sz=1.0):
"""Set the scale of the 3D scalar field (i.e., size of a voxel).
:param float sx: Scale factor along the X axis
@@ -1239,11 +1244,10 @@ class ScalarFieldView(Plot3DWindow):
self.centerScene() # Reset viewpoint
def getScale(self):
- """Returns the scales provided by :meth:`setScale` as a numpy.ndarray.
- """
+ """Returns the scales provided by :meth:`setScale` as a numpy.ndarray."""
return self._dataScale.scale
- def setTranslation(self, x=0., y=0., z=0.):
+ def setTranslation(self, x=0.0, y=0.0, z=0.0):
"""Set the translation of the origin of the data array in data coordinates.
:param float x: Offset of the data origin on the X axis
@@ -1257,8 +1261,7 @@ class ScalarFieldView(Plot3DWindow):
self.centerScene() # Reset viewpoint
def getTranslation(self):
- """Returns the offset set by :meth:`setTranslation` as a numpy.ndarray.
- """
+ """Returns the offset set by :meth:`setTranslation` as a numpy.ndarray."""
return self._dataTranslate.translation
def setTransformMatrix(self, matrix3x3):
@@ -1349,9 +1352,7 @@ class ScalarFieldView(Plot3DWindow):
:return: object describing the labels
"""
- return self._Labels((self._bbox.xlabel,
- self._bbox.ylabel,
- self._bbox.zlabel))
+ return self._Labels((self._bbox.xlabel, self._bbox.ylabel, self._bbox.zlabel))
# Colors
@@ -1359,7 +1360,7 @@ class ScalarFieldView(Plot3DWindow):
"""Update item depending on foreground/highlight color"""
self._bbox.tickColor = self._foregroundColor
self._selectionBox.strokeColor = self._foregroundColor
- if self.getInteractiveMode() == 'plane':
+ if self.getInteractiveMode() == "plane":
self._cutPlane.setStrokeColor(self._highlightColor)
self._bbox.color = self._foregroundColor
else:
@@ -1438,18 +1439,17 @@ class ScalarFieldView(Plot3DWindow):
elif None in (xrange_, yrange, zrange):
# One of the range is None and no data available
- raise RuntimeError(
- 'Data is not set, cannot get default range from it.')
+ raise RuntimeError("Data is not set, cannot get default range from it.")
# Clip selected region to data shape and make sure min <= max
- selectedRange = numpy.array((
- (max(0, min(*zrange)),
- min(self._data.shape[0], max(*zrange))),
- (max(0, min(*yrange)),
- min(self._data.shape[1], max(*yrange))),
- (max(0, min(*xrange_)),
- min(self._data.shape[2], max(*xrange_))),
- ), dtype=numpy.int64)
+ selectedRange = numpy.array(
+ (
+ (max(0, min(*zrange)), min(self._data.shape[0], max(*zrange))),
+ (max(0, min(*yrange)), min(self._data.shape[1], max(*yrange))),
+ (max(0, min(*xrange_)), min(self._data.shape[2], max(*xrange_))),
+ ),
+ dtype=numpy.int64,
+ )
# numpy.equal supports None
if not numpy.all(numpy.equal(selectedRange, self._selectedRange)):
@@ -1463,7 +1463,8 @@ class ScalarFieldView(Plot3DWindow):
scales = self._selectedRange[:, 1] - self._selectedRange[:, 0]
self._selectionBox.size = scales[::-1]
self._selectionBox.transforms = [
- transform.Translate(*self._selectedRange[::-1, 0])]
+ transform.Translate(*self._selectedRange[::-1, 0])
+ ]
self.sigSelectedRegionChanged.emit(self.getSelectedRegion())
@@ -1473,10 +1474,14 @@ class ScalarFieldView(Plot3DWindow):
return None
else:
dataBBox = self._group.transforms.transformBounds(
- self._selectedRange[::-1].T).T
- return SelectedRegion(self._selectedRange, dataBBox,
- translation=self.getTranslation(),
- scale=self.getScale())
+ self._selectedRange[::-1].T
+ ).T
+ return SelectedRegion(
+ self._selectedRange,
+ dataBBox,
+ translation=self.getTranslation(),
+ scale=self.getScale(),
+ )
# Handle iso-surfaces
@@ -1531,8 +1536,8 @@ class ScalarFieldView(Plot3DWindow):
:param isosurface: The isosurface object to remove"""
if isosurface not in self.getIsosurfaces():
_logger.warning(
- "Try to remove isosurface that is not in the list: %s",
- str(isosurface))
+ "Try to remove isosurface that is not in the list: %s", str(isosurface)
+ )
else:
isosurface.sigLevelChanged.disconnect(self._updateIsosurfaces)
self._isosurfaces.remove(isosurface)
@@ -1547,6 +1552,5 @@ class ScalarFieldView(Plot3DWindow):
def _updateIsosurfaces(self, level=None):
"""Handle updates of iso-surfaces level and add/remove"""
# Sorting using minus, this supposes data 'object' to be max values
- sortedIso = sorted(self.getIsosurfaces(),
- key=lambda iso: - iso.getLevel())
+ sortedIso = sorted(self.getIsosurfaces(), key=lambda iso: -iso.getLevel())
self._isogroup.children = [iso._get3DPrimitive() for iso in sortedIso]
diff --git a/src/silx/gui/plot3d/SceneWidget.py b/src/silx/gui/plot3d/SceneWidget.py
index 883f5e7..d4d21cb 100644
--- a/src/silx/gui/plot3d/SceneWidget.py
+++ b/src/silx/gui/plot3d/SceneWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module provides a widget to view data sets in 3D."""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
@@ -45,7 +42,7 @@ from .scene import interaction
from ._model import SceneModel, visitQAbstractItemModel
from ._model.items import Item3DRow
-__all__ = ['items', 'SceneWidget']
+__all__ = ["items", "SceneWidget"]
class _SceneSelectionHighlightManager(object):
@@ -91,8 +88,7 @@ class _SceneSelectionHighlightManager(object):
else: # disabled
self.__unselectItem(current)
- selection.sigCurrentChanged.disconnect(
- self.__currentChanged)
+ selection.sigCurrentChanged.disconnect(self.__currentChanged)
def getSceneWidget(self):
"""Returns the SceneWidget this class controls highlight for.
@@ -104,7 +100,7 @@ class _SceneSelectionHighlightManager(object):
def __selectItem(self, current):
"""Highlight given item.
- :param ~silx.gui.plot3d.items.Item3D current: New current or None
+ :param ~silx.gui.plot3d.items.Item3D current: New current or None
"""
if current is None:
return
@@ -134,8 +130,9 @@ class _SceneSelectionHighlightManager(object):
# Restore bbox visibility and color
current.sigItemChanged.disconnect(self.__selectedChanged)
- if (self._previousBBoxState is not None and
- isinstance(current, items.DataItem3D)):
+ if self._previousBBoxState is not None and isinstance(
+ current, items.DataItem3D
+ ):
current.setBoundingBoxVisible(self._previousBBoxState)
current._setForegroundColor(sceneWidget.getForegroundColor())
@@ -163,10 +160,10 @@ class _SceneSelectionHighlightManager(object):
class HighlightMode(enum.Enum):
""":class:`SceneSelection` highlight modes"""
- NONE = 'noHighlight'
+ NONE = "noHighlight"
"""Do not highlight selected item"""
- BOUNDING_BOX = 'boundingBox'
+ BOUNDING_BOX = "boundingBox"
"""Highlight selected item bounding box"""
@@ -247,12 +244,10 @@ class SceneSelection(qt.QObject):
item.sigItemChanged.connect(self.__currentChanged)
self.__current = weakref.ref(item)
else:
- raise ValueError(
- 'Item is not in this SceneWidget: %s' % str(item))
+ raise ValueError("Item is not in this SceneWidget: %s" % str(item))
else:
- raise ValueError(
- 'Not an Item3D: %s' % str(item))
+ raise ValueError("Not an Item3D: %s" % str(item))
current = self.getCurrentItem()
self.sigCurrentChanged.emit(current, previous)
@@ -285,24 +280,29 @@ class SceneSelection(qt.QObject):
:raise ValueError: If the selection model does not correspond
to the same :class:`SceneWidget`
"""
- if (not isinstance(selectionModel, qt.QItemSelectionModel) or
- not isinstance(selectionModel.model(), SceneModel) or
- selectionModel.model().sceneWidget() is not self.parent()):
- raise ValueError("Expecting a QItemSelectionModel "
- "attached to the same SceneWidget")
+ if (
+ not isinstance(selectionModel, qt.QItemSelectionModel)
+ or not isinstance(selectionModel.model(), SceneModel)
+ or selectionModel.model().sceneWidget() is not self.parent()
+ ):
+ raise ValueError(
+ "Expecting a QItemSelectionModel " "attached to the same SceneWidget"
+ )
# Disconnect from previous selection model
previousSelectionModel = self._getSyncSelectionModel()
if previousSelectionModel is not None:
previousSelectionModel.selectionChanged.disconnect(
- self.__selectionModelSelectionChanged)
+ self.__selectionModelSelectionChanged
+ )
self.__selectionModel = selectionModel
if selectionModel is not None:
# Connect to new selection model
selectionModel.selectionChanged.connect(
- self.__selectionModelSelectionChanged)
+ self.__selectionModelSelectionChanged
+ )
self.__updateSelectionModel()
def __selectionModelSelectionChanged(self, selected, deselected):
@@ -344,15 +344,19 @@ class SceneSelection(qt.QObject):
model = selectionModel.model()
for index in visitQAbstractItemModel(model):
itemRow = index.internalPointer()
- if (isinstance(itemRow, Item3DRow) and
- itemRow.item() is currentItem and
- index.flags() & qt.Qt.ItemIsSelectable):
+ if (
+ isinstance(itemRow, Item3DRow)
+ and itemRow.item() is currentItem
+ and index.flags() & qt.Qt.ItemIsSelectable
+ ):
# This is the item we are looking for: select it in the model
self.__syncInProgress = True
selectionModel.select(
- index, qt.QItemSelectionModel.Clear |
- qt.QItemSelectionModel.Select |
- qt.QItemSelectionModel.Current)
+ index,
+ qt.QItemSelectionModel.Clear
+ | qt.QItemSelectionModel.Select
+ | qt.QItemSelectionModel.Current,
+ )
self.__syncInProgress = False
break
@@ -366,15 +370,14 @@ class SceneWidget(Plot3DWidget):
self._selection = None # Store lazy-loaded SceneSelection
self._items = []
- self._textColor = 1., 1., 1., 1.
- self._foregroundColor = 1., 1., 1., 1.
- self._highlightColor = 0.7, 0.7, 0., 1.
+ self._textColor = 1.0, 1.0, 1.0, 1.0
+ self._foregroundColor = 1.0, 1.0, 1.0, 1.0
+ self._highlightColor = 0.7, 0.7, 0.0, 1.0
self._sceneGroup = RootGroupWithAxesItem(parent=self)
- self._sceneGroup.setLabel('Data')
+ self._sceneGroup.setLabel("Data")
- self.viewport.scene.children.append(
- self._sceneGroup._getScenePrimitive())
+ self.viewport.scene.children.append(self._sceneGroup._getScenePrimitive())
def model(self):
"""Returns the model corresponding the scene of this widget
@@ -422,20 +425,21 @@ class SceneWidget(Plot3DWidget):
devicePixelRatio = self.getDevicePixelRatio()
for result in self.getSceneGroup().pickItems(
- x * devicePixelRatio, y * devicePixelRatio, condition):
+ x * devicePixelRatio, y * devicePixelRatio, condition
+ ):
yield result
# Interactive modes
def _handleSelectionChanged(self, current, previous):
"""Handle change of selection to update interactive mode"""
- if self.getInteractiveMode() == 'panSelectedPlane':
+ if self.getInteractiveMode() == "panSelectedPlane":
if isinstance(current, items.PlaneMixIn):
# Update pan plane to use new selected plane
- self.setInteractiveMode('panSelectedPlane')
+ self.setInteractiveMode("panSelectedPlane")
else: # Switch to rotate scene if new selection is not a plane
- self.setInteractiveMode('rotate')
+ self.setInteractiveMode("rotate")
def setInteractiveMode(self, mode):
"""Set the interactive mode.
@@ -446,26 +450,25 @@ class SceneWidget(Plot3DWidget):
:param str mode:
The interactive mode: 'rotate', 'pan', 'panSelectedPlane' or None
"""
- if self.getInteractiveMode() == 'panSelectedPlane':
- self.selection().sigCurrentChanged.disconnect(
- self._handleSelectionChanged)
+ if self.getInteractiveMode() == "panSelectedPlane":
+ self.selection().sigCurrentChanged.disconnect(self._handleSelectionChanged)
- if mode == 'panSelectedPlane':
+ if mode == "panSelectedPlane":
selected = self.selection().getCurrentItem()
if isinstance(selected, items.PlaneMixIn):
mode = interaction.PanPlaneZoomOnWheelControl(
self.viewport,
selected._getPlane(),
- mode='position',
+ mode="position",
orbitAroundCenter=False,
- scaleTransform=self._sceneScale)
+ scaleTransform=self._sceneScale,
+ )
- self.selection().sigCurrentChanged.connect(
- self._handleSelectionChanged)
+ self.selection().sigCurrentChanged.connect(self._handleSelectionChanged)
else: # No selected plane, fallback to rotate scene
- mode = 'rotate'
+ mode = "rotate"
super(SceneWidget, self).setInteractiveMode(mode)
@@ -475,7 +478,7 @@ class SceneWidget(Plot3DWidget):
:rtype: str
"""
if isinstance(self.eventHandler, interaction.PanPlaneZoomOnWheelControl):
- return 'panSelectedPlane'
+ return "panSelectedPlane"
else:
return super(SceneWidget, self).getInteractiveMode()
@@ -634,7 +637,7 @@ class SceneWidget(Plot3DWidget):
bbox = self._sceneGroup._getScenePrimitive()
bbox.tickColor = color
- self.sigStyleChanged.emit('textColor')
+ self.sigStyleChanged.emit("textColor")
def getForegroundColor(self):
"""Return color used for bounding box
@@ -660,7 +663,7 @@ class SceneWidget(Plot3DWidget):
if item is not selected:
item._setForegroundColor(color)
- self.sigStyleChanged.emit('foregroundColor')
+ self.sigStyleChanged.emit("foregroundColor")
def getHighlightColor(self):
"""Return color used for highlighted item bounding box
@@ -684,4 +687,4 @@ class SceneWidget(Plot3DWidget):
if selected is not None:
selected._setForegroundColor(color)
- self.sigStyleChanged.emit('highlightColor')
+ self.sigStyleChanged.emit("highlightColor")
diff --git a/src/silx/gui/plot3d/SceneWindow.py b/src/silx/gui/plot3d/SceneWindow.py
index 052a4dc..98c93fd 100644
--- a/src/silx/gui/plot3d/SceneWindow.py
+++ b/src/silx/gui/plot3d/SceneWindow.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides a QMainWindow with a 3D SceneWidget and toolbars.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "29/11/2017"
@@ -47,7 +44,7 @@ from .ParamTreeView import ParamTreeView
from . import items # noqa
-__all__ = ['items', 'SceneWidget', 'SceneWindow']
+__all__ = ["items", "SceneWidget", "SceneWindow"]
class _PanPlaneAction(InteractiveModeAction):
@@ -57,27 +54,24 @@ class _PanPlaneAction(InteractiveModeAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, plot3d=None):
- super(_PanPlaneAction, self).__init__(
- parent, 'panSelectedPlane', plot3d)
- self.setIcon(icons.getQIcon('3d-plane-pan'))
- self.setText('Pan plane')
+ super(_PanPlaneAction, self).__init__(parent, "panSelectedPlane", plot3d)
+ self.setIcon(icons.getQIcon("3d-plane-pan"))
+ self.setText("Pan plane")
self.setCheckable(True)
- self.setToolTip(
- 'Pan selected plane. Press <b>Ctrl</b> to rotate the scene.')
+ self.setToolTip("Pan selected plane. Press <b>Ctrl</b> to rotate the scene.")
def _planeChanged(self, event):
"""Handle plane updates"""
- if event in (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.POSITION):
+ if event in (items.ItemChangedType.VISIBLE, items.ItemChangedType.POSITION):
plane = self.sender()
- isPlaneInteractive = \
- plane._getPlane().plane.isPlane and plane.isVisible()
+ isPlaneInteractive = plane._getPlane().plane.isPlane and plane.isVisible()
if isPlaneInteractive != self.isEnabled():
self.setEnabled(isPlaneInteractive)
- mode = 'panSelectedPlane' if isPlaneInteractive else 'rotate'
+ mode = "panSelectedPlane" if isPlaneInteractive else "rotate"
self.getPlot3DWidget().setInteractiveMode(mode)
def _selectionChanged(self, current, previous):
@@ -88,24 +82,21 @@ class _PanPlaneAction(InteractiveModeAction):
if isinstance(current, items.PlaneMixIn):
current.sigItemChanged.connect(self._planeChanged)
self.setEnabled(True)
- self.getPlot3DWidget().setInteractiveMode('panSelectedPlane')
+ self.getPlot3DWidget().setInteractiveMode("panSelectedPlane")
else:
self.setEnabled(False)
def setPlot3DWidget(self, widget):
previous = self.getPlot3DWidget()
if isinstance(previous, SceneWidget):
- previous.selection().sigCurrentChanged.disconnect(
- self._selectionChanged)
- self._selectionChanged(
- None, previous.selection().getCurrentItem())
+ previous.selection().sigCurrentChanged.disconnect(self._selectionChanged)
+ self._selectionChanged(None, previous.selection().getCurrentItem())
super(_PanPlaneAction, self).setPlot3DWidget(widget)
if isinstance(widget, SceneWidget):
self._selectionChanged(widget.selection().getCurrentItem(), None)
- widget.selection().sigCurrentChanged.connect(
- self._selectionChanged)
+ widget.selection().sigCurrentChanged.connect(self._selectionChanged)
class SceneWindow(qt.QMainWindow):
@@ -131,16 +122,17 @@ class SceneWindow(qt.QMainWindow):
self._interactiveModeToolBar = InteractiveModeToolBar(parent=self)
panPlaneAction = _PanPlaneAction(self, plot3d=self._sceneWidget)
- self._interactiveModeToolBar.addAction(
- self._positionInfo.toggleAction())
+ self._interactiveModeToolBar.addAction(self._positionInfo.toggleAction())
self._interactiveModeToolBar.addAction(panPlaneAction)
self._viewpointToolBar = ViewpointToolBar(parent=self)
self._outputToolBar = OutputToolBar(parent=self)
- for toolbar in (self._interactiveModeToolBar,
- self._viewpointToolBar,
- self._outputToolBar):
+ for toolbar in (
+ self._interactiveModeToolBar,
+ self._viewpointToolBar,
+ self._outputToolBar,
+ ):
toolbar.setPlot3DWidget(self._sceneWidget)
self.addToolBar(toolbar)
self.addActions(toolbar.actions())
@@ -149,20 +141,18 @@ class SceneWindow(qt.QMainWindow):
self._paramTreeView.setModel(self._sceneWidget.model())
selectionModel = self._paramTreeView.selectionModel()
- self._sceneWidget.selection()._setSyncSelectionModel(
- selectionModel)
+ self._sceneWidget.selection()._setSyncSelectionModel(selectionModel)
paramDock = qt.QDockWidget()
- paramDock.setWindowTitle('Object parameters')
+ paramDock.setWindowTitle("Object parameters")
paramDock.setWidget(self._paramTreeView)
self.addDockWidget(qt.Qt.RightDockWidgetArea, paramDock)
self._sceneGroupResetWidget = GroupPropertiesWidget()
- self._sceneGroupResetWidget.setGroup(
- self._sceneWidget.getSceneGroup())
+ self._sceneGroupResetWidget.setGroup(self._sceneWidget.getSceneGroup())
resetDock = qt.QDockWidget()
- resetDock.setWindowTitle('Global parameters')
+ resetDock.setWindowTitle("Global parameters")
resetDock.setWidget(self._sceneGroupResetWidget)
self.addDockWidget(qt.Qt.RightDockWidgetArea, resetDock)
self.tabifyDockWidget(paramDock, resetDock)
diff --git a/src/silx/gui/plot3d/__init__.py b/src/silx/gui/plot3d/__init__.py
index af74613..470d37b 100644
--- a/src/silx/gui/plot3d/__init__.py
+++ b/src/silx/gui/plot3d/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
@@ -27,7 +26,6 @@ This package provides widgets displaying 3D content based on OpenGL.
It depends on PyOpenGL and PyQtx.QtOpenGL or PyQt>=5.4.
"""
-from __future__ import absolute_import
__authors__ = ["T. Vincent"]
__license__ = "MIT"
@@ -37,4 +35,4 @@ __date__ = "18/01/2017"
try:
import OpenGL as _OpenGL
except ImportError:
- raise ImportError('PyOpenGL is not installed')
+ raise ImportError("PyOpenGL is not installed")
diff --git a/src/silx/gui/plot3d/_model/__init__.py b/src/silx/gui/plot3d/_model/__init__.py
index 4b16e32..fd8eafb 100644
--- a/src/silx/gui/plot3d/_model/__init__.py
+++ b/src/silx/gui/plot3d/_model/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
@@ -26,8 +25,6 @@
This package provides :class:`SceneWidget` content and parameters model.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "11/01/2018"
diff --git a/src/silx/gui/plot3d/_model/core.py b/src/silx/gui/plot3d/_model/core.py
index e8e0820..cb34ab9 100644
--- a/src/silx/gui/plot3d/_model/core.py
+++ b/src/silx/gui/plot3d/_model/core.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
@@ -26,8 +25,6 @@
This module provides base classes to implement models for 3D scene content.
"""
-from __future__ import absolute_import, division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "11/01/2018"
@@ -245,7 +242,7 @@ class StaticRow(BaseRow):
:param children: Iterable of BaseRow to start with (not signaled)
"""
- def __init__(self, display=('', None), roles=None, children=()):
+ def __init__(self, display=("", None), roles=None, children=()):
super(StaticRow, self).__init__(children)
self._dataByRoles = {} if roles is None else roles
self._dataByRoles[qt.Qt.DisplayRole] = display
@@ -281,15 +278,16 @@ class ProxyRow(BaseRow):
:param editorHint: Data to provide as UserRole for editor selection/setup
"""
- def __init__(self,
- name='',
- fget=None,
- fset=None,
- notify=None,
- toModelData=None,
- fromModelData=None,
- editorHint=None):
-
+ def __init__(
+ self,
+ name="",
+ fget=None,
+ fset=None,
+ notify=None,
+ toModelData=None,
+ fromModelData=None,
+ editorHint=None,
+ ):
super(ProxyRow, self).__init__()
self.__name = name
self.__editorHint = editorHint
@@ -320,8 +318,9 @@ class ProxyRow(BaseRow):
elif column == 1:
if role == qt.Qt.UserRole: # EditorHint
return self.__editorHint
- elif role == qt.Qt.DisplayRole or (role == qt.Qt.EditRole and
- self._fset is not None):
+ elif role == qt.Qt.DisplayRole or (
+ role == qt.Qt.EditRole and self._fset is not None
+ ):
data = self._fget()
if self._toModelData is not None:
data = self._toModelData(data)
@@ -367,6 +366,6 @@ class AngleDegreeRow(ProxyRow):
def data(self, column, role):
if column == 1 and role == qt.Qt.DisplayRole:
- return u'%g°' % super(AngleDegreeRow, self).data(column, role)
+ return "%g°" % super(AngleDegreeRow, self).data(column, role)
else:
return super(AngleDegreeRow, self).data(column, role)
diff --git a/src/silx/gui/plot3d/_model/items.py b/src/silx/gui/plot3d/_model/items.py
index 492f44b..8441be7 100644
--- a/src/silx/gui/plot3d/_model/items.py
+++ b/src/silx/gui/plot3d/_model/items.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,14 +25,11 @@
This module provides base classes to implement models for 3D scene content
"""
-from __future__ import absolute_import, division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
-from collections import OrderedDict
import functools
import logging
import weakref
@@ -77,15 +73,17 @@ class ItemProxyRow(ProxyRow):
:param editorHint: Data to provide as UserRole for editor selection/setup
"""
- def __init__(self,
- item,
- name='',
- fget=None,
- fset=None,
- events=None,
- toModelData=None,
- fromModelData=None,
- editorHint=None):
+ def __init__(
+ self,
+ item,
+ name="",
+ fget=None,
+ fset=None,
+ events=None,
+ toModelData=None,
+ fromModelData=None,
+ editorHint=None,
+ ):
super(ItemProxyRow, self).__init__(
name=name,
fget=fget,
@@ -93,10 +91,10 @@ class ItemProxyRow(ProxyRow):
notify=None,
toModelData=toModelData,
fromModelData=fromModelData,
- editorHint=editorHint)
+ editorHint=editorHint,
+ )
- if isinstance(events, (items.ItemChangedType,
- items.Item3DChangedType)):
+ if isinstance(events, (items.ItemChangedType, items.Item3DChangedType)):
events = (events,)
self.__events = events
item.sigItemChanged.connect(self._itemChanged)
@@ -125,8 +123,7 @@ class ItemAngleDegreeRow(AngleDegreeRow, ItemProxyRow):
class _DirectionalLightProxy(qt.QObject):
- """Proxy to handle directional light with angles rather than vector.
- """
+ """Proxy to handle directional light with angles rather than vector."""
sigAzimuthAngleChanged = qt.Signal()
"""Signal sent when the azimuth angle has changed."""
@@ -187,11 +184,11 @@ class _DirectionalLightProxy(qt.QObject):
"""Handle light direction update in the scene"""
# Invert direction to manipulate the 'source' pointing to
# the center of the viewport
- x, y, z = - self._light.direction
+ x, y, z = -self._light.direction
# Horizontal plane is plane xz
azimuth = int(round(numpy.degrees(numpy.arctan2(x, z))))
- altitude = int(round(numpy.degrees(numpy.pi/2. - numpy.arccos(y))))
+ altitude = int(round(numpy.degrees(numpy.pi / 2.0 - numpy.arccos(y))))
if azimuth != self.getAzimuthAngle():
self.setAzimuthAngle(azimuth)
@@ -202,12 +199,12 @@ class _DirectionalLightProxy(qt.QObject):
def _updateLight(self):
"""Update light direction in the scene"""
azimuth = numpy.radians(self._azimuth)
- delta = numpy.pi/2. - numpy.radians(self._altitude)
- if delta == 0.: # Avoids zenith position
+ delta = numpy.pi / 2.0 - numpy.radians(self._altitude)
+ if delta == 0.0: # Avoids zenith position
delta = 0.0001
- z = - numpy.sin(delta) * numpy.cos(azimuth)
- x = - numpy.sin(delta) * numpy.sin(azimuth)
- y = - numpy.cos(delta)
+ z = -numpy.sin(delta) * numpy.cos(azimuth)
+ x = -numpy.sin(delta) * numpy.sin(azimuth)
+ y = -numpy.cos(delta)
self._light.direction = x, y, z
@@ -219,69 +216,87 @@ class Settings(StaticRow):
def __init__(self, sceneWidget):
background = ColorProxyRow(
- name='Background',
+ name="Background",
fget=sceneWidget.getBackgroundColor,
fset=sceneWidget.setBackgroundColor,
- notify=sceneWidget.sigStyleChanged)
+ notify=sceneWidget.sigStyleChanged,
+ )
foreground = ColorProxyRow(
- name='Foreground',
+ name="Foreground",
fget=sceneWidget.getForegroundColor,
fset=sceneWidget.setForegroundColor,
- notify=sceneWidget.sigStyleChanged)
+ notify=sceneWidget.sigStyleChanged,
+ )
text = ColorProxyRow(
- name='Text',
+ name="Text",
fget=sceneWidget.getTextColor,
fset=sceneWidget.setTextColor,
- notify=sceneWidget.sigStyleChanged)
+ notify=sceneWidget.sigStyleChanged,
+ )
highlight = ColorProxyRow(
- name='Highlight',
+ name="Highlight",
fget=sceneWidget.getHighlightColor,
fset=sceneWidget.setHighlightColor,
- notify=sceneWidget.sigStyleChanged)
+ notify=sceneWidget.sigStyleChanged,
+ )
axesIndicator = ProxyRow(
- name='Axes Indicator',
+ name="Axes Indicator",
fget=sceneWidget.isOrientationIndicatorVisible,
fset=sceneWidget.setOrientationIndicatorVisible,
- notify=sceneWidget.sigStyleChanged)
+ notify=sceneWidget.sigStyleChanged,
+ )
# Light direction
self._lightProxy = _DirectionalLightProxy(sceneWidget.viewport.light)
azimuthNode = ProxyRow(
- name='Azimuth',
+ name="Azimuth",
fget=self._lightProxy.getAzimuthAngle,
fset=self._lightProxy.setAzimuthAngle,
notify=self._lightProxy.sigAzimuthAngleChanged,
- editorHint=(-90, 90))
+ editorHint=(-90, 90),
+ )
altitudeNode = ProxyRow(
- name='Altitude',
+ name="Altitude",
fget=self._lightProxy.getAltitudeAngle,
fset=self._lightProxy.setAltitudeAngle,
notify=self._lightProxy.sigAltitudeAngleChanged,
- editorHint=(-90, 90))
+ editorHint=(-90, 90),
+ )
- lightDirection = StaticRow(('Light Direction', None),
- children=(azimuthNode, altitudeNode))
+ lightDirection = StaticRow(
+ ("Light Direction", None), children=(azimuthNode, altitudeNode)
+ )
# Fog
fog = ProxyRow(
- name='Fog',
+ name="Fog",
fget=sceneWidget.getFogMode,
fset=sceneWidget.setFogMode,
notify=sceneWidget.sigStyleChanged,
toModelData=lambda mode: mode is Plot3DWidget.FogMode.LINEAR,
- fromModelData=lambda mode: Plot3DWidget.FogMode.LINEAR if mode else Plot3DWidget.FogMode.NONE)
+ fromModelData=lambda mode: Plot3DWidget.FogMode.LINEAR
+ if mode
+ else Plot3DWidget.FogMode.NONE,
+ )
# Settings row
- children = (background, foreground, text, highlight,
- axesIndicator, lightDirection, fog)
- super(Settings, self).__init__(('Settings', None), children=children)
+ children = (
+ background,
+ foreground,
+ text,
+ highlight,
+ axesIndicator,
+ lightDirection,
+ fog,
+ )
+ super(Settings, self).__init__(("Settings", None), children=children)
class Item3DRow(BaseRow):
@@ -299,8 +314,8 @@ class Item3DRow(BaseRow):
super(Item3DRow, self).__init__()
self.setFlags(
- self.flags(0) | qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsSelectable,
- 0)
+ self.flags(0) | qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsSelectable, 0
+ )
self.setFlags(self.flags(1) | qt.Qt.ItemIsSelectable, 1)
self._item = weakref.ref(item)
@@ -328,12 +343,12 @@ class Item3DRow(BaseRow):
return qt.Qt.Unchecked
elif role == qt.Qt.DecorationRole:
- return icons.getQIcon('item-3dim')
+ return icons.getQIcon("item-3dim")
elif role == qt.Qt.DisplayRole:
if self.__name is None:
item = self.item()
- return '' if item is None else item.getLabel()
+ return "" if item is None else item.getLabel()
else:
return self.__name
@@ -343,7 +358,7 @@ class Item3DRow(BaseRow):
if column == 0 and role == qt.Qt.CheckStateRole:
item = self.item()
if item is not None:
- item.setVisible(value == qt.Qt.Checked)
+ item.setVisible(qt.Qt.CheckState(value) == qt.Qt.Checked)
return True
else:
return False
@@ -362,10 +377,11 @@ class DataItem3DBoundingBoxRow(ItemProxyRow):
def __init__(self, item):
super(DataItem3DBoundingBoxRow, self).__init__(
item=item,
- name='Bounding box',
+ name="Bounding box",
fget=item.isBoundingBoxVisible,
fset=item.setBoundingBoxVisible,
- events=items.Item3DChangedType.BOUNDING_BOX_VISIBLE)
+ events=items.Item3DChangedType.BOUNDING_BOX_VISIBLE,
+ )
class MatrixProxyRow(ItemProxyRow):
@@ -381,10 +397,11 @@ class MatrixProxyRow(ItemProxyRow):
super(MatrixProxyRow, self).__init__(
item=item,
- name='',
+ name="",
fget=self._getMatrixRow,
fset=self._setMatrixRow,
- events=items.Item3DChangedType.TRANSFORM)
+ events=items.Item3DChangedType.TRANSFORM,
+ )
def _getMatrixRow(self):
"""Returns the matrix row.
@@ -425,19 +442,20 @@ class DataItem3DTransformRow(StaticRow):
:param DataItem3D item: The item for which to display/control transform
"""
- _ROTATION_CENTER_OPTIONS = 'Origin', 'Lower', 'Center', 'Upper'
+ _ROTATION_CENTER_OPTIONS = "Origin", "Lower", "Center", "Upper"
def __init__(self, item):
- super(DataItem3DTransformRow, self).__init__(('Transform', None))
+ super(DataItem3DTransformRow, self).__init__(("Transform", None))
self._item = weakref.ref(item)
translation = ItemProxyRow(
item=item,
- name='Translation',
+ name="Translation",
fget=item.getTranslation,
fset=self._setTranslation,
events=items.Item3DChangedType.TRANSFORM,
- toModelData=lambda data: qt.QVector3D(*data))
+ toModelData=lambda data: qt.QVector3D(*data),
+ )
self.addRow(translation)
# Here to keep a reference
@@ -446,69 +464,80 @@ class DataItem3DTransformRow(StaticRow):
self._zSetCenter = functools.partial(self._setCenter, index=2)
rotateCenter = StaticRow(
- ('Center', None),
+ ("Center", None),
children=(
- ItemProxyRow(item=item,
- name='X axis',
- fget=item.getRotationCenter,
- fset=self._xSetCenter,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=functools.partial(
- self._centerToModelData, index=0),
- editorHint=self._ROTATION_CENTER_OPTIONS),
- ItemProxyRow(item=item,
- name='Y axis',
- fget=item.getRotationCenter,
- fset=self._ySetCenter,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=functools.partial(
- self._centerToModelData, index=1),
- editorHint=self._ROTATION_CENTER_OPTIONS),
- ItemProxyRow(item=item,
- name='Z axis',
- fget=item.getRotationCenter,
- fset=self._zSetCenter,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=functools.partial(
- self._centerToModelData, index=2),
- editorHint=self._ROTATION_CENTER_OPTIONS),
- ))
+ ItemProxyRow(
+ item=item,
+ name="X axis",
+ fget=item.getRotationCenter,
+ fset=self._xSetCenter,
+ events=items.Item3DChangedType.TRANSFORM,
+ toModelData=functools.partial(self._centerToModelData, index=0),
+ editorHint=self._ROTATION_CENTER_OPTIONS,
+ ),
+ ItemProxyRow(
+ item=item,
+ name="Y axis",
+ fget=item.getRotationCenter,
+ fset=self._ySetCenter,
+ events=items.Item3DChangedType.TRANSFORM,
+ toModelData=functools.partial(self._centerToModelData, index=1),
+ editorHint=self._ROTATION_CENTER_OPTIONS,
+ ),
+ ItemProxyRow(
+ item=item,
+ name="Z axis",
+ fget=item.getRotationCenter,
+ fset=self._zSetCenter,
+ events=items.Item3DChangedType.TRANSFORM,
+ toModelData=functools.partial(self._centerToModelData, index=2),
+ editorHint=self._ROTATION_CENTER_OPTIONS,
+ ),
+ ),
+ )
rotate = StaticRow(
- ('Rotation', None),
+ ("Rotation", None),
children=(
ItemAngleDegreeRow(
item=item,
- name='Angle',
+ name="Angle",
fget=item.getRotation,
fset=self._setAngle,
events=items.Item3DChangedType.TRANSFORM,
- toModelData=lambda data: data[0]),
+ toModelData=lambda data: data[0],
+ ),
ItemProxyRow(
item=item,
- name='Axis',
+ name="Axis",
fget=item.getRotation,
fset=self._setAxis,
events=items.Item3DChangedType.TRANSFORM,
- toModelData=lambda data: qt.QVector3D(*data[1])),
- rotateCenter
- ))
+ toModelData=lambda data: qt.QVector3D(*data[1]),
+ ),
+ rotateCenter,
+ ),
+ )
self.addRow(rotate)
scale = ItemProxyRow(
item=item,
- name='Scale',
+ name="Scale",
fget=item.getScale,
fset=self._setScale,
events=items.Item3DChangedType.TRANSFORM,
- toModelData=lambda data: qt.QVector3D(*data))
+ toModelData=lambda data: qt.QVector3D(*data),
+ )
self.addRow(scale)
matrix = StaticRow(
- ('Matrix', None),
- children=(MatrixProxyRow(item, 0),
- MatrixProxyRow(item, 1),
- MatrixProxyRow(item, 2)))
+ ("Matrix", None),
+ children=(
+ MatrixProxyRow(item, 0),
+ MatrixProxyRow(item, 1),
+ MatrixProxyRow(item, 2),
+ ),
+ )
self.addRow(matrix)
def item(self):
@@ -525,8 +554,8 @@ class DataItem3DTransformRow(StaticRow):
value = center[index]
if isinstance(value, str):
return value.title()
- elif value == 0.:
- return 'Origin'
+ elif value == 0.0:
+ return "Origin"
else:
return str(value)
@@ -538,8 +567,8 @@ class DataItem3DTransformRow(StaticRow):
"""
item = self.item()
if item is not None:
- if value == 'Origin':
- value = 0.
+ if value == "Origin":
+ value = 0.0
elif value not in self._ROTATION_CENTER_OPTIONS:
value = float(value)
else:
@@ -586,8 +615,8 @@ class DataItem3DTransformRow(StaticRow):
item = self.item()
if item is not None:
sx, sy, sz = scale.x(), scale.y(), scale.z()
- if sx == 0. or sy == 0. or sz == 0.:
- _logger.warning('Cannot set scale to 0: ignored')
+ if sx == 0.0 or sy == 0.0 or sz == 0.0:
+ _logger.warning("Cannot set scale to 0: ignored")
else:
item.setScale(scale.x(), scale.y(), scale.z())
@@ -653,13 +682,14 @@ class InterpolationRow(ItemProxyRow):
modes = [mode.title() for mode in item.INTERPOLATION_MODES]
super(InterpolationRow, self).__init__(
item=item,
- name='Interpolation',
+ name="Interpolation",
fget=item.getInterpolation,
fset=item.setInterpolation,
events=items.Item3DChangedType.INTERPOLATION,
toModelData=lambda mode: mode.title(),
fromModelData=lambda mode: mode.lower(),
- editorHint=modes)
+ editorHint=modes,
+ )
class _ColormapBaseProxyRow(ProxyRow):
@@ -739,15 +769,14 @@ class _ColormapBoundRow(_ColormapBaseProxyRow):
def __init__(self, item, name, index):
self._index = index
_ColormapBaseProxyRow.__init__(
- self,
- item,
- name=name,
- fget=self._getBound,
- fset=self._setBound)
+ self, item, name=name, fget=self._getBound, fset=self._setBound
+ )
- self.setToolTip('Colormap %s bound:\n'
- 'Check to set bound manually, '
- 'uncheck for autoscale' % name.lower())
+ self.setToolTip(
+ "Colormap %s bound:\n"
+ "Check to set bound manually, "
+ "uncheck for autoscale" % name.lower()
+ )
def _getRawBound(self):
"""Proxy to get raw colormap bound
@@ -773,7 +802,7 @@ class _ColormapBoundRow(_ColormapBaseProxyRow):
bound = self._getColormapRange()[self._index]
return bound
else:
- return 1. # Fallback
+ return 1.0 # Fallback
def _setBound(self, value):
"""Proxy to set colormap bound.
@@ -819,7 +848,11 @@ class _ColormapBoundRow(_ColormapBaseProxyRow):
def setData(self, column, value, role):
if column == 0 and role == qt.Qt.CheckStateRole:
if self._colormap is not None:
- bound = self._getBound() if value == qt.Qt.Checked else None
+ bound = (
+ self._getBound()
+ if qt.Qt.CheckState(value) == qt.Qt.Checked
+ else None
+ )
self._setBound(bound)
return True
else:
@@ -841,10 +874,13 @@ class _ColormapGammaRow(_ColormapBaseProxyRow):
item,
name="Gamma",
fget=self._getGammaNormalizationParameter,
- fset=self._setGammaNormalizationParameter)
+ fset=self._setGammaNormalizationParameter,
+ )
- self.setToolTip('Colormap gamma correction parameter:\n'
- 'Only meaningful for gamma normalization.')
+ self.setToolTip(
+ "Colormap gamma correction parameter:\n"
+ "Only meaningful for gamma normalization."
+ )
def _getGammaNormalizationParameter(self):
"""Proxy for :meth:`Colormap.getGammaNormalizationParameter`"""
@@ -863,11 +899,11 @@ class _ColormapGammaRow(_ColormapBaseProxyRow):
if self._colormap is not None:
return self._colormap.getNormalization()
else:
- return ''
+ return ""
def flags(self, column):
if column in (0, 1):
- if self._getNormalization() == 'gamma':
+ if self._getNormalization() == "gamma":
flags = qt.Qt.ItemIsEditable | qt.Qt.ItemIsEnabled
else:
flags = qt.Qt.NoItemFlags # Disabled if not gamma correction
@@ -884,10 +920,7 @@ class ColormapRow(_ColormapBaseProxyRow):
"""
def __init__(self, item):
- super(ColormapRow, self).__init__(
- item,
- name='Colormap',
- fget=self._get)
+ super(ColormapRow, self).__init__(item, name="Colormap", fget=self._get)
self._colormapImage = None
@@ -895,33 +928,42 @@ class ColormapRow(_ColormapBaseProxyRow):
for cmap in preferredColormaps():
self._colormapsMapping[cmap.title()] = cmap
- self.addRow(ProxyRow(
- name='Name',
- fget=self._getName,
- fset=self._setName,
- notify=self._sigColormapChanged,
- editorHint=list(self._colormapsMapping.keys())))
+ self.addRow(
+ ProxyRow(
+ name="Name",
+ fget=self._getName,
+ fset=self._setName,
+ notify=self._sigColormapChanged,
+ editorHint=list(self._colormapsMapping.keys()),
+ )
+ )
norms = [norm.title() for norm in self._colormap.NORMALIZATIONS]
- self.addRow(ProxyRow(
- name='Normalization',
- fget=self._getNormalization,
- fset=self._setNormalization,
- notify=self._sigColormapChanged,
- editorHint=norms))
+ self.addRow(
+ ProxyRow(
+ name="Normalization",
+ fget=self._getNormalization,
+ fset=self._setNormalization,
+ notify=self._sigColormapChanged,
+ editorHint=norms,
+ )
+ )
self.addRow(_ColormapGammaRow(item))
modes = [mode.title() for mode in self._colormap.AUTOSCALE_MODES]
- self.addRow(ProxyRow(
- name='Autoscale Mode',
- fget=self._getAutoscaleMode,
- fset=self._setAutoscaleMode,
- notify=self._sigColormapChanged,
- editorHint=modes))
-
- self.addRow(_ColormapBoundRow(item, name='Min.', index=0))
- self.addRow(_ColormapBoundRow(item, name='Max.', index=1))
+ self.addRow(
+ ProxyRow(
+ name="Autoscale Mode",
+ fget=self._getAutoscaleMode,
+ fset=self._setAutoscaleMode,
+ notify=self._sigColormapChanged,
+ editorHint=modes,
+ )
+ )
+
+ self.addRow(_ColormapBoundRow(item, name="Min.", index=0))
+ self.addRow(_ColormapBoundRow(item, name="Max.", index=1))
self._sigColormapChanged.connect(self._updateColormapImage)
@@ -945,7 +987,7 @@ class ColormapRow(_ColormapBaseProxyRow):
if self._colormap is not None and self._colormap.getName() is not None:
return self._colormap.getName().title()
else:
- return ''
+ return ""
def _setName(self, name):
"""Proxy for :meth:`Colormap.setName`"""
@@ -959,7 +1001,7 @@ class ColormapRow(_ColormapBaseProxyRow):
if self._colormap is not None:
return self._colormap.getNormalization().title()
else:
- return ''
+ return ""
def _setNormalization(self, normalization):
"""Proxy for :meth:`Colormap.setNormalization`"""
@@ -971,7 +1013,7 @@ class ColormapRow(_ColormapBaseProxyRow):
if self._colormap is not None:
return self._colormap.getAutoscaleMode().title()
else:
- return ''
+ return ""
def _setAutoscaleMode(self, mode):
"""Proxy for :meth:`Colormap.setAutoscaleMode`"""
@@ -1004,11 +1046,12 @@ class SymbolRow(ItemProxyRow):
names = [item.getSymbolName(s) for s in item.getSupportedSymbols()]
super(SymbolRow, self).__init__(
item=item,
- name='Marker',
+ name="Marker",
fget=item.getSymbolName,
fset=item.setSymbol,
events=items.ItemChangedType.SYMBOL,
- editorHint=names)
+ editorHint=names,
+ )
class SymbolSizeRow(ItemProxyRow):
@@ -1020,11 +1063,12 @@ class SymbolSizeRow(ItemProxyRow):
def __init__(self, item):
super(SymbolSizeRow, self).__init__(
item=item,
- name='Marker size',
+ name="Marker size",
fget=item.getSymbolSize,
fset=item.setSymbolSize,
events=items.ItemChangedType.SYMBOL_SIZE,
- editorHint=(1, 20)) # TODO link with OpenGL max point size
+ editorHint=(1, 20),
+ ) # TODO link with OpenGL max point size
class PlaneEquationRow(ItemProxyRow):
@@ -1036,12 +1080,13 @@ class PlaneEquationRow(ItemProxyRow):
def __init__(self, item):
super(PlaneEquationRow, self).__init__(
item=item,
- name='Equation',
+ name="Equation",
fget=item.getParameters,
fset=item.setParameters,
events=items.ItemChangedType.POSITION,
toModelData=lambda data: qt.QVector4D(*data),
- fromModelData=lambda data: (data.x(), data.y(), data.z(), data.w()))
+ fromModelData=lambda data: (data.x(), data.y(), data.z(), data.w()),
+ )
self._item = weakref.ref(item)
def data(self, column, role):
@@ -1049,8 +1094,12 @@ class PlaneEquationRow(ItemProxyRow):
item = self._item()
if item is not None:
params = item.getParameters()
- return ('%gx %+gy %+gz %+g = 0' %
- (params[0], params[1], params[2], params[3]))
+ return "%gx %+gy %+gz %+g = 0" % (
+ params[0],
+ params[1],
+ params[2],
+ params[3],
+ )
return super(PlaneEquationRow, self).data(column, role)
@@ -1060,26 +1109,33 @@ class PlaneRow(ItemProxyRow):
:param Item3D item: Scene item with plane equation property
"""
- _PLANES = OrderedDict((('Plane 0', (1., 0., 0.)),
- ('Plane 1', (0., 1., 0.)),
- ('Plane 2', (0., 0., 1.)),
- ('-', None)))
+ _PLANES = dict(
+ (
+ ("Plane 0", (1.0, 0.0, 0.0)),
+ ("Plane 1", (0.0, 1.0, 0.0)),
+ ("Plane 2", (0.0, 0.0, 1.0)),
+ ("-", None),
+ )
+ )
"""Mapping of plane names to normals"""
- _PLANE_ICONS = {'Plane 0': '3d-plane-normal-x',
- 'Plane 1': '3d-plane-normal-y',
- 'Plane 2': '3d-plane-normal-z',
- '-': '3d-plane'}
+ _PLANE_ICONS = {
+ "Plane 0": "3d-plane-normal-x",
+ "Plane 1": "3d-plane-normal-y",
+ "Plane 2": "3d-plane-normal-z",
+ "-": "3d-plane",
+ }
"""Mapping of plane names to normals"""
def __init__(self, item):
super(PlaneRow, self).__init__(
item=item,
- name='Plane',
+ name="Plane",
fget=self.__getPlaneName,
fset=self.__setPlaneName,
events=items.ItemChangedType.POSITION,
- editorHint=tuple(self._PLANES.keys()))
+ editorHint=tuple(self._PLANES.keys()),
+ )
self._item = weakref.ref(item)
self._lastName = None
@@ -1104,7 +1160,7 @@ class PlaneRow(ItemProxyRow):
for name, normal in self._PLANES.items():
if numpy.array_equal(planeNormal, normal):
return name
- return '-'
+ return "-"
def __setPlaneName(self, data):
"""Set plane normal according to given plane name
@@ -1132,18 +1188,20 @@ class ComplexModeRow(ItemProxyRow):
:param Item3D item: Scene item with symbol property
"""
- def __init__(self, item, name='Mode'):
- names = [m.value.replace('_', ' ').title()
- for m in item.supportedComplexModes()]
+ def __init__(self, item, name="Mode"):
+ names = [
+ m.value.replace("_", " ").title() for m in item.supportedComplexModes()
+ ]
super(ComplexModeRow, self).__init__(
item=item,
name=name,
fget=item.getComplexMode,
fset=item.setComplexMode,
events=items.ItemChangedType.COMPLEX_MODE,
- toModelData=lambda data: data.value.replace('_', ' ').title(),
- fromModelData=lambda data: data.lower().replace(' ', '_'),
- editorHint=names)
+ toModelData=lambda data: data.value.replace("_", " ").title(),
+ fromModelData=lambda data: data.lower().replace(" ", "_"),
+ editorHint=names,
+ )
class RemoveIsosurfaceRow(BaseRow):
@@ -1164,7 +1222,7 @@ class RemoveIsosurfaceRow(BaseRow):
layout.setSpacing(0)
removeBtn = qt.QToolButton()
- removeBtn.setText('Delete')
+ removeBtn.setText("Delete")
removeBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
layout.addWidget(removeBtn)
removeBtn.clicked.connect(self._removeClicked)
@@ -1219,28 +1277,37 @@ class IsosurfaceRow(Item3DRow):
item.sigItemChanged.connect(self._levelChanged)
- self.addRow(ItemProxyRow(
- item=item,
- name='Level',
- fget=self._getValueForLevelSlider,
- fset=self._setLevelFromSliderValue,
- events=items.Item3DChangedType.ISO_LEVEL,
- editorHint=self._LEVEL_SLIDER_RANGE))
-
- self.addRow(ItemColorProxyRow(
- item=item,
- name='Color',
- fget=self._rgbColor,
- fset=self._setRgbColor,
- events=items.ItemChangedType.COLOR))
-
- self.addRow(ItemProxyRow(
- item=item,
- name='Opacity',
- fget=self._opacity,
- fset=self._setOpacity,
- events=items.ItemChangedType.COLOR,
- editorHint=(0, 255)))
+ self.addRow(
+ ItemProxyRow(
+ item=item,
+ name="Level",
+ fget=self._getValueForLevelSlider,
+ fset=self._setLevelFromSliderValue,
+ events=items.Item3DChangedType.ISO_LEVEL,
+ editorHint=self._LEVEL_SLIDER_RANGE,
+ )
+ )
+
+ self.addRow(
+ ItemColorProxyRow(
+ item=item,
+ name="Color",
+ fget=self._rgbColor,
+ fset=self._setRgbColor,
+ events=items.ItemChangedType.COLOR,
+ )
+ )
+
+ self.addRow(
+ ItemProxyRow(
+ item=item,
+ name="Opacity",
+ fget=self._opacity,
+ fset=self._setOpacity,
+ events=items.ItemChangedType.COLOR,
+ editorHint=(0, 255),
+ )
+ )
self.addRow(RemoveIsosurfaceRow(item))
@@ -1259,7 +1326,7 @@ class IsosurfaceRow(Item3DRow):
if dataMax != dataMin:
offset = (item.getLevel() - dataMin) / (dataMax - dataMin)
else:
- offset = 0.
+ offset = 0.0
sliderMin, sliderMax = self._LEVEL_SLIDER_RANGE
value = sliderMin + (sliderMax - sliderMin) * offset
@@ -1345,8 +1412,8 @@ class IsosurfaceRow(Item3DRow):
return self._rgbColor()
elif column == 1 and role in (qt.Qt.DisplayRole, qt.Qt.EditRole):
- item = self.item()
- return None if item is None else item.getLevel()
+ item = self.item()
+ return None if item is None else item.getLevel()
return super(IsosurfaceRow, self).data(column, role)
@@ -1366,9 +1433,11 @@ class ComplexIsosurfaceRow(IsosurfaceRow):
:param ComplexIsosurface item:
"""
- _EVENTS = (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.COLOR,
- items.ItemChangedType.COMPLEX_MODE)
+ _EVENTS = (
+ items.ItemChangedType.VISIBLE,
+ items.ItemChangedType.COLOR,
+ items.ItemChangedType.COMPLEX_MODE,
+ )
"""Events for which to update the first column in the tree"""
def __init__(self, item):
@@ -1418,8 +1487,10 @@ class ComplexIsosurfaceRow(IsosurfaceRow):
def data(self, column, role):
if column == 0 and role == qt.Qt.DecorationRole:
item = self.item()
- if (item is not None and
- item.getComplexMode() != items.ComplexMixIn.ComplexMode.NONE):
+ if (
+ item is not None
+ and item.getComplexMode() != items.ComplexMixIn.ComplexMode.NONE
+ ):
return self._colormapRow.getColormapImage()
return super(ComplexIsosurfaceRow, self).data(column, role)
@@ -1444,7 +1515,7 @@ class AddIsosurfaceRow(BaseRow):
layout.setSpacing(0)
addBtn = qt.QToolButton()
- addBtn.setText('+')
+ addBtn.setText("+")
addBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
layout.addWidget(addBtn)
addBtn.clicked.connect(self._addClicked)
@@ -1477,11 +1548,9 @@ class AddIsosurfaceRow(BaseRow):
if volume is not None:
dataRange = volume.getDataRange()
if dataRange is None:
- dataRange = 0., 1.
+ dataRange = 0.0, 1.0
- volume.addIsosurface(
- numpy.mean((dataRange[0], dataRange[-1])),
- '#0000FF')
+ volume.addIsosurface(numpy.mean((dataRange[0], dataRange[-1])), "#0000FF")
class VolumeIsoSurfacesRow(StaticRow):
@@ -1492,8 +1561,7 @@ class VolumeIsoSurfacesRow(StaticRow):
"""
def __init__(self, volume):
- super(VolumeIsoSurfacesRow, self).__init__(
- ('Isosurfaces', None))
+ super(VolumeIsoSurfacesRow, self).__init__(("Isosurfaces", None))
self._volume = weakref.ref(volume)
volume.sigIsosurfaceAdded.connect(self._isosurfaceAdded)
@@ -1554,7 +1622,7 @@ class Scatter2DPropertyMixInRow(object):
"""
def __init__(self, item, propertyName):
- assert propertyName in ('lineWidth', 'symbol', 'symbolSize')
+ assert propertyName in ("lineWidth", "symbol", "symbolSize")
self.__propertyName = propertyName
self.__isEnabled = item.isPropertyEnabled(propertyName)
@@ -1603,7 +1671,7 @@ class Scatter2DSymbolRow(Scatter2DPropertyMixInRow, SymbolRow):
def __init__(self, item):
SymbolRow.__init__(self, item)
- Scatter2DPropertyMixInRow.__init__(self, item, 'symbol')
+ Scatter2DPropertyMixInRow.__init__(self, item, "symbol")
class Scatter2DSymbolSizeRow(Scatter2DPropertyMixInRow, SymbolSizeRow):
@@ -1616,7 +1684,7 @@ class Scatter2DSymbolSizeRow(Scatter2DPropertyMixInRow, SymbolSizeRow):
def __init__(self, item):
SymbolSizeRow.__init__(self, item)
- Scatter2DPropertyMixInRow.__init__(self, item, 'symbolSize')
+ Scatter2DPropertyMixInRow.__init__(self, item, "symbolSize")
class Scatter2DLineWidth(Scatter2DPropertyMixInRow, ItemProxyRow):
@@ -1629,14 +1697,16 @@ class Scatter2DLineWidth(Scatter2DPropertyMixInRow, ItemProxyRow):
def __init__(self, item):
# TODO link editorHint with OpenGL max line width
- ItemProxyRow.__init__(self,
- item=item,
- name='Line width',
- fget=item.getLineWidth,
- fset=item.setLineWidth,
- events=items.ItemChangedType.LINE_WIDTH,
- editorHint=(1, 10))
- Scatter2DPropertyMixInRow.__init__(self, item, 'lineWidth')
+ ItemProxyRow.__init__(
+ self,
+ item=item,
+ name="Line width",
+ fget=item.getLineWidth,
+ fset=item.setLineWidth,
+ events=items.ItemChangedType.LINE_WIDTH,
+ editorHint=(1, 10),
+ )
+ Scatter2DPropertyMixInRow.__init__(self, item, "lineWidth")
def initScatter2DNode(node, item):
@@ -1645,22 +1715,28 @@ def initScatter2DNode(node, item):
:param Item3DRow node: The model node to setup
:param Scatter2D item: The Scatter2D the node is representing
"""
- node.addRow(ItemProxyRow(
- item=item,
- name='Mode',
- fget=item.getVisualization,
- fset=item.setVisualization,
- events=items.ItemChangedType.VISUALIZATION_MODE,
- editorHint=[m.value.title() for m in item.supportedVisualizations()],
- toModelData=lambda data: data.value.title(),
- fromModelData=lambda data: data.lower()))
-
- node.addRow(ItemProxyRow(
- item=item,
- name='Height map',
- fget=item.isHeightMap,
- fset=item.setHeightMap,
- events=items.Item3DChangedType.HEIGHT_MAP))
+ node.addRow(
+ ItemProxyRow(
+ item=item,
+ name="Mode",
+ fget=item.getVisualization,
+ fset=item.setVisualization,
+ events=items.ItemChangedType.VISUALIZATION_MODE,
+ editorHint=[m.value.title() for m in item.supportedVisualizations()],
+ toModelData=lambda data: data.value.title(),
+ fromModelData=lambda data: data.lower(),
+ )
+ )
+
+ node.addRow(
+ ItemProxyRow(
+ item=item,
+ name="Height map",
+ fget=item.isHeightMap,
+ fset=item.setHeightMap,
+ events=items.Item3DChangedType.HEIGHT_MAP,
+ )
+ )
node.addRow(ColormapRow(item))
@@ -1694,12 +1770,15 @@ def initVolumeCutPlaneNode(node, item):
node.addRow(ColormapRow(item))
- node.addRow(ItemProxyRow(
- item=item,
- name='Show <=Min',
- fget=item.getDisplayValuesBelowMin,
- fset=item.setDisplayValuesBelowMin,
- events=items.ItemChangedType.ALPHA))
+ node.addRow(
+ ItemProxyRow(
+ item=item,
+ name="Show <=Min",
+ fget=item.getDisplayValuesBelowMin,
+ fset=item.setDisplayValuesBelowMin,
+ events=items.ItemChangedType.ALPHA,
+ )
+ )
node.addRow(InterpolationRow(item))
diff --git a/src/silx/gui/plot3d/_model/model.py b/src/silx/gui/plot3d/_model/model.py
index 186838f..2c687f2 100644
--- a/src/silx/gui/plot3d/_model/model.py
+++ b/src/silx/gui/plot3d/_model/model.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
@@ -26,8 +25,6 @@
This module provides the :class:`SceneWidget` content and parameters model.
"""
-from __future__ import absolute_import, division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "11/01/2018"
@@ -179,6 +176,6 @@ class SceneModel(qt.QAbstractItemModel):
def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
"""See :meth:`QAbstractItemModel.headerData`"""
if orientation == qt.Qt.Horizontal and role == qt.Qt.DisplayRole:
- return 'Item' if section == 0 else 'Value'
+ return "Item" if section == 0 else "Value"
else:
return None
diff --git a/src/silx/gui/plot3d/actions/Plot3DAction.py b/src/silx/gui/plot3d/actions/Plot3DAction.py
index 94b9572..a2ee93c 100644
--- a/src/silx/gui/plot3d/actions/Plot3DAction.py
+++ b/src/silx/gui/plot3d/actions/Plot3DAction.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""Base class for QAction attached to a Plot3DWidget."""
-from __future__ import absolute_import, division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "06/09/2017"
diff --git a/src/silx/gui/plot3d/actions/__init__.py b/src/silx/gui/plot3d/actions/__init__.py
index 26243cf..e6c7312 100644
--- a/src/silx/gui/plot3d/actions/__init__.py
+++ b/src/silx/gui/plot3d/actions/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot3d/actions/io.py b/src/silx/gui/plot3d/actions/io.py
index 25f4ade..3c6212f 100644
--- a/src/silx/gui/plot3d/actions/io.py
+++ b/src/silx/gui/plot3d/actions/io.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,8 +26,6 @@
It provides QAction to copy, save (snapshot and video), print a Plot3DWidget.
"""
-from __future__ import absolute_import, division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "06/09/2017"
@@ -60,9 +57,9 @@ class CopyAction(Plot3DAction):
def __init__(self, parent, plot3d=None):
super(CopyAction, self).__init__(parent, plot3d)
- self.setIcon(getQIcon('edit-copy'))
- self.setText('Copy')
- self.setToolTip('Copy a snapshot of the 3D scene to the clipboard')
+ self.setIcon(getQIcon("edit-copy"))
+ self.setText("Copy")
+ self.setToolTip("Copy a snapshot of the 3D scene to the clipboard")
self.setCheckable(False)
self.setShortcut(qt.QKeySequence.Copy)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -71,7 +68,7 @@ class CopyAction(Plot3DAction):
def _triggered(self, checked=False):
plot3d = self.getPlot3DWidget()
if plot3d is None:
- _logger.error('Cannot copy widget, no associated Plot3DWidget')
+ _logger.error("Cannot copy widget, no associated Plot3DWidget")
else:
image = plot3d.grabGL()
qt.QApplication.clipboard().setImage(image)
@@ -88,9 +85,9 @@ class SaveAction(Plot3DAction):
def __init__(self, parent, plot3d=None):
super(SaveAction, self).__init__(parent, plot3d)
- self.setIcon(getQIcon('document-save'))
- self.setText('Save...')
- self.setToolTip('Save a snapshot of the 3D scene')
+ self.setIcon(getQIcon("document-save"))
+ self.setText("Save...")
+ self.setToolTip("Save a snapshot of the 3D scene")
self.setCheckable(False)
self.setShortcut(qt.QKeySequence.Save)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -99,13 +96,14 @@ class SaveAction(Plot3DAction):
def _triggered(self, checked=False):
plot3d = self.getPlot3DWidget()
if plot3d is None:
- _logger.error('Cannot save widget, no associated Plot3DWidget')
+ _logger.error("Cannot save widget, no associated Plot3DWidget")
else:
dialog = qt.QFileDialog(self.parent())
- dialog.setWindowTitle('Save snapshot as')
+ dialog.setWindowTitle("Save snapshot as")
dialog.setModal(True)
- dialog.setNameFilters(('Plot3D Snapshot PNG (*.png)',
- 'Plot3D Snapshot JPEG (*.jpg)'))
+ dialog.setNameFilters(
+ ("Plot3D Snapshot PNG (*.png)", "Plot3D Snapshot JPEG (*.jpg)")
+ )
dialog.setFileMode(qt.QFileDialog.AnyFile)
dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
@@ -119,17 +117,18 @@ class SaveAction(Plot3DAction):
# 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()):
+ if (
+ len(filename) <= len(extension)
+ or filename[-len(extension) :].lower() != extension.lower()
+ ):
filename += extension
image = plot3d.grabGL()
if not image.save(filename):
- _logger.error('Failed to save image as %s', filename)
+ _logger.error("Failed to save image as %s", filename)
qt.QMessageBox.critical(
- self.parent(),
- 'Save snapshot as',
- 'Failed to save snapshot')
+ self.parent(), "Save snapshot as", "Failed to save snapshot"
+ )
class PrintAction(Plot3DAction):
@@ -143,9 +142,9 @@ class PrintAction(Plot3DAction):
def __init__(self, parent, plot3d=None):
super(PrintAction, self).__init__(parent, plot3d)
- self.setIcon(getQIcon('document-print'))
- self.setText('Print...')
- self.setToolTip('Print a snapshot of the 3D scene')
+ self.setIcon(getQIcon("document-print"))
+ self.setText("Print...")
+ self.setToolTip("Print a snapshot of the 3D scene")
self.setCheckable(False)
self.setShortcut(qt.QKeySequence.Print)
self.setShortcutContext(qt.Qt.WidgetShortcut)
@@ -161,11 +160,11 @@ class PrintAction(Plot3DAction):
def _triggered(self, checked=False):
plot3d = self.getPlot3DWidget()
if plot3d is None:
- _logger.error('Cannot print widget, no associated Plot3DWidget')
+ _logger.error("Cannot print widget, no associated Plot3DWidget")
else:
printer = self.getPrinter()
dialog = qt.QPrintDialog(printer, plot3d)
- dialog.setWindowTitle('Print Plot3D snapshot')
+ dialog.setWindowTitle("Print Plot3D snapshot")
if not dialog.exec():
return
@@ -177,19 +176,15 @@ class PrintAction(Plot3DAction):
return
pageRect = printer.pageRect(qt.QPrinter.DevicePixel)
- if (pageRect.width() < image.width() or
- pageRect.height() < image.height()):
+ if pageRect.width() < image.width() or pageRect.height() < image.height():
# Downscale to page
xScale = pageRect.width() / image.width()
yScale = pageRect.height() / image.height()
scale = min(xScale, yScale)
else:
- scale = 1.
+ scale = 1.0
- rect = qt.QRectF(0,
- 0,
- scale * image.width(),
- scale * image.height())
+ rect = qt.QRectF(0, 0, scale * image.width(), scale * image.height())
painter.drawImage(rect, image)
painter.end()
@@ -204,15 +199,14 @@ class VideoAction(Plot3DAction):
Plot3DWidget the action is associated with
"""
- PNG_SERIE_FILTER = 'Serie of PNG files (*.png)'
- MNG_FILTER = 'Multiple-image Network Graphics file (*.mng)'
+ PNG_SERIE_FILTER = "Serie of PNG files (*.png)"
+ MNG_FILTER = "Multiple-image Network Graphics file (*.mng)"
def __init__(self, parent, plot3d=None):
super(VideoAction, self).__init__(parent, plot3d)
- self.setText('Record video..')
- self.setIcon(getQIcon('camera'))
- self.setToolTip(
- 'Record a video of a 360 degrees rotation of the 3D scene.')
+ self.setText("Record video..")
+ self.setIcon(getQIcon("camera"))
+ self.setToolTip("Record a video of a 360 degrees rotation of the 3D scene.")
self.setCheckable(False)
self.triggered[bool].connect(self._triggered)
@@ -220,17 +214,15 @@ class VideoAction(Plot3DAction):
"""Action triggered callback"""
plot3d = self.getPlot3DWidget()
if plot3d is None:
- _logger.warning(
- 'Ignoring action triggered without Plot3DWidget set')
+ _logger.warning("Ignoring action triggered without Plot3DWidget set")
return
dialog = qt.QFileDialog(parent=plot3d)
- dialog.setWindowTitle('Save video as...')
+ dialog.setWindowTitle("Save video as...")
dialog.setModal(True)
- dialog.setNameFilters([self.PNG_SERIE_FILTER,
- self.MNG_FILTER])
- dialog.setFileMode(dialog.AnyFile)
- dialog.setAcceptMode(dialog.AcceptSave)
+ dialog.setNameFilters([self.PNG_SERIE_FILTER, self.MNG_FILTER])
+ dialog.setFileMode(qt.QFileDialog.AnyFile)
+ dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
if not dialog.exec():
return
@@ -240,18 +232,20 @@ class VideoAction(Plot3DAction):
# 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()):
+ if (
+ len(filename) <= len(extension)
+ or filename[-len(extension) :].lower() != extension.lower()
+ ):
filename += extension
- nbFrames = int(4. * 25) # 4 seconds, 25 fps
+ nbFrames = int(4.0 * 25) # 4 seconds, 25 fps
if nameFilter == self.PNG_SERIE_FILTER:
self._saveAsPNGSerie(filename, nbFrames)
elif nameFilter == self.MNG_FILTER:
self._saveAsMNG(filename, nbFrames)
else:
- _logger.error('Unsupported file filter: %s', nameFilter)
+ _logger.error("Unsupported file filter: %s", nameFilter)
def _saveAsPNGSerie(self, filename, nbFrames):
"""Save video as serie of PNG files.
@@ -266,10 +260,11 @@ class VideoAction(Plot3DAction):
# Define filename template
nbDigits = int(numpy.log10(nbFrames)) + 1
- indexFormat = '%%0%dd' % nbDigits
- extensionIndex = filename.rfind('.')
- filenameFormat = \
+ indexFormat = "%%0%dd" % nbDigits
+ extensionIndex = filename.rfind(".")
+ filenameFormat = (
filename[:extensionIndex] + indexFormat + filename[extensionIndex:]
+ )
try:
for index, image in enumerate(self._video360(nbFrames)):
@@ -288,7 +283,7 @@ class VideoAction(Plot3DAction):
frames = (convertQImageToArray(im) for im in self._video360(nbFrames))
try:
- with open(filename, 'wb') as file_:
+ with open(filename, "wb") as file_:
for chunk in mng.convert(frames, nb_images=nbFrames):
file_.write(chunk)
except GeneratorExit:
@@ -303,11 +298,11 @@ class VideoAction(Plot3DAction):
plot3d = self.getPlot3DWidget()
assert plot3d is not None
- angleStep = 360. / nbFrames
+ angleStep = 360.0 / nbFrames
# Create progress bar dialog
dialog = qt.QDialog(plot3d)
- dialog.setWindowTitle('Record Video')
+ dialog.setWindowTitle("Record Video")
layout = qt.QVBoxLayout(dialog)
progress = qt.QProgressBar()
progress.setRange(0, nbFrames)
@@ -326,7 +321,7 @@ class VideoAction(Plot3DAction):
progress.setValue(frame)
image = plot3d.grabGL()
yield image
- plot3d.viewport.orbitCamera('left', angleStep)
+ plot3d.viewport.orbitCamera("left", angleStep)
qapp.processEvents()
if not dialog.isVisible():
break # It as been rejected by the abort button
@@ -334,4 +329,4 @@ class VideoAction(Plot3DAction):
dialog.accept()
if dialog.result() == qt.QDialog.Rejected:
- raise GeneratorExit('Aborted')
+ raise GeneratorExit("Aborted")
diff --git a/src/silx/gui/plot3d/actions/mode.py b/src/silx/gui/plot3d/actions/mode.py
index b9cd7c8..99a83b4 100644
--- a/src/silx/gui/plot3d/actions/mode.py
+++ b/src/silx/gui/plot3d/actions/mode.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ It provides QAction to rotate or pan a Plot3DWidget
as well as toggle a picking mode.
"""
-from __future__ import absolute_import, division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "06/09/2017"
@@ -66,8 +63,9 @@ class InteractiveModeAction(Plot3DAction):
plot3d = self.getPlot3DWidget()
if plot3d is None:
_logger.error(
- 'Cannot set %s interaction, no associated Plot3DWidget' %
- self._interaction)
+ "Cannot set %s interaction, no associated Plot3DWidget"
+ % self._interaction
+ )
else:
plot3d.setInteractiveMode(self._interaction)
self.setChecked(True)
@@ -77,8 +75,7 @@ class InteractiveModeAction(Plot3DAction):
# Disconnect from previous Plot3DWidget
plot3d = self.getPlot3DWidget()
if plot3d is not None:
- plot3d.sigInteractiveModeChanged.disconnect(
- self._interactiveModeChanged)
+ plot3d.sigInteractiveModeChanged.disconnect(self._interactiveModeChanged)
super(InteractiveModeAction, self).setPlot3DWidget(widget)
@@ -87,13 +84,12 @@ class InteractiveModeAction(Plot3DAction):
self.setChecked(False)
else:
self.setChecked(widget.getInteractiveMode() == self._interaction)
- widget.sigInteractiveModeChanged.connect(
- self._interactiveModeChanged)
+ widget.sigInteractiveModeChanged.connect(self._interactiveModeChanged)
def _interactiveModeChanged(self):
plot3d = self.getPlot3DWidget()
if plot3d is None:
- _logger.error('Received a signal while there is no widget')
+ _logger.error("Received a signal while there is no widget")
else:
self.setChecked(plot3d.getInteractiveMode() == self._interaction)
@@ -107,11 +103,11 @@ class RotateArcballAction(InteractiveModeAction):
"""
def __init__(self, parent, plot3d=None):
- super(RotateArcballAction, self).__init__(parent, 'rotate', plot3d)
+ super(RotateArcballAction, self).__init__(parent, "rotate", plot3d)
- self.setIcon(getQIcon('rotate-3d'))
- self.setText('Rotate')
- self.setToolTip('Rotate the view. Press <b>Ctrl</b> to pan.')
+ self.setIcon(getQIcon("rotate-3d"))
+ self.setText("Rotate")
+ self.setToolTip("Rotate the view. Press <b>Ctrl</b> to pan.")
class PanAction(InteractiveModeAction):
@@ -123,11 +119,11 @@ class PanAction(InteractiveModeAction):
"""
def __init__(self, parent, plot3d=None):
- super(PanAction, self).__init__(parent, 'pan', plot3d)
+ super(PanAction, self).__init__(parent, "pan", plot3d)
- self.setIcon(getQIcon('pan'))
- self.setText('Pan')
- self.setToolTip('Pan the view. Press <b>Ctrl</b> to rotate.')
+ self.setIcon(getQIcon("pan"))
+ self.setText("Pan")
+ self.setToolTip("Pan the view. Press <b>Ctrl</b> to rotate.")
class PickingModeAction(Plot3DAction):
@@ -148,9 +144,9 @@ class PickingModeAction(Plot3DAction):
def __init__(self, parent, plot3d=None):
super(PickingModeAction, self).__init__(parent, plot3d)
- self.setIcon(getQIcon('pointing-hand'))
- self.setText('Picking')
- self.setToolTip('Toggle picking with left button click')
+ self.setIcon(getQIcon("pointing-hand"))
+ self.setText("Picking")
+ self.setToolTip("Toggle picking with left button click")
self.setCheckable(True)
self.triggered[bool].connect(self._triggered)
diff --git a/src/silx/gui/plot3d/actions/viewpoint.py b/src/silx/gui/plot3d/actions/viewpoint.py
index d764c40..57a7c7a 100644
--- a/src/silx/gui/plot3d/actions/viewpoint.py
+++ b/src/silx/gui/plot3d/actions/viewpoint.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
@@ -27,8 +26,6 @@
It provides QAction to rotate or pan a Plot3DWidget.
"""
-from __future__ import absolute_import, division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "03/10/2017"
@@ -53,9 +50,10 @@ class _SetViewpointAction(Plot3DAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, face, plot3d=None):
super(_SetViewpointAction, self).__init__(parent, plot3d)
- assert face in ('side', 'front', 'back', 'left', 'right', 'top', 'bottom')
+ assert face in ("side", "front", "back", "left", "right", "top", "bottom")
self._face = face
self.setIconVisibleInMenu(True)
@@ -65,8 +63,7 @@ class _SetViewpointAction(Plot3DAction):
def _triggered(self, checked=False):
plot3d = self.getPlot3DWidget()
if plot3d is None:
- _logger.error(
- 'Cannot start/stop rotation, no associated Plot3DWidget')
+ _logger.error("Cannot start/stop rotation, no associated Plot3DWidget")
else:
plot3d.viewport.camera.extrinsic.reset(face=self._face)
plot3d.centerScene()
@@ -79,12 +76,13 @@ class FrontViewpointAction(_SetViewpointAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, plot3d=None):
- super(FrontViewpointAction, self).__init__(parent, 'front', plot3d)
+ super(FrontViewpointAction, self).__init__(parent, "front", plot3d)
- self.setIcon(getQIcon('cube-front'))
- self.setText('Front')
- self.setToolTip('View along the -Z axis')
+ self.setIcon(getQIcon("cube-front"))
+ self.setText("Front")
+ self.setToolTip("View along the -Z axis")
class BackViewpointAction(_SetViewpointAction):
@@ -94,12 +92,13 @@ class BackViewpointAction(_SetViewpointAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, plot3d=None):
- super(BackViewpointAction, self).__init__(parent, 'back', plot3d)
+ super(BackViewpointAction, self).__init__(parent, "back", plot3d)
- self.setIcon(getQIcon('cube-back'))
- self.setText('Back')
- self.setToolTip('View along the +Z axis')
+ self.setIcon(getQIcon("cube-back"))
+ self.setText("Back")
+ self.setToolTip("View along the +Z axis")
class LeftViewpointAction(_SetViewpointAction):
@@ -109,12 +108,13 @@ class LeftViewpointAction(_SetViewpointAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, plot3d=None):
- super(LeftViewpointAction, self).__init__(parent, 'left', plot3d)
+ super(LeftViewpointAction, self).__init__(parent, "left", plot3d)
- self.setIcon(getQIcon('cube-left'))
- self.setText('Left')
- self.setToolTip('View along the +X axis')
+ self.setIcon(getQIcon("cube-left"))
+ self.setText("Left")
+ self.setToolTip("View along the +X axis")
class RightViewpointAction(_SetViewpointAction):
@@ -124,12 +124,13 @@ class RightViewpointAction(_SetViewpointAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, plot3d=None):
- super(RightViewpointAction, self).__init__(parent, 'right', plot3d)
+ super(RightViewpointAction, self).__init__(parent, "right", plot3d)
- self.setIcon(getQIcon('cube-right'))
- self.setText('Right')
- self.setToolTip('View along the -X axis')
+ self.setIcon(getQIcon("cube-right"))
+ self.setText("Right")
+ self.setToolTip("View along the -X axis")
class TopViewpointAction(_SetViewpointAction):
@@ -139,12 +140,13 @@ class TopViewpointAction(_SetViewpointAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, plot3d=None):
- super(TopViewpointAction, self).__init__(parent, 'top', plot3d)
+ super(TopViewpointAction, self).__init__(parent, "top", plot3d)
- self.setIcon(getQIcon('cube-top'))
- self.setText('Top')
- self.setToolTip('View along the -Y axis')
+ self.setIcon(getQIcon("cube-top"))
+ self.setText("Top")
+ self.setToolTip("View along the -Y axis")
class BottomViewpointAction(_SetViewpointAction):
@@ -154,12 +156,13 @@ class BottomViewpointAction(_SetViewpointAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, plot3d=None):
- super(BottomViewpointAction, self).__init__(parent, 'bottom', plot3d)
+ super(BottomViewpointAction, self).__init__(parent, "bottom", plot3d)
- self.setIcon(getQIcon('cube-bottom'))
- self.setText('Bottom')
- self.setToolTip('View along the +Y axis')
+ self.setIcon(getQIcon("cube-bottom"))
+ self.setText("Bottom")
+ self.setToolTip("View along the +Y axis")
class SideViewpointAction(_SetViewpointAction):
@@ -169,12 +172,13 @@ class SideViewpointAction(_SetViewpointAction):
:param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
Plot3DWidget the action is associated with
"""
+
def __init__(self, parent, plot3d=None):
- super(SideViewpointAction, self).__init__(parent, 'side', plot3d)
+ super(SideViewpointAction, self).__init__(parent, "side", plot3d)
- self.setIcon(getQIcon('cube'))
- self.setText('Side')
- self.setToolTip('Side view')
+ self.setIcon(getQIcon("cube"))
+ self.setText("Side")
+ self.setToolTip("Side view")
class RotateViewpoint(Plot3DAction):
@@ -188,7 +192,7 @@ class RotateViewpoint(Plot3DAction):
_TIMEOUT_MS = 50
"""Time interval between to frames (in milliseconds)"""
- _DEGREE_PER_SECONDS = 360. / 5.
+ _DEGREE_PER_SECONDS = 360.0 / 5.0
"""Rotation speed of the animation"""
def __init__(self, parent, plot3d=None):
@@ -200,18 +204,16 @@ class RotateViewpoint(Plot3DAction):
self._timer.setInterval(self._TIMEOUT_MS) # 20fps
self._timer.timeout.connect(self._rotate)
- self.setIcon(getQIcon('cube-rotate'))
- self.setText('Rotate scene')
- self.setToolTip('Rotate the 3D scene around the vertical axis')
+ self.setIcon(getQIcon("cube-rotate"))
+ self.setText("Rotate scene")
+ self.setToolTip("Rotate the 3D scene around the vertical axis")
self.setCheckable(True)
self.triggered[bool].connect(self._triggered)
-
def _triggered(self, checked=False):
plot3d = self.getPlot3DWidget()
if plot3d is None:
- _logger.error(
- 'Cannot start/stop rotation, no associated Plot3DWidget')
+ _logger.error("Cannot start/stop rotation, no associated Plot3DWidget")
elif checked:
self._previousTime = time.time()
self._timer.start()
@@ -222,10 +224,10 @@ class RotateViewpoint(Plot3DAction):
def _rotate(self):
"""Perform a step of the rotation"""
if self._previousTime is None:
- _logger.error('Previous time not set!')
- angleStep = 0.
+ _logger.error("Previous time not set!")
+ angleStep = 0.0
else:
angleStep = self._DEGREE_PER_SECONDS * (time.time() - self._previousTime)
- self.getPlot3DWidget().viewport.orbitCamera('left', angleStep)
+ self.getPlot3DWidget().viewport.orbitCamera("left", angleStep)
self._previousTime = time.time()
diff --git a/src/silx/gui/plot3d/conftest.py b/src/silx/gui/plot3d/conftest.py
index da02238..37c35d5 100644
--- a/src/silx/gui/plot3d/conftest.py
+++ b/src/silx/gui/plot3d/conftest.py
@@ -1,5 +1,6 @@
import pytest
+
@pytest.mark.usefixtures("use_opengl")
def setup_module(module):
pass
diff --git a/src/silx/gui/plot3d/items/__init__.py b/src/silx/gui/plot3d/items/__init__.py
index e7c4af1..b091ffc 100644
--- a/src/silx/gui/plot3d/items/__init__.py
+++ b/src/silx/gui/plot3d/items/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This package provides classes that describes :class:`.SceneWidget` content.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/11/2017"
@@ -34,8 +31,13 @@ __date__ = "15/11/2017"
from .core import DataItem3D, Item3D, GroupItem, GroupWithAxesItem # noqa
from .core import ItemChangedType, Item3DChangedType # noqa
-from .mixins import (ColormapMixIn, ComplexMixIn, InterpolationMixIn, # noqa
- PlaneMixIn, SymbolMixIn) # noqa
+from .mixins import (
+ ColormapMixIn,
+ ComplexMixIn,
+ InterpolationMixIn, # noqa
+ PlaneMixIn,
+ SymbolMixIn,
+) # noqa
from .clipplane import ClipPlane # noqa
from .image import ImageData, ImageRgba, HeightMapData, HeightMapRGBA # noqa
from .mesh import Mesh, ColormapMesh, Box, Cylinder, Hexagon # noqa
diff --git a/src/silx/gui/plot3d/items/_pick.py b/src/silx/gui/plot3d/items/_pick.py
index 0d6a495..aad5daf 100644
--- a/src/silx/gui/plot3d/items/_pick.py
+++ b/src/silx/gui/plot3d/items/_pick.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides classes supporting item picking.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/09/2018"
@@ -56,7 +53,7 @@ class PickContext(object):
self._widgetPosition = x, y
assert isinstance(viewport, Viewport)
self._viewport = viewport
- self._ndcZRange = -1., 1.
+ self._ndcZRange = -1.0, 1.0
self._enabled = True
self._condition = condition
@@ -111,7 +108,7 @@ class PickContext(object):
"""
return self._enabled
- def setNDCZRange(self, near=-1., far=1.):
+ def setNDCZRange(self, near=-1.0, far=1.0):
"""Set near and far Z value in normalized device coordinates
This allows to clip the ray to a subset of the NDC range
@@ -145,36 +142,33 @@ class PickContext(object):
or None if picked point is outside viewport
:rtype: Union[None,numpy.ndarray]
"""
- assert frame in ('ndc', 'camera', 'scene') or isinstance(frame, Base)
+ assert frame in ("ndc", "camera", "scene") or isinstance(frame, Base)
positionNdc = self.getNDCPosition()
if positionNdc is None:
return None
near, far = self._ndcZRange
- rayNdc = numpy.array((positionNdc + (near, 1.),
- positionNdc + (far, 1.)),
- dtype=numpy.float64)
- if frame == 'ndc':
+ rayNdc = numpy.array(
+ (positionNdc + (near, 1.0), positionNdc + (far, 1.0)), dtype=numpy.float64
+ )
+ if frame == "ndc":
return rayNdc
viewport = self.getViewport()
rayCamera = viewport.camera.intrinsic.transformPoints(
- rayNdc,
- direct=False,
- perspectiveDivide=True)
- if frame == 'camera':
+ rayNdc, direct=False, perspectiveDivide=True
+ )
+ if frame == "camera":
return rayCamera
- rayScene = viewport.camera.extrinsic.transformPoints(
- rayCamera, direct=False)
- if frame == 'scene':
+ rayScene = viewport.camera.extrinsic.transformPoints(rayCamera, direct=False)
+ if frame == "scene":
return rayScene
# frame is a scene Base object
- rayObject = frame.objectToSceneTransform.transformPoints(
- rayScene, direct=False)
+ rayObject = frame.objectToSceneTransform.transformPoints(rayScene, direct=False)
return rayObject
@@ -196,8 +190,7 @@ class PickingResult(_PickingResult):
"""
super(PickingResult, self).__init__(item, indices)
- self._objectPositions = numpy.array(
- positions, copy=False, dtype=numpy.float64)
+ self._objectPositions = numpy.array(positions, copy=False, dtype=numpy.float64)
# Store matrices to generate positions on demand
primitive = item._getScenePrimitive()
@@ -222,7 +215,7 @@ class PickingResult(_PickingResult):
item = self.getItem()
if self._fetchdata is None:
- if hasattr(item, 'getData'):
+ if hasattr(item, "getData"):
data = item.getData(copy=False)
else:
return None
@@ -231,7 +224,7 @@ class PickingResult(_PickingResult):
return numpy.array(data[indices], copy=copy)
- def getPositions(self, frame='scene', copy=True):
+ def getPositions(self, frame="scene", copy=True):
"""Returns picking positions in item coordinates.
:param str frame: The frame in which the positions are returned
@@ -242,24 +235,26 @@ class PickingResult(_PickingResult):
:return: Nx3 array of (x, y, z) coordinates
:rtype: numpy.ndarray
"""
- if frame == 'ndc':
+ if frame == "ndc":
if self._ndcPositions is None: # Lazy-loading
self._ndcPositions = self._objectToNDCTransform.transformPoints(
- self._objectPositions, perspectiveDivide=True)
+ self._objectPositions, perspectiveDivide=True
+ )
positions = self._ndcPositions
- elif frame == 'scene':
+ elif frame == "scene":
if self._scenePositions is None: # Lazy-loading
self._scenePositions = self._objectToSceneTransform.transformPoints(
- self._objectPositions)
+ self._objectPositions
+ )
positions = self._scenePositions
- elif frame == 'object':
+ elif frame == "object":
positions = self._objectPositions
else:
- raise ValueError('Unsupported frame argument: %s' % str(frame))
+ raise ValueError("Unsupported frame argument: %s" % str(frame))
return numpy.array(positions, copy=copy)
diff --git a/src/silx/gui/plot3d/items/clipplane.py b/src/silx/gui/plot3d/items/clipplane.py
index 3e819d0..283230b 100644
--- a/src/silx/gui/plot3d/items/clipplane.py
+++ b/src/silx/gui/plot3d/items/clipplane.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides a scene clip plane class.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/11/2017"
@@ -50,7 +47,8 @@ class ClipPlane(Item3D, PlaneMixIn):
def __init__(self, parent=None):
plane = primitives.ClipPlane()
Item3D.__init__(self, parent=parent, primitive=plane)
- PlaneMixIn.__init__(self, plane=plane)
+ PlaneMixIn.__init__(self)
+ self._setPlane(plane)
def __pickPreProcessing(self, context):
"""Common processing for :meth:`_pickPostProcess` and :meth:`_pickFull`
@@ -76,12 +74,15 @@ class ClipPlane(Item3D, PlaneMixIn):
rayObject[0, :3],
rayObject[1, :3],
planeNorm=self.getNormal(),
- planePt=self.getPoint())
+ planePt=self.getPoint(),
+ )
# A single intersection inside bounding box
- picked = (len(points) == 1 and
- numpy.all(bounds[0] <= points[0]) and
- numpy.all(points[0] <= bounds[1]))
+ picked = (
+ len(points) == 1
+ and numpy.all(bounds[0] <= points[0])
+ and numpy.all(points[0] <= bounds[1])
+ )
return picked, points, rayObject
@@ -99,18 +100,20 @@ class ClipPlane(Item3D, PlaneMixIn):
if picked: # A single intersection inside bounding box
# Clip NDC z range for following brother items
ndcIntersect = plane.objectToNDCTransform.transformPoint(
- points[0], perspectiveDivide=True)
+ points[0], perspectiveDivide=True
+ )
ndcNormal = plane.objectToNDCTransform.transformNormal(
- self.getNormal())
+ self.getNormal()
+ )
if ndcNormal[2] < 0:
- context.setNDCZRange(-1., ndcIntersect[2])
+ context.setNDCZRange(-1.0, ndcIntersect[2])
else:
- context.setNDCZRange(ndcIntersect[2], 1.)
+ context.setNDCZRange(ndcIntersect[2], 1.0)
else:
# TODO check this might not be correct
- rayObject[:, 3] = 1. # Make sure 4h coordinate is one
- if numpy.sum(rayObject[0] * self.getParameters()) < 0.:
+ rayObject[:, 3] = 1.0 # Make sure 4h coordinate is one
+ if numpy.sum(rayObject[0] * self.getParameters()) < 0.0:
# Disable picking for remaining brothers
context.setEnabled(False)
diff --git a/src/silx/gui/plot3d/items/core.py b/src/silx/gui/plot3d/items/core.py
index 0388ce7..4caf41d 100644
--- a/src/silx/gui/plot3d/items/core.py
+++ b/src/silx/gui/plot3d/items/core.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides the base class for items of the :class:`.SceneWidget`.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/11/2017"
@@ -47,25 +44,25 @@ from ._pick import PickContext
class Item3DChangedType(enum.Enum):
"""Type of modification provided by :attr:`Item3D.sigItemChanged` signal."""
- INTERPOLATION = 'interpolationChanged'
+ INTERPOLATION = "interpolationChanged"
"""Item3D image interpolation changed flag."""
- TRANSFORM = 'transformChanged'
+ TRANSFORM = "transformChanged"
"""Item3D transform changed flag."""
- HEIGHT_MAP = 'heightMapChanged'
+ HEIGHT_MAP = "heightMapChanged"
"""Item3D height map changed flag."""
- ISO_LEVEL = 'isoLevelChanged'
+ ISO_LEVEL = "isoLevelChanged"
"""Isosurface level changed flag."""
- LABEL = 'labelChanged'
+ LABEL = "labelChanged"
"""Item's label changed flag."""
- BOUNDING_BOX_VISIBLE = 'boundingBoxVisibleChanged'
+ BOUNDING_BOX_VISIBLE = "boundingBoxVisibleChanged"
"""Item's bounding box visibility changed"""
- ROOT_ITEM = 'rootItemChanged'
+ ROOT_ITEM = "rootItemChanged"
"""Item's root changed flag."""
@@ -88,7 +85,9 @@ class Item3D(qt.QObject):
"""
def __init__(self, parent, primitive=None):
- qt.QObject.__init__(self, parent)
+ qt.QObject.__init__(self)
+ if parent is not None:
+ self.setParent(parent)
if primitive is None:
primitive = scene.Group()
@@ -100,12 +99,9 @@ class Item3D(qt.QObject):
labelIndex = self._LABEL_INDICES[self.__class__]
self._label = str(self.__class__.__name__)
if labelIndex != 0:
- self._label += u' %d' % labelIndex
+ self._label += " %d" % labelIndex
self._LABEL_INDICES[self.__class__] += 1
- if isinstance(parent, Item3D):
- parent.sigItemChanged.connect(self.__parentItemChanged)
-
def setParent(self, parent):
"""Override set parent to handle root item change"""
previousParent = self.parent()
@@ -206,7 +202,7 @@ class Item3D(qt.QObject):
:param color: RGBA color
:type color: tuple of 4 float in [0., 1.]
"""
- if hasattr(super(Item3D, self), '_setForegroundColor'):
+ if hasattr(super(Item3D, self), "_setForegroundColor"):
super(Item3D, self)._setForegroundColor(color)
def __syncForegroundColor(self):
@@ -216,8 +212,7 @@ class Item3D(qt.QObject):
if root is not None:
widget = root.parent()
if isinstance(widget, qt.QWidget):
- self._setForegroundColor(
- widget.getForegroundColor().getRgbF())
+ self._setForegroundColor(widget.getForegroundColor().getRgbF())
# picking
@@ -228,10 +223,12 @@ class Item3D(qt.QObject):
:return: Data indices at picked position or None
:rtype: Union[None,PickingResult]
"""
- if (self.isVisible() and
- context.isEnabled() and
- context.isItemPickable(self) and
- self._pickFastCheck(context)):
+ if (
+ self.isVisible()
+ and context.isEnabled()
+ and context.isItemPickable(self)
+ and self._pickFastCheck(context)
+ ):
return self._pickFull(context)
return None
@@ -254,8 +251,10 @@ class Item3D(qt.QObject):
bounds = primitive.objectToNDCTransform.transformBounds(bounds)
- return (bounds[0, 0] <= positionNdc[0] <= bounds[1, 0] and
- bounds[0, 1] <= positionNdc[1] <= bounds[1, 1])
+ return (
+ bounds[0, 0] <= positionNdc[0] <= bounds[1, 0]
+ and bounds[0, 1] <= positionNdc[1] <= bounds[1, 1]
+ )
def _pickFull(self, context):
"""Perform precise picking in this item at given widget position.
@@ -298,17 +297,21 @@ class DataItem3D(Item3D):
# Group transforms to do to data before rotation
# This is useful to handle rotation center relative to bbox
self._transformObjectToRotate = transform.TransformList(
- [self._matrix, self._scale])
+ [self._matrix, self._scale]
+ )
self._transformObjectToRotate.addListener(self._updateRotationCenter)
- self._rotationCenter = 0., 0., 0.
+ self._rotationCenter = 0.0, 0.0, 0.0
- self.__transforms = transform.TransformList([
- self._translate,
- self._rotateForwardTranslation,
- self._rotate,
- self._rotateBackwardTranslation,
- self._transformObjectToRotate])
+ self.__transforms = transform.TransformList(
+ [
+ self._translate,
+ self._rotateForwardTranslation,
+ self._rotate,
+ self._rotateBackwardTranslation,
+ self._transformObjectToRotate,
+ ]
+ )
self._getScenePrimitive().transforms = self.__transforms
@@ -330,7 +333,7 @@ class DataItem3D(Item3D):
"""
return self.__transforms
- def setScale(self, sx=1., sy=1., sz=1.):
+ def setScale(self, sx=1.0, sy=1.0, sz=1.0):
"""Set the scale of the item in the scene.
:param float sx: Scale factor along the X axis
@@ -349,7 +352,7 @@ class DataItem3D(Item3D):
"""
return self._scale.scale
- def setTranslation(self, x=0., y=0., z=0.):
+ def setTranslation(self, x=0.0, y=0.0, z=0.0):
"""Set the translation of the origin of the item in the scene.
:param float x: Offset of the data origin on the X axis
@@ -368,7 +371,7 @@ class DataItem3D(Item3D):
"""
return self._translate.translation
- _ROTATION_CENTER_TAGS = 'lower', 'center', 'upper'
+ _ROTATION_CENTER_TAGS = "lower", "center", "upper"
def _updateRotationCenter(self, *args, **kwargs):
"""Update rotation center relative to bounding box"""
@@ -377,28 +380,31 @@ class DataItem3D(Item3D):
# Patch position relative to bounding box
if position in self._ROTATION_CENTER_TAGS:
bounds = self._getScenePrimitive().bounds(
- transformed=False, dataBounds=True)
+ transformed=False, dataBounds=True
+ )
bounds = self._transformObjectToRotate.transformBounds(bounds)
if bounds is None:
- position = 0.
- elif position == 'lower':
+ position = 0.0
+ elif position == "lower":
position = bounds[0, index]
- elif position == 'center':
+ elif position == "center":
position = 0.5 * (bounds[0, index] + bounds[1, index])
- elif position == 'upper':
+ elif position == "upper":
position = bounds[1, index]
center.append(position)
- if not numpy.all(numpy.equal(
- center, self._rotateForwardTranslation.translation)):
+ if not numpy.all(
+ numpy.equal(center, self._rotateForwardTranslation.translation)
+ ):
self._rotateForwardTranslation.translation = center
- self._rotateBackwardTranslation.translation = \
- - self._rotateForwardTranslation.translation
+ self._rotateBackwardTranslation.translation = (
+ -self._rotateForwardTranslation.translation
+ )
self._updated(Item3DChangedType.TRANSFORM)
- def setRotationCenter(self, x=0., y=0., z=0.):
+ def setRotationCenter(self, x=0.0, y=0.0, z=0.0):
"""Set the center of rotation of the item.
Position of the rotation center is either a float
@@ -433,7 +439,7 @@ class DataItem3D(Item3D):
"""
return self._rotationCenter
- def setRotation(self, angle=0., axis=(0., 0., 1.)):
+ def setRotation(self, angle=0.0, axis=(0.0, 0.0, 1.0)):
"""Set the rotation of the item in the scene
:param float angle: The rotation angle in degrees.
@@ -442,8 +448,9 @@ class DataItem3D(Item3D):
axis = numpy.array(axis, dtype=numpy.float32)
assert axis.ndim == 1
assert axis.size == 3
- if (self._rotate.angle != angle or
- not numpy.all(numpy.equal(axis, self._rotate.axis))):
+ if self._rotate.angle != angle or not numpy.all(
+ numpy.equal(axis, self._rotate.axis)
+ ):
self._rotate.setAngleAxis(angle, axis)
self._updated(Item3DChangedType.TRANSFORM)
@@ -525,7 +532,7 @@ class BaseNodeItem(DataItem3D):
:rtype: tuple
"""
- raise NotImplementedError('getItems must be implemented in subclass')
+ raise NotImplementedError("getItems must be implemented in subclass")
def visit(self, included=True):
"""Generator visiting the group content.
@@ -538,7 +545,7 @@ class BaseNodeItem(DataItem3D):
yield self
for child in self.getItems():
yield child
- if hasattr(child, 'visit'):
+ if hasattr(child, "visit"):
for item in child.visit(included=False):
yield item
@@ -557,8 +564,7 @@ class BaseNodeItem(DataItem3D):
"""
viewport = self._getScenePrimitive().viewport
if viewport is None:
- raise RuntimeError(
- 'Cannot perform picking: Item not attached to a widget')
+ raise RuntimeError("Cannot perform picking: Item not attached to a widget")
context = PickContext(x, y, viewport, condition)
for result in self._pickItems(context):
@@ -641,12 +647,10 @@ class _BaseGroupItem(BaseNodeItem):
item.setParent(self)
if index is None:
- self._getGroupPrimitive().children.append(
- item._getScenePrimitive())
+ self._getGroupPrimitive().children.append(item._getScenePrimitive())
self._items.append(item)
else:
- self._getGroupPrimitive().children.insert(
- index, item._getScenePrimitive())
+ self._getGroupPrimitive().children.insert(index, item._getScenePrimitive())
self._items.insert(index, item)
self.sigItemAdded.emit(item)
@@ -694,8 +698,9 @@ class GroupWithAxesItem(_BaseGroupItem):
:param parent: The View widget this item belongs to.
"""
- super(GroupWithAxesItem, self).__init__(parent=parent,
- group=axes.LabelledAxes())
+ super(GroupWithAxesItem, self).__init__(
+ parent=parent, group=axes.LabelledAxes()
+ )
# Axes labels
@@ -750,9 +755,9 @@ class GroupWithAxesItem(_BaseGroupItem):
:return: object describing the labels
"""
labelledAxes = self._getScenePrimitive()
- return self._Labels((labelledAxes.xlabel,
- labelledAxes.ylabel,
- labelledAxes.zlabel))
+ return self._Labels(
+ (labelledAxes.xlabel, labelledAxes.ylabel, labelledAxes.zlabel)
+ )
class RootGroupWithAxesItem(GroupWithAxesItem):
diff --git a/src/silx/gui/plot3d/items/image.py b/src/silx/gui/plot3d/items/image.py
index 5a50459..d4d31c6 100644
--- a/src/silx/gui/plot3d/items/image.py
+++ b/src/silx/gui/plot3d/items/image.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides 2D data and RGB(A) image item class.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/11/2017"
@@ -69,11 +66,12 @@ class _Image(DataItem3D, InterpolationMixIn):
points = utils.segmentPlaneIntersect(
rayObject[0, :3],
rayObject[1, :3],
- planeNorm=numpy.array((0., 0., 1.), dtype=numpy.float64),
- planePt=numpy.array((0., 0., 0.), dtype=numpy.float64))
+ planeNorm=numpy.array((0.0, 0.0, 1.0), dtype=numpy.float64),
+ planePt=numpy.array((0.0, 0.0, 0.0), dtype=numpy.float64),
+ )
if len(points) == 1: # Single intersection
- if points[0][0] < 0. or points[0][1] < 0.:
+ if points[0][0] < 0.0 or points[0][1] < 0.0:
return None # Outside image
row, column = int(points[0][1]), int(points[0][0])
data = self.getData(copy=False)
@@ -81,8 +79,9 @@ class _Image(DataItem3D, InterpolationMixIn):
if row < height and column < width:
return PickingResult(
self,
- positions=[(points[0][0], points[0][1], 0.)],
- indices=([row], [column]))
+ positions=[(points[0][0], points[0][1], 0.0)],
+ indices=([row], [column]),
+ )
else:
return None # Outside image
else: # Either no intersection or segment and image are coplanar
@@ -186,7 +185,7 @@ class _HeightMap(DataItem3D):
DataItem3D.__init__(self, parent=parent)
self.__data = numpy.zeros((0, 0), dtype=numpy.float32)
- def _pickFull(self, context, threshold=0., sort='depth'):
+ def _pickFull(self, context, threshold=0.0, sort="depth"):
"""Perform picking in this item at given widget position.
:param PickContext context: Current picking context
@@ -200,9 +199,9 @@ class _HeightMap(DataItem3D):
:return: Object holding the results or None
:rtype: Union[None,PickingResult]
"""
- assert sort in ('index', 'depth')
+ assert sort in ("index", "depth")
- rayNdc = context.getPickingSegment(frame='ndc')
+ rayNdc = context.getPickingSegment(frame="ndc")
if rayNdc is None: # No picking outside viewport
return None
@@ -215,40 +214,46 @@ class _HeightMap(DataItem3D):
height, width = heightData.shape
z = numpy.ravel(heightData)
y, x = numpy.mgrid[0:height, 0:width]
- dataPoints = numpy.transpose((numpy.ravel(x),
- numpy.ravel(y),
- z,
- numpy.ones_like(z)))
+ dataPoints = numpy.transpose(
+ (numpy.ravel(x), numpy.ravel(y), z, numpy.ones_like(z))
+ )
primitive = self._getScenePrimitive()
pointsNdc = primitive.objectToNDCTransform.transformPoints(
- dataPoints, perspectiveDivide=True)
+ dataPoints, perspectiveDivide=True
+ )
# Perform picking
distancesNdc = numpy.abs(pointsNdc[:, :2] - rayNdc[0, :2])
# TODO issue with symbol size: using pixel instead of points
- threshold += 1. # symbol size
- thresholdNdc = 2. * threshold / numpy.array(primitive.viewport.size)
- picked = numpy.where(numpy.logical_and(
+ threshold += 1.0 # symbol size
+ thresholdNdc = 2.0 * threshold / numpy.array(primitive.viewport.size)
+ picked = numpy.where(
+ numpy.logical_and(
numpy.all(distancesNdc < thresholdNdc, axis=1),
- numpy.logical_and(rayNdc[0, 2] <= pointsNdc[:, 2],
- pointsNdc[:, 2] <= rayNdc[1, 2])))[0]
+ numpy.logical_and(
+ rayNdc[0, 2] <= pointsNdc[:, 2], pointsNdc[:, 2] <= rayNdc[1, 2]
+ ),
+ )
+ )[0]
- if sort == 'depth':
+ if sort == "depth":
# Sort picked points from front to back
picked = picked[numpy.argsort(pointsNdc[picked, 2])]
if picked.size > 0:
# Convert indices from 1D to 2D
- return PickingResult(self,
- positions=dataPoints[picked, :3],
- indices=(picked // width, picked % width),
- fetchdata=self.getData)
+ return PickingResult(
+ self,
+ positions=dataPoints[picked, :3],
+ indices=(picked // width, picked % width),
+ fetchdata=self.getData,
+ )
else:
return None
- def setData(self, data, copy: bool=True):
+ def setData(self, data, copy: bool = True):
"""Set the height field data.
:param data:
@@ -261,7 +266,7 @@ class _HeightMap(DataItem3D):
self.__data = data
self._updated(ItemChangedType.DATA)
- def getData(self, copy: bool=True) -> numpy.ndarray:
+ def getData(self, copy: bool = True) -> numpy.ndarray:
"""Get the height field 2D data.
:param bool copy:
@@ -309,23 +314,22 @@ class HeightMapData(_HeightMap, ColormapMixIn):
if data.shape != heightData.shape: # data and height size miss-match
# Colormapped data is interpolated (nearest-neighbour) to match the height field
- data = data[numpy.floor(y * data.shape[0] / height).astype(numpy.int32),
- numpy.floor(x * data.shape[1] / height).astype(numpy.int32)]
+ data = data[
+ numpy.floor(y * data.shape[0] / height).astype(numpy.int32),
+ numpy.floor(x * data.shape[1] / height).astype(numpy.int32),
+ ]
x = numpy.ravel(x)
y = numpy.ravel(y)
primitive = primitives.Points(
- x=x,
- y=y,
- z=numpy.ravel(heightData),
- value=numpy.ravel(data),
- size=1)
- primitive.marker = 's'
+ x=x, y=y, z=numpy.ravel(heightData), value=numpy.ravel(data), size=1
+ )
+ primitive.marker = "s"
ColormapMixIn._setSceneColormap(self, primitive.colormap)
self._getScenePrimitive().children = [primitive]
- def setColormappedData(self, data, copy: bool=True):
+ def setColormappedData(self, data, copy: bool = True):
"""Set the 2D data used to compute colors.
:param data: 2D array of data
@@ -338,7 +342,7 @@ class HeightMapData(_HeightMap, ColormapMixIn):
self.__data = data
self._updated(ItemChangedType.DATA)
- def getColormappedData(self, copy: bool=True) -> numpy.ndarray:
+ def getColormappedData(self, copy: bool = True) -> numpy.ndarray:
"""Returns the 2D data used to compute colors.
:param copy:
@@ -383,8 +387,10 @@ class HeightMapRGBA(_HeightMap):
if rgba.shape[:2] != heightData.shape: # image and height size miss-match
# RGBA data is interpolated (nearest-neighbour) to match the height field
- rgba = rgba[numpy.floor(y * rgba.shape[0] / height).astype(numpy.int32),
- numpy.floor(x * rgba.shape[1] / height).astype(numpy.int32)]
+ rgba = rgba[
+ numpy.floor(y * rgba.shape[0] / height).astype(numpy.int32),
+ numpy.floor(x * rgba.shape[1] / height).astype(numpy.int32),
+ ]
x = numpy.ravel(x)
y = numpy.ravel(y)
@@ -394,11 +400,12 @@ class HeightMapRGBA(_HeightMap):
y=y,
z=numpy.ravel(heightData),
color=rgba.reshape(-1, rgba.shape[-1]),
- size=1)
- primitive.marker = 's'
+ size=1,
+ )
+ primitive.marker = "s"
self._getScenePrimitive().children = [primitive]
- def setColorData(self, data, copy: bool=True):
+ def setColorData(self, data, copy: bool = True):
"""Set the RGB(A) image to use.
Supported array format: float32 in [0, 1], uint8.
@@ -416,7 +423,7 @@ class HeightMapRGBA(_HeightMap):
self.__rgba = data
self._updated(ItemChangedType.DATA)
- def getColorData(self, copy: bool=True) -> numpy.ndarray:
+ def getColorData(self, copy: bool = True) -> numpy.ndarray:
"""Get the RGB(A) image data.
:param copy: True (default) to get a copy,
diff --git a/src/silx/gui/plot3d/items/mesh.py b/src/silx/gui/plot3d/items/mesh.py
index 4e19939..89056c3 100644
--- a/src/silx/gui/plot3d/items/mesh.py
+++ b/src/silx/gui/plot3d/items/mesh.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides regular mesh item class.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "17/07/2018"
@@ -85,7 +82,7 @@ class _MeshBase(DataItem3D):
if self._getMesh() is None:
return numpy.empty((0, 3), dtype=numpy.float32)
else:
- return self._getMesh().getAttribute('position', copy=copy)
+ return self._getMesh().getAttribute("position", copy=copy)
def getNormalData(self, copy=True):
"""Get the mesh vertex normals.
@@ -99,7 +96,7 @@ class _MeshBase(DataItem3D):
if self._getMesh() is None:
return None
else:
- return self._getMesh().getAttribute('normal', copy=copy)
+ return self._getMesh().getAttribute("normal", copy=copy)
def getIndices(self, copy=True):
"""Get the vertex indices.
@@ -146,21 +143,23 @@ class _MeshBase(DataItem3D):
positions = utils.unindexArrays(mode, vertexIndices, positions)[0]
triangles = positions.reshape(-1, 3, 3)
else:
- if mode == 'triangles':
+ if mode == "triangles":
triangles = positions.reshape(-1, 3, 3)
- elif mode == 'triangle_strip':
+ elif mode == "triangle_strip":
# Expand strip
- triangles = numpy.empty((len(positions) - 2, 3, 3),
- dtype=positions.dtype)
+ triangles = numpy.empty(
+ (len(positions) - 2, 3, 3), dtype=positions.dtype
+ )
triangles[:, 0] = positions[:-2]
triangles[:, 1] = positions[1:-1]
triangles[:, 2] = positions[2:]
- elif mode == 'fan':
+ elif mode == "fan":
# Expand fan
- triangles = numpy.empty((len(positions) - 2, 3, 3),
- dtype=positions.dtype)
+ triangles = numpy.empty(
+ (len(positions) - 2, 3, 3), dtype=positions.dtype
+ )
triangles[:, 0] = positions[0]
triangles[:, 1] = positions[1:-1]
triangles[:, 2] = positions[2:]
@@ -170,7 +169,8 @@ class _MeshBase(DataItem3D):
return None
trianglesIndices, t, barycentric = glu.segmentTrianglesIntersection(
- rayObject, triangles)
+ rayObject, triangles
+ )
if len(trianglesIndices) == 0:
return None
@@ -180,13 +180,13 @@ class _MeshBase(DataItem3D):
# Get vertex index from triangle index and closest point in triangle
closest = numpy.argmax(barycentric, axis=1)
- if mode == 'triangles':
+ if mode == "triangles":
indices = trianglesIndices * 3 + closest
- elif mode == 'triangle_strip':
+ elif mode == "triangle_strip":
indices = trianglesIndices + closest
- elif mode == 'fan':
+ elif mode == "fan":
indices = trianglesIndices + closest # For corners 1 and 2
indices[closest == 0] = 0 # For first corner (common)
@@ -194,10 +194,9 @@ class _MeshBase(DataItem3D):
# Convert from indices in expanded triangles to input vertices
indices = vertexIndices[indices]
- return PickingResult(self,
- positions=points,
- indices=indices,
- fetchdata=self.getPositionData)
+ return PickingResult(
+ self, positions=points, indices=indices, fetchdata=self.getPositionData
+ )
class Mesh(_MeshBase):
@@ -209,13 +208,9 @@ class Mesh(_MeshBase):
def __init__(self, parent=None):
_MeshBase.__init__(self, parent=parent)
- def setData(self,
- position,
- color,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
+ def setData(
+ self, position, color, normal=None, mode="triangles", indices=None, copy=True
+ ):
"""Set mesh geometry data.
Supported drawing modes are: 'triangles', 'triangle_strip', 'fan'
@@ -230,12 +225,13 @@ class Mesh(_MeshBase):
:param bool copy: True (default) to copy the data,
False to use as is (do not modify!).
"""
- assert mode in ('triangles', 'triangle_strip', 'fan')
+ assert mode in ("triangles", "triangle_strip", "fan")
if position is None or len(position) == 0:
mesh = None
else:
mesh = primitives.Mesh3D(
- position, color, normal, mode=mode, indices=indices, copy=copy)
+ position, color, normal, mode=mode, indices=indices, copy=copy
+ )
self._setMesh(mesh)
def getData(self, copy=True):
@@ -247,10 +243,12 @@ class Mesh(_MeshBase):
:return: The positions, colors, normals and mode
:rtype: tuple of numpy.ndarray
"""
- return (self.getPositionData(copy=copy),
- self.getColorData(copy=copy),
- self.getNormalData(copy=copy),
- self.getDrawMode())
+ return (
+ self.getPositionData(copy=copy),
+ self.getColorData(copy=copy),
+ self.getNormalData(copy=copy),
+ self.getDrawMode(),
+ )
def getColorData(self, copy=True):
"""Get the mesh vertex colors.
@@ -264,7 +262,7 @@ class Mesh(_MeshBase):
if self._getMesh() is None:
return numpy.empty((0, 4), dtype=numpy.float32)
else:
- return self._getMesh().getAttribute('color', copy=copy)
+ return self._getMesh().getAttribute("color", copy=copy)
class ColormapMesh(_MeshBase, ColormapMixIn):
@@ -277,13 +275,9 @@ class ColormapMesh(_MeshBase, ColormapMixIn):
_MeshBase.__init__(self, parent=parent)
ColormapMixIn.__init__(self, function.Colormap())
- def setData(self,
- position,
- value,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
+ def setData(
+ self, position, value, normal=None, mode="triangles", indices=None, copy=True
+ ):
"""Set mesh geometry data.
Supported drawing modes are: 'triangles', 'triangle_strip', 'fan'
@@ -298,18 +292,21 @@ class ColormapMesh(_MeshBase, ColormapMixIn):
:param bool copy: True (default) to copy the data,
False to use as is (do not modify!).
"""
- assert mode in ('triangles', 'triangle_strip', 'fan')
+ assert mode in ("triangles", "triangle_strip", "fan")
if position is None or len(position) == 0:
mesh = None
else:
mesh = primitives.ColormapMesh3D(
position=position,
- value=numpy.array(value, copy=False).reshape(-1, 1), # Make it a 2D array
+ value=numpy.array(value, copy=False).reshape(
+ -1, 1
+ ), # Make it a 2D array
colormap=self._getSceneColormap(),
normal=normal,
mode=mode,
indices=indices,
- copy=copy)
+ copy=copy,
+ )
self._setMesh(mesh)
self._setColormappedData(self.getValueData(copy=False), copy=False)
@@ -323,10 +320,12 @@ class ColormapMesh(_MeshBase, ColormapMixIn):
:return: The positions, values, normals and mode
:rtype: tuple of numpy.ndarray
"""
- return (self.getPositionData(copy=copy),
- self.getValueData(copy=copy),
- self.getNormalData(copy=copy),
- self.getDrawMode())
+ return (
+ self.getPositionData(copy=copy),
+ self.getValueData(copy=copy),
+ self.getNormalData(copy=copy),
+ self.getDrawMode(),
+ )
def getValueData(self, copy=True):
"""Get the mesh vertex values.
@@ -340,7 +339,7 @@ class ColormapMesh(_MeshBase, ColormapMixIn):
if self._getMesh() is None:
return numpy.empty((0,), dtype=numpy.float32)
else:
- return self._getMesh().getAttribute('value', copy=copy)
+ return self._getMesh().getAttribute("value", copy=copy)
class _CylindricalVolume(DataItem3D):
@@ -365,8 +364,7 @@ class _CylindricalVolume(DataItem3D):
"""
raise NotImplementedError("Must be implemented in subclass")
- def _setData(self, position, radius, height, angles, color, flatFaces,
- rotation):
+ def _setData(self, position, radius, height, angles, color, flatFaces, rotation):
"""Set volume geometry data.
:param numpy.ndarray position:
@@ -387,10 +385,8 @@ class _CylindricalVolume(DataItem3D):
else:
self._nbFaces = len(angles) - 1
- volume = numpy.empty(shape=(len(angles) - 1, 12, 3),
- dtype=numpy.float32)
- normal = numpy.empty(shape=(len(angles) - 1, 12, 3),
- dtype=numpy.float32)
+ volume = numpy.empty(shape=(len(angles) - 1, 12, 3), dtype=numpy.float32)
+ normal = numpy.empty(shape=(len(angles) - 1, 12, 3), dtype=numpy.float32)
for i in range(0, len(angles) - 1):
# c6
@@ -407,71 +403,103 @@ class _CylindricalVolume(DataItem3D):
# \ /
# \/
# c1
- c1 = numpy.array([0, 0, -height/2])
+ c1 = numpy.array([0, 0, -height / 2])
c1 = rotation.transformPoint(c1)
- c2 = numpy.array([radius * numpy.cos(angles[i]),
- radius * numpy.sin(angles[i]),
- -height/2])
+ c2 = numpy.array(
+ [
+ radius * numpy.cos(angles[i]),
+ radius * numpy.sin(angles[i]),
+ -height / 2,
+ ]
+ )
c2 = rotation.transformPoint(c2)
- c3 = numpy.array([radius * numpy.cos(angles[i+1]),
- radius * numpy.sin(angles[i+1]),
- -height/2])
+ c3 = numpy.array(
+ [
+ radius * numpy.cos(angles[i + 1]),
+ radius * numpy.sin(angles[i + 1]),
+ -height / 2,
+ ]
+ )
c3 = rotation.transformPoint(c3)
- c4 = numpy.array([radius * numpy.cos(angles[i]),
- radius * numpy.sin(angles[i]),
- height/2])
+ c4 = numpy.array(
+ [
+ radius * numpy.cos(angles[i]),
+ radius * numpy.sin(angles[i]),
+ height / 2,
+ ]
+ )
c4 = rotation.transformPoint(c4)
- c5 = numpy.array([radius * numpy.cos(angles[i+1]),
- radius * numpy.sin(angles[i+1]),
- height/2])
+ c5 = numpy.array(
+ [
+ radius * numpy.cos(angles[i + 1]),
+ radius * numpy.sin(angles[i + 1]),
+ height / 2,
+ ]
+ )
c5 = rotation.transformPoint(c5)
- c6 = numpy.array([0, 0, height/2])
+ c6 = numpy.array([0, 0, height / 2])
c6 = rotation.transformPoint(c6)
- volume[i] = numpy.array([c1, c3, c2,
- c2, c3, c4,
- c3, c5, c4,
- c4, c5, c6])
+ volume[i] = numpy.array(
+ [c1, c3, c2, c2, c3, c4, c3, c5, c4, c4, c5, c6]
+ )
if flatFaces:
- normal[i] = numpy.array([numpy.cross(c3-c1, c2-c1), # c1
- numpy.cross(c2-c3, c1-c3), # c3
- numpy.cross(c1-c2, c3-c2), # c2
- numpy.cross(c3-c2, c4-c2), # c2
- numpy.cross(c4-c3, c2-c3), # c3
- numpy.cross(c2-c4, c3-c4), # c4
- numpy.cross(c5-c3, c4-c3), # c3
- numpy.cross(c4-c5, c3-c5), # c5
- numpy.cross(c3-c4, c5-c4), # c4
- numpy.cross(c5-c4, c6-c4), # c4
- numpy.cross(c6-c5, c5-c5), # c5
- numpy.cross(c4-c6, c5-c6)]) # c6
+ normal[i] = numpy.array(
+ [
+ numpy.cross(c3 - c1, c2 - c1), # c1
+ numpy.cross(c2 - c3, c1 - c3), # c3
+ numpy.cross(c1 - c2, c3 - c2), # c2
+ numpy.cross(c3 - c2, c4 - c2), # c2
+ numpy.cross(c4 - c3, c2 - c3), # c3
+ numpy.cross(c2 - c4, c3 - c4), # c4
+ numpy.cross(c5 - c3, c4 - c3), # c3
+ numpy.cross(c4 - c5, c3 - c5), # c5
+ numpy.cross(c3 - c4, c5 - c4), # c4
+ numpy.cross(c5 - c4, c6 - c4), # c4
+ numpy.cross(c6 - c5, c5 - c5), # c5
+ numpy.cross(c4 - c6, c5 - c6),
+ ]
+ ) # c6
else:
- normal[i] = numpy.array([numpy.cross(c3-c1, c2-c1),
- numpy.cross(c2-c3, c1-c3),
- numpy.cross(c1-c2, c3-c2),
- c2-c1, c3-c1, c4-c6, # c2 c2 c4
- c3-c1, c5-c6, c4-c6, # c3 c5 c4
- numpy.cross(c5-c4, c6-c4),
- numpy.cross(c6-c5, c5-c5),
- numpy.cross(c4-c6, c5-c6)])
+ normal[i] = numpy.array(
+ [
+ numpy.cross(c3 - c1, c2 - c1),
+ numpy.cross(c2 - c3, c1 - c3),
+ numpy.cross(c1 - c2, c3 - c2),
+ c2 - c1,
+ c3 - c1,
+ c4 - c6, # c2 c2 c4
+ c3 - c1,
+ c5 - c6,
+ c4 - c6, # c3 c5 c4
+ numpy.cross(c5 - c4, c6 - c4),
+ numpy.cross(c6 - c5, c5 - c5),
+ numpy.cross(c4 - c6, c5 - c6),
+ ]
+ )
# Multiplication according to the number of positions
- vertices = numpy.tile(volume.reshape(-1, 3), (len(position), 1))\
- .reshape((-1, 3))
- normals = numpy.tile(normal.reshape(-1, 3), (len(position), 1))\
- .reshape((-1, 3))
+ vertices = numpy.tile(volume.reshape(-1, 3), (len(position), 1)).reshape(
+ (-1, 3)
+ )
+ normals = numpy.tile(normal.reshape(-1, 3), (len(position), 1)).reshape(
+ (-1, 3)
+ )
# Translations
- numpy.add(vertices, numpy.tile(position, (1, (len(angles)-1) * 12))
- .reshape((-1, 3)), out=vertices)
+ numpy.add(
+ vertices,
+ numpy.tile(position, (1, (len(angles) - 1) * 12)).reshape((-1, 3)),
+ out=vertices,
+ )
# Colors
if numpy.ndim(color) == 2:
- color = numpy.tile(color, (1, 12 * (len(angles) - 1)))\
- .reshape(-1, 3)
+ color = numpy.tile(color, (1, 12 * (len(angles) - 1))).reshape(-1, 3)
self._mesh = primitives.Mesh3D(
- vertices, color, normals, mode='triangles', copy=False)
+ vertices, color, normals, mode="triangles", copy=False
+ )
self._getScenePrimitive().children.append(self._mesh)
self._updated(ItemChangedType.DATA)
@@ -491,11 +519,10 @@ class _CylindricalVolume(DataItem3D):
return None
rayObject = rayObject[:, :3]
- positions = self._mesh.getAttribute('position', copy=False)
+ positions = self._mesh.getAttribute("position", copy=False)
triangles = positions.reshape(-1, 3, 3) # 'triangle' draw mode
- trianglesIndices, t = glu.segmentTrianglesIntersection(
- rayObject, triangles)[:2]
+ trianglesIndices, t = glu.segmentTrianglesIntersection(rayObject, triangles)[:2]
if len(trianglesIndices) == 0:
return None
@@ -514,10 +541,9 @@ class _CylindricalVolume(DataItem3D):
points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
- return PickingResult(self,
- positions=points,
- indices=indices,
- fetchdata=self.getPosition)
+ return PickingResult(
+ self, positions=points, indices=indices, fetchdata=self.getPosition
+ )
class Box(_CylindricalVolume):
@@ -536,8 +562,13 @@ class Box(_CylindricalVolume):
self.rotation = None
self.setData()
- def setData(self, size=(1, 1, 1), color=(1, 1, 1),
- position=(0, 0, 0), rotation=(0, (0, 0, 0))):
+ def setData(
+ self,
+ size=(1, 1, 1),
+ color=(1, 1, 1),
+ position=(0, 0, 0),
+ rotation=(0, (0, 0, 0)),
+ ):
"""
Set Box geometry data.
@@ -553,28 +584,28 @@ class Box(_CylindricalVolume):
self.position = numpy.atleast_2d(numpy.array(position, copy=True))
self.size = numpy.array(size, copy=True)
self.color = numpy.array(color, copy=True)
- self.rotation = Rotate(rotation[0],
- rotation[1][0], rotation[1][1], rotation[1][2])
+ self.rotation = Rotate(
+ rotation[0], rotation[1][0], rotation[1][1], rotation[1][2]
+ )
- assert (numpy.ndim(self.color) == 1 or
- len(self.color) == len(self.position))
+ assert numpy.ndim(self.color) == 1 or len(self.color) == len(self.position)
- diagonal = numpy.sqrt(self.size[0]**2 + self.size[1]**2)
+ diagonal = numpy.sqrt(self.size[0] ** 2 + self.size[1] ** 2)
alpha = 2 * numpy.arcsin(self.size[1] / diagonal)
beta = 2 * numpy.arcsin(self.size[0] / diagonal)
- angles = numpy.array([0,
- alpha,
- alpha + beta,
- alpha + beta + alpha,
- 2 * numpy.pi])
+ angles = numpy.array(
+ [0, alpha, alpha + beta, alpha + beta + alpha, 2 * numpy.pi]
+ )
numpy.subtract(angles, 0.5 * alpha, out=angles)
- self._setData(self.position,
- numpy.sqrt(self.size[0]**2 + self.size[1]**2)/2,
- self.size[2],
- angles,
- self.color,
- True,
- self.rotation)
+ self._setData(
+ self.position,
+ numpy.sqrt(self.size[0] ** 2 + self.size[1] ** 2) / 2,
+ self.size[2],
+ angles,
+ self.color,
+ True,
+ self.rotation,
+ )
def getPosition(self, copy=True):
"""Get box(es) position(s).
@@ -625,8 +656,15 @@ class Cylinder(_CylindricalVolume):
self.rotation = None
self.setData()
- def setData(self, radius=1, height=1, color=(1, 1, 1), nbFaces=20,
- position=(0, 0, 0), rotation=(0, (0, 0, 0))):
+ def setData(
+ self,
+ radius=1,
+ height=1,
+ color=(1, 1, 1),
+ nbFaces=20,
+ position=(0, 0, 0),
+ rotation=(0, (0, 0, 0)),
+ ):
"""
Set the cylinder geometry data
@@ -647,20 +685,22 @@ class Cylinder(_CylindricalVolume):
self.height = float(height)
self.color = numpy.array(color, copy=True)
self.nbFaces = int(nbFaces)
- self.rotation = Rotate(rotation[0],
- rotation[1][0], rotation[1][1], rotation[1][2])
-
- assert (numpy.ndim(self.color) == 1 or
- len(self.color) == len(self.position))
-
- angles = numpy.linspace(0, 2*numpy.pi, self.nbFaces + 1)
- self._setData(self.position,
- self.radius,
- self.height,
- angles,
- self.color,
- False,
- self.rotation)
+ self.rotation = Rotate(
+ rotation[0], rotation[1][0], rotation[1][1], rotation[1][2]
+ )
+
+ assert numpy.ndim(self.color) == 1 or len(self.color) == len(self.position)
+
+ angles = numpy.linspace(0, 2 * numpy.pi, self.nbFaces + 1)
+ self._setData(
+ self.position,
+ self.radius,
+ self.height,
+ angles,
+ self.color,
+ False,
+ self.rotation,
+ )
def getPosition(self, copy=True):
"""Get cylinder(s) position(s).
@@ -719,8 +759,14 @@ class Hexagon(_CylindricalVolume):
self.rotation = None
self.setData()
- def setData(self, radius=1, height=1, color=(1, 1, 1),
- position=(0, 0, 0), rotation=(0, (0, 0, 0))):
+ def setData(
+ self,
+ radius=1,
+ height=1,
+ color=(1, 1, 1),
+ position=(0, 0, 0),
+ rotation=(0, (0, 0, 0)),
+ ):
"""
Set the uniform hexagonal prism geometry data
@@ -738,20 +784,22 @@ class Hexagon(_CylindricalVolume):
self.radius = float(radius)
self.height = float(height)
self.color = numpy.array(color, copy=True)
- self.rotation = Rotate(rotation[0], rotation[1][0], rotation[1][1],
- rotation[1][2])
-
- assert (numpy.ndim(self.color) == 1 or
- len(self.color) == len(self.position))
-
- angles = numpy.linspace(0, 2*numpy.pi, 7)
- self._setData(self.position,
- self.radius,
- self.height,
- angles,
- self.color,
- True,
- self.rotation)
+ self.rotation = Rotate(
+ rotation[0], rotation[1][0], rotation[1][1], rotation[1][2]
+ )
+
+ assert numpy.ndim(self.color) == 1 or len(self.color) == len(self.position)
+
+ angles = numpy.linspace(0, 2 * numpy.pi, 7)
+ self._setData(
+ self.position,
+ self.radius,
+ self.height,
+ angles,
+ self.color,
+ True,
+ self.rotation,
+ )
def getPosition(self, copy=True):
"""Get hexagonal prim(s) position(s).
@@ -761,7 +809,7 @@ class Hexagon(_CylindricalVolume):
False to get internal representation (do not modify!).
:return: Position(s) of hexagonal prism(s) as a (N, 3) array.
:rtype: numpy.ndarray
- """
+ """
return numpy.array(self.position, copy=copy)
def getRadius(self):
diff --git a/src/silx/gui/plot3d/items/mixins.py b/src/silx/gui/plot3d/items/mixins.py
index f512365..c69c3ac 100644
--- a/src/silx/gui/plot3d/items/mixins.py
+++ b/src/silx/gui/plot3d/items/mixins.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,11 +29,8 @@ __license__ = "MIT"
__date__ = "24/04/2018"
-import collections
import numpy
-from silx.math.combo import min_max
-
from ...plot.items.core import ItemMixInBase
from ...plot.items.core import ColormapMixIn as _ColormapMixIn
from ...plot.items.core import SymbolMixIn as _SymbolMixIn
@@ -54,24 +50,21 @@ class InterpolationMixIn(ItemMixInBase):
This object MUST have an interpolation property that is updated.
"""
- NEAREST_INTERPOLATION = 'nearest'
+ NEAREST_INTERPOLATION = "nearest"
"""Nearest interpolation mode (see :meth:`setInterpolation`)"""
- LINEAR_INTERPOLATION = 'linear'
+ LINEAR_INTERPOLATION = "linear"
"""Linear interpolation mode (see :meth:`setInterpolation`)"""
INTERPOLATION_MODES = NEAREST_INTERPOLATION, LINEAR_INTERPOLATION
"""Supported interpolation modes for :meth:`setInterpolation`"""
- def __init__(self, mode=NEAREST_INTERPOLATION, primitive=None):
- self.__primitive = primitive
+ def __init__(self):
+ self.__primitive = None
+ self.__interpolationMode = self.NEAREST_INTERPOLATION
self._syncPrimitiveInterpolation()
- self.__interpolationMode = None
- self.setInterpolation(mode)
-
def _setPrimitive(self, primitive):
-
"""Set the scene object for which to sync interpolation"""
self.__primitive = primitive
self._syncPrimitiveInterpolation()
@@ -152,24 +145,28 @@ class ComplexMixIn(_ComplexMixIn):
_ComplexMixIn.ComplexMode.IMAGINARY,
_ComplexMixIn.ComplexMode.ABSOLUTE,
_ComplexMixIn.ComplexMode.PHASE,
- _ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
+ _ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE,
+ )
"""Overrides supported ComplexMode"""
class SymbolMixIn(_SymbolMixIn):
"""Mix-in class for symbol and symbolSize properties for Item3D"""
- _SUPPORTED_SYMBOLS = collections.OrderedDict((
- ('o', 'Circle'),
- ('d', 'Diamond'),
- ('s', 'Square'),
- ('+', 'Plus'),
- ('x', 'Cross'),
- ('*', 'Star'),
- ('|', 'Vertical Line'),
- ('_', 'Horizontal Line'),
- ('.', 'Point'),
- (',', 'Pixel')))
+ _SUPPORTED_SYMBOLS = dict(
+ (
+ ("o", "Circle"),
+ ("d", "Diamond"),
+ ("s", "Square"),
+ ("+", "Plus"),
+ ("x", "Cross"),
+ ("*", "Star"),
+ ("|", "Vertical Line"),
+ ("_", "Horizontal Line"),
+ (".", "Point"),
+ (",", "Pixel"),
+ )
+ )
def _getSceneSymbol(self):
"""Returns a symbol name and size suitable for scene primitives.
@@ -178,11 +175,11 @@ class SymbolMixIn(_SymbolMixIn):
"""
symbol = self.getSymbol()
size = self.getSymbolSize()
- if symbol == ',': # pixel
- return 's', 1.
- elif symbol == '.': # point
+ if symbol == ",": # pixel
+ return "s", 1.0
+ elif symbol == ".": # point
# Size as in plot OpenGL backend, mimic matplotlib
- return 'o', numpy.ceil(0.5 * size) + 1.
+ return "o", numpy.ceil(0.5 * size) + 1.0
else:
return symbol, size
@@ -190,18 +187,24 @@ class SymbolMixIn(_SymbolMixIn):
class PlaneMixIn(ItemMixInBase):
"""Mix-in class for plane items (based on PlaneInGroup primitive)"""
- def __init__(self, plane):
+ def __init__(self):
+ self.__plane = None
+ self._setPlane(primitives.PlaneInGroup())
+
+ def _setPlane(self, plane: primitives.PlaneInGroup):
+ """Set plane primitive"""
+ if self.__plane is not None:
+ self.__plane.removeListener(self._planeChanged)
+ self.__plane.plane.removeListener(self._planePositionChanged)
+
assert isinstance(plane, primitives.PlaneInGroup)
self.__plane = plane
- self.__plane.alpha = 1.
+ self.__plane.alpha = 1.0
self.__plane.addListener(self._planeChanged)
self.__plane.plane.addListener(self._planePositionChanged)
- def _getPlane(self):
- """Returns plane primitive
-
- :rtype: primitives.PlaneInGroup
- """
+ def _getPlane(self) -> primitives.PlaneInGroup:
+ """Returns plane primitive"""
return self.__plane
def _planeChanged(self, source, *args, **kwargs):
@@ -212,7 +215,9 @@ class PlaneMixIn(ItemMixInBase):
def _planePositionChanged(self, source, *args, **kwargs):
"""Handle update of cut plane position and normal"""
- if self.__plane.visible: # TODO send even if hidden? or send also when showing if moved while hidden
+ if (
+ self.__plane.visible
+ ): # TODO send even if hidden? or send also when showing if moved while hidden
self._updated(ItemChangedType.POSITION)
# Plane position
@@ -284,5 +289,5 @@ class PlaneMixIn(ItemMixInBase):
:param color: RGBA color as 4 floats in [0, 1]
"""
self.__plane.color = rgba(color)
- if hasattr(super(PlaneMixIn, self), '_setForegroundColor'):
+ if hasattr(super(PlaneMixIn, self), "_setForegroundColor"):
super(PlaneMixIn, self)._setForegroundColor(color)
diff --git a/src/silx/gui/plot3d/items/scatter.py b/src/silx/gui/plot3d/items/scatter.py
index 24abaa5..b8f2f39 100644
--- a/src/silx/gui/plot3d/items/scatter.py
+++ b/src/silx/gui/plot3d/items/scatter.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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,22 +24,17 @@
"""This module provides 2D and 3D scatter data item class.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/11/2017"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import logging
+import sys
import numpy
+from matplotlib.tri import Triangulation
-from ....utils.deprecation import deprecated
from ... import _glutils as glu
-from ...plot._utils.delaunay import delaunay
from ..scene import function, primitives, utils
from ...plot.items import ScatterVisualizationMixIn
@@ -68,7 +62,8 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
noData = numpy.zeros((0, 1), dtype=numpy.float32)
symbol, size = self._getSceneSymbol()
self._scatter = primitives.Points(
- x=noData, y=noData, z=noData, value=noData, size=size)
+ x=noData, y=noData, z=noData, value=noData, size=size
+ )
self._scatter.marker = symbol
self._getScenePrimitive().children.append(self._scatter)
@@ -80,7 +75,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE):
symbol, size = self._getSceneSymbol()
self._scatter.marker = symbol
- self._scatter.setAttribute('size', size, copy=True)
+ self._scatter.setAttribute("size", size, copy=True)
super(Scatter3D, self)._updated(event)
@@ -95,10 +90,10 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
True (default) to copy the data,
False to use provided data (do not modify!)
"""
- self._scatter.setAttribute('x', x, copy=copy)
- self._scatter.setAttribute('y', y, copy=copy)
- self._scatter.setAttribute('z', z, copy=copy)
- self._scatter.setAttribute('value', value, copy=copy)
+ self._scatter.setAttribute("x", x, copy=copy)
+ self._scatter.setAttribute("y", y, copy=copy)
+ self._scatter.setAttribute("z", z, copy=copy)
+ self._scatter.setAttribute("value", value, copy=copy)
self._setColormappedData(self.getValueData(copy=False), copy=False)
self._updated(ItemChangedType.DATA)
@@ -110,10 +105,12 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
False to return internal data (do not modify!)
:return: (x, y, z, value)
"""
- return (self.getXData(copy),
- self.getYData(copy),
- self.getZData(copy),
- self.getValueData(copy))
+ return (
+ self.getXData(copy),
+ self.getYData(copy),
+ self.getZData(copy),
+ self.getValueData(copy),
+ )
def getXData(self, copy=True):
"""Returns X data coordinates.
@@ -123,7 +120,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: X coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('x', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("x", copy=copy).reshape(-1)
def getYData(self, copy=True):
"""Returns Y data coordinates.
@@ -133,7 +130,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Y coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('y', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("y", copy=copy).reshape(-1)
def getZData(self, copy=True):
"""Returns Z data coordinates.
@@ -143,7 +140,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Z coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('z', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("z", copy=copy).reshape(-1)
def getValueData(self, copy=True):
"""Returns data values.
@@ -153,14 +150,9 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: data values
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('value', copy=copy).reshape(-1)
-
- @deprecated(reason="Consistency with PlotWidget items",
- replacement="getValueData", since_version="0.10.0")
- def getValues(self, copy=True):
- return self.getValueData(copy)
+ return self._scatter.getAttribute("value", copy=copy).reshape(-1)
- def _pickFull(self, context, threshold=0., sort='depth'):
+ def _pickFull(self, context, threshold=0.0, sort="depth"):
"""Perform picking in this item at given widget position.
:param PickContext context: Current picking context
@@ -174,9 +166,9 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Object holding the results or None
:rtype: Union[None,PickingResult]
"""
- assert sort in ('index', 'depth')
+ assert sort in ("index", "depth")
- rayNdc = context.getPickingSegment(frame='ndc')
+ rayNdc = context.getPickingSegment(frame="ndc")
if rayNdc is None: # No picking outside viewport
return None
@@ -187,49 +179,57 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
primitive = self._getScenePrimitive()
- dataPoints = numpy.transpose((xData,
- self.getYData(copy=False),
- self.getZData(copy=False),
- numpy.ones_like(xData)))
+ dataPoints = numpy.transpose(
+ (
+ xData,
+ self.getYData(copy=False),
+ self.getZData(copy=False),
+ numpy.ones_like(xData),
+ )
+ )
pointsNdc = primitive.objectToNDCTransform.transformPoints(
- dataPoints, perspectiveDivide=True)
+ dataPoints, perspectiveDivide=True
+ )
# Perform picking
distancesNdc = numpy.abs(pointsNdc[:, :2] - rayNdc[0, :2])
# TODO issue with symbol size: using pixel instead of points
threshold += self.getSymbolSize()
- thresholdNdc = 2. * threshold / numpy.array(primitive.viewport.size)
- picked = numpy.where(numpy.logical_and(
+ thresholdNdc = 2.0 * threshold / numpy.array(primitive.viewport.size)
+ picked = numpy.where(
+ numpy.logical_and(
numpy.all(distancesNdc < thresholdNdc, axis=1),
- numpy.logical_and(rayNdc[0, 2] <= pointsNdc[:, 2],
- pointsNdc[:, 2] <= rayNdc[1, 2])))[0]
+ numpy.logical_and(
+ rayNdc[0, 2] <= pointsNdc[:, 2], pointsNdc[:, 2] <= rayNdc[1, 2]
+ ),
+ )
+ )[0]
- if sort == 'depth':
+ if sort == "depth":
# Sort picked points from front to back
picked = picked[numpy.argsort(pointsNdc[picked, 2])]
if picked.size > 0:
- return PickingResult(self,
- positions=dataPoints[picked, :3],
- indices=picked,
- fetchdata=self.getValueData)
+ return PickingResult(
+ self,
+ positions=dataPoints[picked, :3],
+ indices=picked,
+ fetchdata=self.getValueData,
+ )
else:
return None
-class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
- ScatterVisualizationMixIn):
+class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn, ScatterVisualizationMixIn):
"""2D scatter data with settable visualization mode.
:param parent: The View widget this item belongs to.
"""
_VISUALIZATION_PROPERTIES = {
- ScatterVisualizationMixIn.Visualization.POINTS:
- ('symbol', 'symbolSize'),
- ScatterVisualizationMixIn.Visualization.LINES:
- ('lineWidth',),
+ ScatterVisualizationMixIn.Visualization.POINTS: ("symbol", "symbolSize"),
+ ScatterVisualizationMixIn.Visualization.LINES: ("lineWidth",),
ScatterVisualizationMixIn.Visualization.SOLID: (),
}
"""Dict {visualization mode: property names used in this mode}"""
@@ -244,7 +244,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
ScatterVisualizationMixIn.__init__(self)
self._heightMap = False
- self._lineWidth = 1.
+ self._lineWidth = 1.0
self._x = numpy.zeros((0,), dtype=numpy.float32)
self._y = numpy.zeros((0,), dtype=numpy.float32)
@@ -263,7 +263,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
for child in self._getScenePrimitive().children:
if isinstance(child, primitives.Points):
child.marker = symbol
- child.setAttribute('size', size, copy=True)
+ child.setAttribute("size", size, copy=True)
elif event is ItemChangedType.VISIBLE:
# TODO smart update?, need dirty flags
@@ -284,7 +284,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
By default, it is the current visualization mode.
:return:
"""
- assert name in ('lineWidth', 'symbol', 'symbolSize')
+ assert name in ("lineWidth", "symbol", "symbolSize")
if visualization is None:
visualization = self.getVisualization()
assert visualization in self.supportedVisualizations()
@@ -325,11 +325,11 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
:param float width: Width in pixels
"""
width = float(width)
- assert width >= 1.
+ assert width >= 1.0
if width != self._lineWidth:
self._lineWidth = width
for child in self._getScenePrimitive().children:
- if hasattr(child, 'lineWidth'):
+ if hasattr(child, "lineWidth"):
child.lineWidth = width
self._updated(ItemChangedType.LINE_WIDTH)
@@ -345,15 +345,14 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
True (default) to make a copy of the data,
False to avoid copy if possible (do not modify the arrays).
"""
- x = numpy.array(
- x, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
- y = numpy.array(
- y, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
+ x = numpy.array(x, copy=copy, dtype=numpy.float32, order="C").reshape(-1)
+ y = numpy.array(y, copy=copy, dtype=numpy.float32, order="C").reshape(-1)
assert len(x) == len(y)
if isinstance(value, abc.Iterable):
value = numpy.array(
- value, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
+ value, copy=copy, dtype=numpy.float32, order="C"
+ ).reshape(-1)
assert len(value) == len(x)
else: # Single scalar
value = numpy.array((float(value),), dtype=numpy.float32)
@@ -379,9 +378,11 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
False to return internal data (do not modify!)
:return: (x, y, value)
"""
- return (self.getXData(copy=copy),
- self.getYData(copy=copy),
- self.getValueData(copy=copy))
+ return (
+ self.getXData(copy=copy),
+ self.getYData(copy=copy),
+ self.getValueData(copy=copy),
+ )
def getXData(self, copy=True):
"""Returns X data coordinates.
@@ -413,12 +414,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
"""
return numpy.array(self._value, copy=copy)
- @deprecated(reason="Consistency with PlotWidget items",
- replacement="getValueData", since_version="0.10.0")
- def getValues(self, copy=True):
- return self.getValueData(copy)
-
- def _pickPoints(self, context, points, threshold=1., sort='depth'):
+ def _pickPoints(self, context, points, threshold=1.0, sort="depth"):
"""Perform picking while in 'points' visualization mode
:param PickContext context: Current picking context
@@ -432,34 +428,41 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
:return: Object holding the results or None
:rtype: Union[None,PickingResult]
"""
- assert sort in ('index', 'depth')
+ assert sort in ("index", "depth")
- rayNdc = context.getPickingSegment(frame='ndc')
+ rayNdc = context.getPickingSegment(frame="ndc")
if rayNdc is None: # No picking outside viewport
return None
# Project data to NDC
primitive = self._getScenePrimitive()
pointsNdc = primitive.objectToNDCTransform.transformPoints(
- points, perspectiveDivide=True)
+ points, perspectiveDivide=True
+ )
# Perform picking
distancesNdc = numpy.abs(pointsNdc[:, :2] - rayNdc[0, :2])
thresholdNdc = threshold / numpy.array(primitive.viewport.size)
- picked = numpy.where(numpy.logical_and(
- numpy.all(distancesNdc < thresholdNdc, axis=1),
- numpy.logical_and(rayNdc[0, 2] <= pointsNdc[:, 2],
- pointsNdc[:, 2] <= rayNdc[1, 2])))[0]
+ picked = numpy.where(
+ numpy.logical_and(
+ numpy.all(distancesNdc < thresholdNdc, axis=1),
+ numpy.logical_and(
+ rayNdc[0, 2] <= pointsNdc[:, 2], pointsNdc[:, 2] <= rayNdc[1, 2]
+ ),
+ )
+ )[0]
- if sort == 'depth':
+ if sort == "depth":
# Sort picked points from front to back
picked = picked[numpy.argsort(pointsNdc[picked, 2])]
if picked.size > 0:
- return PickingResult(self,
- positions=points[picked, :3],
- indices=picked,
- fetchdata=self.getValueData)
+ return PickingResult(
+ self,
+ positions=points[picked, :3],
+ indices=picked,
+ fetchdata=self.getValueData,
+ )
else:
return None
@@ -480,7 +483,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
trianglesIndices = self._cachedTrianglesIndices.reshape(-1, 3)
triangles = points[trianglesIndices, :3]
selectedIndices, t, barycentric = glu.segmentTrianglesIntersection(
- rayObject, triangles)
+ rayObject, triangles
+ )
closest = numpy.argmax(barycentric, axis=1)
indices = trianglesIndices.reshape(-1, 3)[selectedIndices, closest]
@@ -491,10 +495,9 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
# Compute intersection points and get closest data point
positions = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
- return PickingResult(self,
- positions=positions,
- indices=indices,
- fetchdata=self.getValueData)
+ return PickingResult(
+ self, positions=positions, indices=indices, fetchdata=self.getValueData
+ )
def _pickFull(self, context):
"""Perform picking in this item at given widget position.
@@ -512,22 +515,20 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
else:
zData = numpy.zeros_like(xData)
- points = numpy.transpose((xData,
- self.getYData(copy=False),
- zData,
- numpy.ones_like(xData)))
+ points = numpy.transpose(
+ (xData, self.getYData(copy=False), zData, numpy.ones_like(xData))
+ )
mode = self.getVisualization()
if mode is self.Visualization.POINTS:
# TODO issue with symbol size: using pixel instead of points
# Get "corrected" symbol size
_, threshold = self._getSceneSymbol()
- return self._pickPoints(
- context, points, threshold=max(3., threshold))
+ return self._pickPoints(context, points, threshold=max(3.0, threshold))
elif mode is self.Visualization.LINES:
# Picking only at point
- return self._pickPoints(context, points, threshold=5.)
+ return self._pickPoints(context, points, threshold=5.0)
else: # mode == 'solid'
return self._pickSolid(context, points)
@@ -546,36 +547,38 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
heightMap = self.isHeightMap()
if mode is self.Visualization.POINTS:
- z = value if heightMap else 0.
+ z = value if heightMap else 0.0
symbol, size = self._getSceneSymbol()
primitive = primitives.Points(
- x=x, y=y, z=z, value=value,
- size=size,
- colormap=self._getSceneColormap())
+ x=x, y=y, z=z, value=value, size=size, colormap=self._getSceneColormap()
+ )
primitive.marker = symbol
else:
# TODO run delaunay in a thread
# Compute lines/triangles indices if not cached
if self._cachedTrianglesIndices is None:
- triangulation = delaunay(x, y)
- if triangulation is None:
+ try:
+ triangulation = Triangulation(x, y)
+ except (RuntimeError, ValueError):
+ _logger.debug("Delaunay tesselation failed: %s", sys.exc_info()[1])
return None
self._cachedTrianglesIndices = numpy.ravel(
- triangulation.simplices.astype(numpy.uint32))
+ triangulation.triangles.astype(numpy.uint32)
+ )
- if (mode is self.Visualization.LINES and
- self._cachedLinesIndices is None):
+ if mode is self.Visualization.LINES and self._cachedLinesIndices is None:
# Compute line indices
self._cachedLinesIndices = utils.triangleToLineIndices(
- self._cachedTrianglesIndices, unicity=True)
+ self._cachedTrianglesIndices, unicity=True
+ )
if mode is self.Visualization.LINES:
indices = self._cachedLinesIndices
- renderMode = 'lines'
+ renderMode = "lines"
else:
indices = self._cachedTrianglesIndices
- renderMode = 'triangles'
+ renderMode = "triangles"
# TODO supports x, y instead of copy
if heightMap:
@@ -593,14 +596,15 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
if len(value) > 1:
value = value[indices]
triangleNormals = utils.trianglesNormal(coordinates)
- normal = numpy.empty((len(triangleNormals) * 3, 3),
- dtype=numpy.float32)
+ normal = numpy.empty(
+ (len(triangleNormals) * 3, 3), dtype=numpy.float32
+ )
normal[0::3, :] = triangleNormals
normal[1::3, :] = triangleNormals
normal[2::3, :] = triangleNormals
indices = None
else:
- normal = (0., 0., 1.)
+ normal = (0.0, 0.0, 1.0)
else:
normal = None
@@ -610,7 +614,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
normal=normal,
colormap=self._getSceneColormap(),
indices=indices,
- mode=renderMode)
+ mode=renderMode,
+ )
primitive.lineWidth = self.getLineWidth()
primitive.lineSmooth = False
diff --git a/src/silx/gui/plot3d/items/volume.py b/src/silx/gui/plot3d/items/volume.py
index f80fea2..7696794 100644
--- a/src/silx/gui/plot3d/items/volume.py
+++ b/src/silx/gui/plot3d/items/volume.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides 3D array item class and its sub-items.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
@@ -61,12 +58,13 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
"""
def __init__(self, parent):
- plane = cutplane.CutPlane(normal=(0, 1, 0))
-
Item3D.__init__(self, parent=None)
ColormapMixIn.__init__(self)
InterpolationMixIn.__init__(self)
- PlaneMixIn.__init__(self, plane=plane)
+ PlaneMixIn.__init__(self)
+
+ plane = cutplane.CutPlane(normal=(0, 1, 0))
+ self._setPlane(plane)
self._dataRange = None
self._data = None
@@ -95,10 +93,13 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
self._dataRange = range_
if range_ is None:
range_ = None, None, None
- self._setColormappedData(self._data, copy=False,
- min_=range_[0],
- minPositive=range_[1],
- max_=range_[2])
+ self._setColormappedData(
+ self._data,
+ copy=False,
+ min_=range_[0],
+ minPositive=range_[1],
+ max_=range_[2],
+ )
self._updated(ItemChangedType.DATA)
@@ -187,10 +188,11 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
rayObject[0, :3],
rayObject[1, :3],
planeNorm=self.getNormal(),
- planePt=self.getPoint())
+ planePt=self.getPoint(),
+ )
if len(points) == 1: # Single intersection
- if numpy.any(points[0] < 0.):
+ if numpy.any(points[0] < 0.0):
return None # Outside volume
z, y, x = int(points[0][2]), int(points[0][1]), int(points[0][0])
@@ -200,9 +202,9 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
depth, height, width = data.shape
if z < depth and y < height and x < width:
- return PickingResult(self,
- positions=[points[0]],
- indices=([z], [y], [x]))
+ return PickingResult(
+ self, positions=[points[0]], indices=([z], [y], [x])
+ )
else:
return None # Outside image
else: # Either no intersection or segment and image are coplanar
@@ -218,9 +220,9 @@ class Isosurface(Item3D):
def __init__(self, parent):
Item3D.__init__(self, parent=None)
self._data = None
- self._level = float('nan')
+ self._level = float("nan")
self._autoLevelFunction = None
- self._color = rgba('#FFD700FF')
+ self._color = rgba("#FFD700FF")
self.setParent(parent)
def _syncDataWithParent(self):
@@ -313,7 +315,7 @@ class Isosurface(Item3D):
"""
primitive = self._getScenePrimitive()
if len(primitive.children) != 0:
- primitive.children[0].setAttribute('color', color)
+ primitive.children[0].setAttribute("color", color)
def setColor(self, color):
"""Set the color of the iso-surface
@@ -337,7 +339,7 @@ class Isosurface(Item3D):
if data is None:
if self.isAutoLevel():
- self._level = float('nan')
+ self._level = float("nan")
else:
if self.isAutoLevel():
@@ -352,12 +354,12 @@ class Isosurface(Item3D):
"Error while executing iso level function %s.%s",
module_,
name,
- exc_info=True)
- level = float('nan')
+ exc_info=True,
+ )
+ level = float("nan")
else:
- _logger.info(
- 'Computed iso-level in %f s.', time.time() - st)
+ _logger.info("Computed iso-level in %f s.", time.time() - st)
if level != self._level:
self._level = level
@@ -365,10 +367,8 @@ class Isosurface(Item3D):
if numpy.isfinite(self._level):
st = time.time()
- vertices, normals, indices = MarchingCubes(
- data,
- isolevel=self._level)
- _logger.info('Computed iso-surface in %f s.', time.time() - st)
+ vertices, normals, indices = MarchingCubes(data, isolevel=self._level)
+ _logger.info("Computed iso-surface in %f s.", time.time() - st)
if len(vertices) != 0:
return vertices, normals, indices
@@ -381,12 +381,14 @@ class Isosurface(Item3D):
vertices, normals, indices = self._computeIsosurface()
if vertices is not None:
- mesh = primitives.Mesh3D(vertices,
- colors=self._color,
- normals=normals,
- mode='triangles',
- indices=indices,
- copy=False)
+ mesh = primitives.Mesh3D(
+ vertices,
+ colors=self._color,
+ normals=normals,
+ mode="triangles",
+ indices=indices,
+ copy=False,
+ )
self._getScenePrimitive().children = [mesh]
def _pickFull(self, context):
@@ -402,8 +404,7 @@ class Isosurface(Item3D):
rayObject = rayObject[:, :3]
data = self.getData(copy=False)
- bins = utils.segmentVolumeIntersect(
- rayObject, numpy.array(data.shape) - 1)
+ bins = utils.segmentVolumeIntersect(rayObject, numpy.array(data.shape) - 1)
if bins is None:
return None
@@ -416,8 +417,10 @@ class Isosurface(Item3D):
# check bin candidates
level = self.getLevel()
- mask = numpy.logical_and(numpy.nanmin(binsData, axis=1) <= level,
- level <= numpy.nanmax(binsData, axis=1))
+ mask = numpy.logical_and(
+ numpy.nanmin(binsData, axis=1) <= level,
+ level <= numpy.nanmax(binsData, axis=1),
+ )
bins = bins[mask]
binsData = binsData[mask]
@@ -479,19 +482,23 @@ class ScalarField3D(BaseNodeItem):
self._isogroup = primitives.GroupDepthOffset()
self._isogroup.transforms = [
# Convert from z, y, x from marching cubes to x, y, z
- transform.Matrix((
- (0., 0., 1., 0.),
- (0., 1., 0., 0.),
- (1., 0., 0., 0.),
- (0., 0., 0., 1.))),
+ transform.Matrix(
+ (
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, 1.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ )
+ ),
# Offset to match cutting plane coords
- transform.Translate(0.5, 0.5, 0.5)
+ transform.Translate(0.5, 0.5, 0.5),
]
self._getScenePrimitive().children = [
self._boundedGroup,
self._cutPlane._getScenePrimitive(),
- self._isogroup]
+ self._isogroup,
+ ]
@staticmethod
def _computeRangeFromData(data):
@@ -510,7 +517,7 @@ class ScalarField3D(BaseNodeItem):
if dataRange is not None:
min_positive = dataRange.min_positive
if min_positive is None:
- min_positive = float('nan')
+ min_positive = float("nan")
return dataRange.minimum, min_positive, dataRange.maximum
def setData(self, data, copy=True):
@@ -529,7 +536,7 @@ class ScalarField3D(BaseNodeItem):
self._boundedGroup.shape = None
else:
- data = numpy.array(data, copy=copy, dtype=numpy.float32, order='C')
+ data = numpy.array(data, copy=copy, dtype=numpy.float32, order="C")
assert data.ndim == 3
assert min(data.shape) >= 2
@@ -628,8 +635,8 @@ class ScalarField3D(BaseNodeItem):
"""
if isosurface not in self.getIsosurfaces():
_logger.warning(
- "Try to remove isosurface that is not in the list: %s",
- str(isosurface))
+ "Try to remove isosurface that is not in the list: %s", str(isosurface)
+ )
else:
isosurface.sigItemChanged.disconnect(self._isosurfaceItemChanged)
self._isosurfaces.remove(isosurface)
@@ -649,8 +656,9 @@ class ScalarField3D(BaseNodeItem):
def _updateIsosurfaces(self):
"""Handle updates of iso-surfaces level and add/remove"""
# Sorting using minus, this supposes data 'object' to be max values
- sortedIso = sorted(self.getIsosurfaces(),
- key=lambda isosurface: - isosurface.getLevel())
+ sortedIso = sorted(
+ self.getIsosurfaces(), key=lambda isosurface: -isosurface.getLevel()
+ )
self._isogroup.children = [iso._getScenePrimitive() for iso in sortedIso]
# BaseNodeItem
@@ -667,6 +675,7 @@ class ScalarField3D(BaseNodeItem):
# ComplexField3D #
##################
+
class ComplexCutPlane(CutPlane, ComplexMixIn):
"""Class representing a cutting plane in a :class:`ComplexField3D` item.
@@ -704,8 +713,9 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
:param parent: The DataItem3D this iso-surface belongs to
"""
- _SUPPORTED_COMPLEX_MODES = \
- (ComplexMixIn.ComplexMode.NONE,) + ComplexMixIn._SUPPORTED_COMPLEX_MODES
+ _SUPPORTED_COMPLEX_MODES = (
+ ComplexMixIn.ComplexMode.NONE,
+ ) + ComplexMixIn._SUPPORTED_COMPLEX_MODES
"""Overrides supported ComplexMode"""
def __init__(self, parent):
@@ -720,8 +730,9 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
:param List[float] color: RGBA channels in [0, 1]
"""
primitive = self._getScenePrimitive()
- if (len(primitive.children) != 0 and
- isinstance(primitive.children[0], primitives.ColormapMesh3D)):
+ if len(primitive.children) != 0 and isinstance(
+ primitive.children[0], primitives.ColormapMesh3D
+ ):
primitive.children[0].alpha = self._color[3]
else:
super(ComplexIsosurface, self)._updateColor(color)
@@ -732,15 +743,14 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
if parent is None:
self._data = None
else:
- self._data = parent.getData(
- mode=parent.getComplexMode(), copy=False)
+ self._data = parent.getData(mode=parent.getComplexMode(), copy=False)
if parent is None or self.getComplexMode() == self.ComplexMode.NONE:
self._setColormappedData(None, copy=False)
else:
self._setColormappedData(
- parent.getData(mode=self.getComplexMode(), copy=False),
- copy=False)
+ parent.getData(mode=self.getComplexMode(), copy=False), copy=False
+ )
self._updateScenePrimitive()
@@ -758,8 +768,7 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
if event == ItemChangedType.COMPLEX_MODE:
self._syncDataWithParent()
- elif event in (ItemChangedType.COLORMAP,
- Item3DChangedType.INTERPOLATION):
+ elif event in (ItemChangedType.COLORMAP, Item3DChangedType.INTERPOLATION):
self._updateScenePrimitive()
super(ComplexIsosurface, self)._updated(event)
@@ -775,7 +784,7 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
if values is not None:
vertices, normals, indices = self._computeIsosurface()
if vertices is not None:
- values = interp3d(values, vertices, method='linear_omp')
+ values = interp3d(values, vertices, method="linear_omp")
# TODO reuse isosurface when only color changes...
mesh = primitives.ColormapMesh3D(
@@ -783,9 +792,10 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
value=values.reshape(-1, 1),
colormap=self._getSceneColormap(),
normal=normals,
- mode='triangles',
+ mode="triangles",
indices=indices,
- copy=False)
+ copy=False,
+ )
mesh.alpha = self._color[3]
self._getScenePrimitive().children = [mesh]
@@ -829,7 +839,7 @@ class ComplexField3D(ScalarField3D, ComplexMixIn):
self._boundedGroup.shape = None
else:
- data = numpy.array(data, copy=copy, dtype=numpy.complex64, order='C')
+ data = numpy.array(data, copy=copy, dtype=numpy.complex64, order="C")
assert data.ndim == 3
assert min(data.shape) >= 2
diff --git a/src/silx/gui/plot3d/scene/__init__.py b/src/silx/gui/plot3d/scene/__init__.py
index 9671725..9f7c470 100644
--- a/src/silx/gui/plot3d/scene/__init__.py
+++ b/src/silx/gui/plot3d/scene/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot3d/scene/axes.py b/src/silx/gui/plot3d/scene/axes.py
index e35e5e1..9102732 100644
--- a/src/silx/gui/plot3d/scene/axes.py
+++ b/src/silx/gui/plot3d/scene/axes.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""Primitive displaying a text field in the scene."""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "17/10/2016"
@@ -43,40 +40,37 @@ _logger = logging.getLogger(__name__)
class LabelledAxes(primitives.GroupBBox):
- """A group displaying a bounding box with axes labels around its children.
- """
+ """A group displaying a bounding box with axes labels around its children."""
def __init__(self):
super(LabelledAxes, self).__init__()
self._ticksForBounds = None
- self._font = text.Font()
+ self._font = text.Font(size=10)
self._boxVisibility = True
# TODO offset labels from anchor in pixels
self._xlabel = text.Text2D(font=self._font)
- self._xlabel.align = 'center'
- self._xlabel.transforms = [self._boxTransforms,
- transform.Translate(tx=0.5)]
+ self._xlabel.align = "center"
+ self._xlabel.transforms = [self._boxTransforms, transform.Translate(tx=0.5)]
self._children.insert(-1, self._xlabel)
self._ylabel = text.Text2D(font=self._font)
- self._ylabel.align = 'center'
- self._ylabel.transforms = [self._boxTransforms,
- transform.Translate(ty=0.5)]
+ self._ylabel.align = "center"
+ self._ylabel.transforms = [self._boxTransforms, transform.Translate(ty=0.5)]
self._children.insert(-1, self._ylabel)
self._zlabel = text.Text2D(font=self._font)
- self._zlabel.align = 'center'
- self._zlabel.transforms = [self._boxTransforms,
- transform.Translate(tz=0.5)]
+ self._zlabel.align = "center"
+ self._zlabel.transforms = [self._boxTransforms, transform.Translate(tz=0.5)]
self._children.insert(-1, self._zlabel)
# Init tick lines with dummy pos
self._tickLines = primitives.DashedLines(
- positions=((0., 0., 0.), (0., 0., 0.)))
+ positions=((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))
+ )
self._tickLines.dash = 5, 10
self._tickLines.visible = False
self._children.insert(-1, self._tickLines)
@@ -85,7 +79,7 @@ class LabelledAxes(primitives.GroupBBox):
self._children.insert(-1, self._tickLabels)
# Sync color
- self.tickColor = 1., 1., 1., 1.
+ self.tickColor = 1.0, 1.0, 1.0, 1.0
def _updateBoxAndAxes(self):
"""Update bbox and axes position and size according to children.
@@ -96,7 +90,7 @@ class LabelledAxes(primitives.GroupBBox):
bounds = self._group.bounds(dataBounds=True)
if bounds is not None:
- tx, ty, tz = (bounds[1] - bounds[0]) / 2.
+ tx, ty, tz = (bounds[1] - bounds[0]) / 2.0
else:
tx, ty, tz = 0.5, 0.5, 0.5
@@ -119,7 +113,7 @@ class LabelledAxes(primitives.GroupBBox):
self._ylabel.foreground = color
self._zlabel.foreground = color
transparentColor = color[0], color[1], color[2], color[3] * 0.6
- self._tickLines.setAttribute('color', transparentColor)
+ self._tickLines.setAttribute("color", transparentColor)
for label in self._tickLabels.children:
label.foreground = color
@@ -188,8 +182,9 @@ class LabelledAxes(primitives.GroupBBox):
self._tickLines.visible = False
self._tickLabels.children = [] # Reset previous labels
- elif (self._ticksForBounds is None or
- not numpy.all(numpy.equal(bounds, self._ticksForBounds))):
+ elif self._ticksForBounds is None or not numpy.all(
+ numpy.equal(bounds, self._ticksForBounds)
+ ):
self._ticksForBounds = bounds
# Update ticks
@@ -201,21 +196,21 @@ class LabelledAxes(primitives.GroupBBox):
# Update tick lines
coords = numpy.empty(
- ((len(xticks) + len(yticks) + len(zticks)), 4, 3),
- dtype=numpy.float32)
+ ((len(xticks) + len(yticks) + len(zticks)), 4, 3), dtype=numpy.float32
+ )
coords[:, :, :] = bounds[0, :] # account for offset from origin
- xcoords = coords[:len(xticks)]
+ xcoords = coords[: len(xticks)]
xcoords[:, :, 0] = numpy.asarray(xticks)[:, numpy.newaxis]
xcoords[:, 1, 1] += ticklength[1] # X ticks on XY plane
xcoords[:, 3, 2] += ticklength[2] # X ticks on XZ plane
- ycoords = coords[len(xticks):len(xticks) + len(yticks)]
+ ycoords = coords[len(xticks) : len(xticks) + len(yticks)]
ycoords[:, :, 1] = numpy.asarray(yticks)[:, numpy.newaxis]
ycoords[:, 1, 0] += ticklength[0] # Y ticks on XY plane
ycoords[:, 3, 2] += ticklength[2] # Y ticks on YZ plane
- zcoords = coords[len(xticks) + len(yticks):]
+ zcoords = coords[len(xticks) + len(yticks) :]
zcoords[:, :, 2] = numpy.asarray(zticks)[:, numpy.newaxis]
zcoords[:, 1, 0] += ticklength[0] # Z ticks on XZ plane
zcoords[:, 3, 1] += ticklength[1] # Z ticks on YZ plane
@@ -225,30 +220,36 @@ class LabelledAxes(primitives.GroupBBox):
# Update labels
color = self.tickColor
- offsets = bounds[0] - ticklength / 20.
+ offsets = bounds[0] - ticklength / 20.0
labels = []
for tick, label in zip(xticks, xlabels):
text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
+ text2d.align = "center"
+ text2d.valign = "center"
text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=tick, ty=offsets[1], tz=offsets[2])]
+ text2d.transforms = [
+ transform.Translate(tx=tick, ty=offsets[1], tz=offsets[2])
+ ]
labels.append(text2d)
for tick, label in zip(yticks, ylabels):
text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
+ text2d.align = "center"
+ text2d.valign = "center"
text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=offsets[0], ty=tick, tz=offsets[2])]
+ text2d.transforms = [
+ transform.Translate(tx=offsets[0], ty=tick, tz=offsets[2])
+ ]
labels.append(text2d)
for tick, label in zip(zticks, zlabels):
text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
+ text2d.align = "center"
+ text2d.valign = "center"
text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=offsets[0], ty=offsets[1], tz=tick)]
+ text2d.transforms = [
+ transform.Translate(tx=offsets[0], ty=offsets[1], tz=tick)
+ ]
labels.append(text2d)
self._tickLabels.children = labels # Reset previous labels
diff --git a/src/silx/gui/plot3d/scene/camera.py b/src/silx/gui/plot3d/scene/camera.py
index 90de7ed..5248c39 100644
--- a/src/silx/gui/plot3d/scene/camera.py
+++ b/src/silx/gui/plot3d/scene/camera.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module provides classes to handle a perspective projection in 3D."""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "25/07/2016"
@@ -38,6 +35,7 @@ from . import transform
# CameraExtrinsic #############################################################
+
class CameraExtrinsic(transform.Transform):
"""Transform matrix to handle camera position and orientation.
@@ -49,21 +47,19 @@ class CameraExtrinsic(transform.Transform):
:type up: numpy.ndarray-like of 3 float32.
"""
- def __init__(self, position=(0., 0., 0.),
- direction=(0., 0., -1.),
- up=(0., 1., 0.)):
-
+ def __init__(
+ self, position=(0.0, 0.0, 0.0), direction=(0.0, 0.0, -1.0), up=(0.0, 1.0, 0.0)
+ ):
super(CameraExtrinsic, self).__init__()
self._position = None
self.position = position # set _position
- self._side = 1., 0., 0.
- self._up = 0., 1., 0.
- self._direction = 0., 0., -1.
+ self._side = 1.0, 0.0, 0.0
+ self._up = 0.0, 1.0, 0.0
+ self._direction = 0.0, 0.0, -1.0
self.setOrientation(direction=direction, up=up) # set _direction, _up
def _makeMatrix(self):
- return transform.mat4LookAtDir(self._position,
- self._direction, self._up)
+ return transform.mat4LookAtDir(self._position, self._direction, self._up)
def copy(self):
"""Return an independent copy"""
@@ -96,8 +92,8 @@ class CameraExtrinsic(transform.Transform):
# Update side and up to make sure they are perpendicular and normalized
side = numpy.cross(direction, up)
sidenormal = numpy.linalg.norm(side)
- if sidenormal == 0.:
- raise RuntimeError('direction and up vectors are parallel.')
+ if sidenormal == 0.0:
+ raise RuntimeError("direction and up vectors are parallel.")
# Alternative: when one of the input parameter is None, it is
# possible to guess correct vectors using previous direction and up
side /= sidenormal
@@ -131,8 +127,7 @@ class CameraExtrinsic(transform.Transform):
@property
def up(self):
- """Vector pointing upward in the image plane (ndarray of 3 float32).
- """
+ """Vector pointing upward in the image plane (ndarray of 3 float32)."""
return self._up.copy()
@up.setter
@@ -146,7 +141,7 @@ class CameraExtrinsic(transform.Transform):
ndarray of 3 float32"""
return self._side.copy()
- def move(self, direction, step=1.):
+ def move(self, direction, step=1.0):
"""Move the camera relative to the image plane.
:param str direction: Direction relative to image plane.
@@ -155,35 +150,35 @@ class CameraExtrinsic(transform.Transform):
:param float step: The step of the pan to perform in the coordinate
in which the camera position is defined.
"""
- if direction in ('up', 'down'):
- vector = self.up * (1. if direction == 'up' else -1.)
- elif direction in ('left', 'right'):
- vector = self.side * (1. if direction == 'right' else -1.)
- elif direction in ('forward', 'backward'):
- vector = self.direction * (1. if direction == 'forward' else -1.)
+ if direction in ("up", "down"):
+ vector = self.up * (1.0 if direction == "up" else -1.0)
+ elif direction in ("left", "right"):
+ vector = self.side * (1.0 if direction == "right" else -1.0)
+ elif direction in ("forward", "backward"):
+ vector = self.direction * (1.0 if direction == "forward" else -1.0)
else:
- raise ValueError('Unsupported direction: %s' % direction)
+ raise ValueError("Unsupported direction: %s" % direction)
self.position += step * vector
- def rotate(self, direction, angle=1.):
+ def rotate(self, direction, angle=1.0):
"""First-person rotation of the camera towards the direction.
:param str direction: Direction of movement relative to image plane.
In: 'up', 'down', 'left', 'right'.
:param float angle: The angle in degrees of the rotation.
"""
- if direction in ('up', 'down'):
- axis = self.side * (1. if direction == 'up' else -1.)
- elif direction in ('left', 'right'):
- axis = self.up * (1. if direction == 'left' else -1.)
+ if direction in ("up", "down"):
+ axis = self.side * (1.0 if direction == "up" else -1.0)
+ elif direction in ("left", "right"):
+ axis = self.up * (1.0 if direction == "left" else -1.0)
else:
- raise ValueError('Unsupported direction: %s' % direction)
+ raise ValueError("Unsupported direction: %s" % direction)
matrix = transform.mat4RotateFromAngleAxis(numpy.radians(angle), *axis)
newdir = numpy.dot(matrix[:3, :3], self.direction)
- if direction in ('up', 'down'):
+ if direction in ("up", "down"):
# Rotate up to avoid up and new direction to be (almost) co-linear
newup = numpy.dot(matrix[:3, :3], self.up)
self.setOrientation(newdir, newup)
@@ -191,7 +186,7 @@ class CameraExtrinsic(transform.Transform):
# No need to rotate up here as it is the rotation axis
self.direction = newdir
- def orbit(self, direction, center=(0., 0., 0.), angle=1.):
+ def orbit(self, direction, center=(0.0, 0.0, 0.0), angle=1.0):
"""Rotate the camera around a point.
:param str direction: Direction of movement relative to image plane.
@@ -200,33 +195,32 @@ class CameraExtrinsic(transform.Transform):
:type center: numpy.ndarray-like of 3 float32.
:param float angle: he angle in degrees of the rotation.
"""
- if direction in ('up', 'down'):
- axis = self.side * (1. if direction == 'down' else -1.)
- elif direction in ('left', 'right'):
- axis = self.up * (1. if direction == 'right' else -1.)
+ if direction in ("up", "down"):
+ axis = self.side * (1.0 if direction == "down" else -1.0)
+ elif direction in ("left", "right"):
+ axis = self.up * (1.0 if direction == "right" else -1.0)
else:
- raise ValueError('Unsupported direction: %s' % direction)
+ raise ValueError("Unsupported direction: %s" % direction)
# Rotate viewing direction
- rotmatrix = transform.mat4RotateFromAngleAxis(
- numpy.radians(angle), *axis)
+ rotmatrix = transform.mat4RotateFromAngleAxis(numpy.radians(angle), *axis)
self.direction = numpy.dot(rotmatrix[:3, :3], self.direction)
# Rotate position around center
center = numpy.array(center, copy=False, dtype=numpy.float32)
matrix = numpy.dot(transform.mat4Translate(*center), rotmatrix)
matrix = numpy.dot(matrix, transform.mat4Translate(*(-center)))
- position = numpy.append(self.position, 1.)
+ position = numpy.append(self.position, 1.0)
self.position = numpy.dot(matrix, position)[:3]
_RESET_CAMERA_ORIENTATIONS = {
- 'side': ((-1., -1., -1.), (0., 1., 0.)),
- 'front': ((0., 0., -1.), (0., 1., 0.)),
- 'back': ((0., 0., 1.), (0., 1., 0.)),
- 'top': ((0., -1., 0.), (0., 0., -1.)),
- 'bottom': ((0., 1., 0.), (0., 0., 1.)),
- 'right': ((-1., 0., 0.), (0., 1., 0.)),
- 'left': ((1., 0., 0.), (0., 1., 0.))
+ "side": ((-1.0, -1.0, -1.0), (0.0, 1.0, 0.0)),
+ "front": ((0.0, 0.0, -1.0), (0.0, 1.0, 0.0)),
+ "back": ((0.0, 0.0, 1.0), (0.0, 1.0, 0.0)),
+ "top": ((0.0, -1.0, 0.0), (0.0, 0.0, -1.0)),
+ "bottom": ((0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
+ "right": ((-1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
+ "left": ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
}
def reset(self, face=None):
@@ -236,12 +230,12 @@ class CameraExtrinsic(transform.Transform):
side, front, back, top, bottom, right, left.
"""
if face not in self._RESET_CAMERA_ORIENTATIONS:
- raise ValueError('Unsupported face: %s' % face)
+ raise ValueError("Unsupported face: %s" % face)
distance = numpy.linalg.norm(self.position)
direction, up = self._RESET_CAMERA_ORIENTATIONS[face]
self.setOrientation(direction, up)
- self.position = - self.direction * distance
+ self.position = -self.direction * distance
class Camera(transform.Transform):
@@ -263,9 +257,16 @@ class Camera(transform.Transform):
:type up: numpy.ndarray-like of 3 float32.
"""
- def __init__(self, fovy=30., near=0.1, far=1., size=(1., 1.),
- position=(0., 0., 0.),
- direction=(0., 0., -1.), up=(0., 1., 0.)):
+ def __init__(
+ self,
+ fovy=30.0,
+ near=0.1,
+ far=1.0,
+ size=(1.0, 1.0),
+ position=(0.0, 0.0, 0.0),
+ direction=(0.0, 0.0, -1.0),
+ up=(0.0, 1.0, 0.0),
+ ):
super(Camera, self).__init__()
self._intrinsic = transform.Perspective(fovy, near, far, size)
self._intrinsic.addListener(self._transformChanged)
@@ -292,8 +293,8 @@ class Camera(transform.Transform):
center = 0.5 * (bounds[0] + bounds[1])
radius = numpy.linalg.norm(0.5 * (bounds[1] - bounds[0]))
- if radius == 0.: # bounds are all collapsed
- radius = 1.
+ if radius == 0.0: # bounds are all collapsed
+ radius = 1.0
if isinstance(self.intrinsic, transform.Perspective):
# Get the viewpoint distance from the bounds center
@@ -305,8 +306,7 @@ class Camera(transform.Transform):
offset = radius / numpy.sin(0.5 * minfov)
# Update camera
- self.extrinsic.position = \
- center - offset * self.extrinsic.direction
+ self.extrinsic.position = center - offset * self.extrinsic.direction
self.intrinsic.setDepthExtent(offset - radius, offset + radius)
elif isinstance(self.intrinsic, transform.Orthographic):
@@ -315,14 +315,14 @@ class Camera(transform.Transform):
left=center[0] - radius,
right=center[0] + radius,
bottom=center[1] - radius,
- top=center[1] + radius)
+ top=center[1] + radius,
+ )
# Update camera
self.extrinsic.position = 0, 0, 0
- self.intrinsic.setDepthExtent(center[2] - radius,
- center[2] + radius)
+ self.intrinsic.setDepthExtent(center[2] - radius, center[2] + radius)
else:
- raise RuntimeError('Unsupported camera: %s' % self.intrinsic)
+ raise RuntimeError("Unsupported camera: %s" % self.intrinsic)
@property
def intrinsic(self):
diff --git a/src/silx/gui/plot3d/scene/core.py b/src/silx/gui/plot3d/scene/core.py
index 43838fe..8773301 100644
--- a/src/silx/gui/plot3d/scene/core.py
+++ b/src/silx/gui/plot3d/scene/core.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
@@ -32,8 +31,6 @@ Nodes with children are provided with :class:`PrivateGroup` and
Leaf rendering nodes should inherit from :class:`Elem`.
"""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "25/07/2016"
@@ -52,6 +49,7 @@ from .viewport import Viewport
# Nodes #######################################################################
+
class Base(event.Notifier):
"""A scene node with common features."""
@@ -67,10 +65,8 @@ class Base(event.Notifier):
# notifying properties
- visible = event.notifyProperty('_visible',
- doc="Visibility flag of the node")
- pickable = event.notifyProperty('_pickable',
- doc="True to make node pickable")
+ visible = event.notifyProperty("_visible", doc="Visibility flag of the node")
+ pickable = event.notifyProperty("_pickable", doc="True to make node pickable")
# Access to tree path
@@ -87,7 +83,7 @@ class Base(event.Notifier):
:param Base parent: The parent.
"""
if parent is not None and self._parentRef is not None:
- raise RuntimeError('Trying to add a node at two places.')
+ raise RuntimeError("Trying to add a node at two places.")
# Alternative: remove it from previous children list
self._parentRef = None if parent is None else weakref.ref(parent)
@@ -99,11 +95,11 @@ class Base(event.Notifier):
then the :class:`Viewport` is the first element of path.
"""
if self.parent is None:
- return self,
+ return (self,)
elif isinstance(self.parent, Viewport):
return self.parent, self
else:
- return self.parent.path + (self, )
+ return self.parent.path + (self,)
@property
def viewport(self):
@@ -157,7 +153,7 @@ class Base(event.Notifier):
# If it is a TransformList, do not create one to enable sharing.
self._transforms = iterable
else:
- assert hasattr(iterable, '__iter__')
+ assert hasattr(iterable, "__iter__")
self._transforms = transform.TransformList(iterable)
self._transforms.addListener(self._transformChanged)
@@ -166,8 +162,9 @@ class Base(event.Notifier):
# Bounds
- _CUBE_CORNERS = numpy.array(list(itertools.product((0., 1.), repeat=3)),
- dtype=numpy.float32)
+ _CUBE_CORNERS = numpy.array(
+ list(itertools.product((0.0, 1.0), repeat=3)), dtype=numpy.float32
+ )
"""Unit cube corners used to transform bounds"""
def _bounds(self, dataBounds=False):
@@ -259,7 +256,8 @@ class PrivateGroup(Base):
def _listWillChangeHook(self, methodName, *args, **kwargs):
super(PrivateGroup.ChildrenList, self)._listWillChangeHook(
- methodName, *args, **kwargs)
+ methodName, *args, **kwargs
+ )
for item in self:
item._setParent(None)
@@ -267,7 +265,8 @@ class PrivateGroup(Base):
for item in self:
item._setParent(self._parentRef())
super(PrivateGroup.ChildrenList, self)._listWasChangedHook(
- methodName, *args, **kwargs)
+ methodName, *args, **kwargs
+ )
def __init__(self, parent, children):
self._parentRef = weakref.ref(parent)
@@ -306,8 +305,7 @@ class PrivateGroup(Base):
bounds = []
for child in self._children:
if child.visible:
- childBounds = child.bounds(
- transformed=True, dataBounds=dataBounds)
+ childBounds = child.bounds(transformed=True, dataBounds=dataBounds)
if childBounds is not None:
bounds.append(childBounds)
@@ -315,9 +313,10 @@ class PrivateGroup(Base):
return None
else:
bounds = numpy.array(bounds, dtype=numpy.float32)
- return numpy.array((bounds[:, 0, :].min(axis=0),
- bounds[:, 1, :].max(axis=0)),
- dtype=numpy.float32)
+ return numpy.array(
+ (bounds[:, 0, :].min(axis=0), bounds[:, 1, :].max(axis=0)),
+ dtype=numpy.float32,
+ )
def prepareGL2(self, ctx):
pass
diff --git a/src/silx/gui/plot3d/scene/cutplane.py b/src/silx/gui/plot3d/scene/cutplane.py
index 88147df..f3b7494 100644
--- a/src/silx/gui/plot3d/scene/cutplane.py
+++ b/src/silx/gui/plot3d/scene/cutplane.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""A cut plane in a 3D texture: hackish implementation...
"""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "11/01/2018"
@@ -45,7 +42,8 @@ from . import transform, utils
class ColormapMesh3D(Geometry):
"""A 3D mesh with color from a 3D texture."""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 normal;
@@ -70,7 +68,8 @@ class ColormapMesh3D(Geometry):
gl_Position = matrix * vec4(position, 1.0);
}
""",
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -94,32 +93,41 @@ class ColormapMesh3D(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
-
- def __init__(self, position, normal, data, copy=True,
- mode='triangles', indices=None, colormap=None):
+ """
+ ),
+ )
+
+ def __init__(
+ self,
+ position,
+ normal,
+ data,
+ copy=True,
+ mode="triangles",
+ indices=None,
+ colormap=None,
+ ):
assert mode in self._TRIANGLE_MODES
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
assert data.ndim == 3
self._data = data
self._texture = None
self._update_texture = True
self._update_texture_filter = False
- self._alpha = 1.
+ self._alpha = 1.0
self._colormap = colormap or Colormap() # Default colormap
self._colormap.addListener(self._cmapChanged)
- self._interpolation = 'linear'
- super(ColormapMesh3D, self).__init__(mode,
- indices,
- position=position,
- normal=normal)
+ self._interpolation = "linear"
+ super(ColormapMesh3D, self).__init__(
+ mode, indices, position=position, normal=normal
+ )
self.isBackfaceVisible = True
- self.textureOffset = 0., 0., 0.
+ self.textureOffset = 0.0, 0.0, 0.0
"""Offset to add to texture coordinates"""
def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
assert data.ndim == 3
self._data = data
self._update_texture = True
@@ -134,7 +142,7 @@ class ColormapMesh3D(Geometry):
@interpolation.setter
def interpolation(self, interpolation):
- assert interpolation in ('linear', 'nearest')
+ assert interpolation in ("linear", "nearest")
self._interpolation = interpolation
self._update_texture_filter = True
self.notify()
@@ -162,21 +170,24 @@ class ColormapMesh3D(Geometry):
if self._texture is not None:
self._texture.discard()
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
filter_ = gl.GL_NEAREST
else:
filter_ = gl.GL_LINEAR
self._update_texture = False
self._update_texture_filter = False
self._texture = _glutils.Texture(
- gl.GL_R32F, self._data, gl.GL_RED,
+ gl.GL_R32F,
+ self._data,
+ gl.GL_RED,
minFilter=filter_,
magFilter=filter_,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
if self._update_texture_filter:
self._update_texture_filter = False
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
filter_ = gl.GL_NEAREST
else:
filter_ = gl.GL_LINEAR
@@ -193,8 +204,8 @@ class ColormapMesh3D(Geometry):
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call
- )
+ colormapCall=self.colormap.call,
+ )
program = ctx.glCtx.prog(self._shaders[0], fragment)
program.use()
@@ -205,18 +216,16 @@ class ColormapMesh3D(Geometry):
gl.glCullFace(gl.GL_BACK)
gl.glEnable(gl.GL_CULL_FACE)
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
+ program.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ program.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
+ gl.glUniform1f(program.uniforms["alpha"], self._alpha)
shape = self._data.shape
- scales = 1./shape[2], 1./shape[1], 1./shape[0]
- gl.glUniform3f(program.uniforms['dataScale'], *scales)
- gl.glUniform3f(program.uniforms['texCoordsOffset'], *self.textureOffset)
+ scales = 1.0 / shape[2], 1.0 / shape[1], 1.0 / shape[0]
+ gl.glUniform3f(program.uniforms["dataScale"], *scales)
+ gl.glUniform3f(program.uniforms["texCoordsOffset"], *self.textureOffset)
- gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
+ gl.glUniform1i(program.uniforms["data"], self._texture.texUnit)
ctx.setupProgram(program)
@@ -230,11 +239,11 @@ class ColormapMesh3D(Geometry):
class CutPlane(PlaneInGroup):
"""A cutting plane in a 3D texture"""
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
+ def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0)):
self._data = None
self._mesh = None
- self._alpha = 1.
- self._interpolation = 'linear'
+ self._alpha = 1.0
+ self._interpolation = "linear"
self._colormap = Colormap()
super(CutPlane, self).__init__(point, normal)
@@ -246,7 +255,7 @@ class CutPlane(PlaneInGroup):
self._mesh = None
else:
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
assert data.ndim == 3
self._data = data
if self._mesh is not None:
@@ -276,7 +285,7 @@ class CutPlane(PlaneInGroup):
@interpolation.setter
def interpolation(self, interpolation):
- assert interpolation in ('nearest', 'linear')
+ assert interpolation in ("nearest", "linear")
if interpolation != self.interpolation:
self._interpolation = interpolation
if self._mesh is not None:
@@ -285,45 +294,47 @@ class CutPlane(PlaneInGroup):
def prepareGL2(self, ctx):
if self.isValid:
-
contourVertices = self.contourVertices
if self._mesh is None and self._data is not None:
- self._mesh = ColormapMesh3D(contourVertices,
- normal=self.plane.normal,
- data=self._data,
- copy=False,
- mode='fan',
- colormap=self.colormap)
+ self._mesh = ColormapMesh3D(
+ contourVertices,
+ normal=self.plane.normal,
+ data=self._data,
+ copy=False,
+ mode="fan",
+ colormap=self.colormap,
+ )
self._mesh.alpha = self._alpha
self._mesh.interpolation = self.interpolation
self._children.insert(0, self._mesh)
if self._mesh is not None:
- if (contourVertices is None or
- len(contourVertices) == 0):
+ if contourVertices is None or len(contourVertices) == 0:
self._mesh.visible = False
else:
self._mesh.visible = True
- self._mesh.setAttribute('normal', self.plane.normal)
- self._mesh.setAttribute('position', contourVertices)
+ self._mesh.setAttribute("normal", self.plane.normal)
+ self._mesh.setAttribute("position", contourVertices)
needTextureOffset = False
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
# If cut plane is co-linear with array bin edges add texture offset
planePt = self.plane.point
- for index, normal in enumerate(((1., 0., 0.),
- (0., 1., 0.),
- (0., 0., 1.))):
- if (numpy.all(numpy.equal(self.plane.normal, normal)) and
- int(planePt[index]) == planePt[index]):
+ for index, normal in enumerate(
+ ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
+ ):
+ if (
+ numpy.all(numpy.equal(self.plane.normal, normal))
+ and int(planePt[index]) == planePt[index]
+ ):
needTextureOffset = True
break
if needTextureOffset:
self._mesh.textureOffset = self.plane.normal * 1e-6
else:
- self._mesh.textureOffset = 0., 0., 0.
+ self._mesh.textureOffset = 0.0, 0.0, 0.0
super(CutPlane, self).prepareGL2(ctx)
@@ -336,8 +347,8 @@ class CutPlane(PlaneInGroup):
vertices = self.contourVertices
if vertices is not None:
return numpy.array(
- (vertices.min(axis=0), vertices.max(axis=0)),
- dtype=numpy.float32)
+ (vertices.min(axis=0), vertices.max(axis=0)), dtype=numpy.float32
+ )
else:
return None # Plane in not slicing the data volume
else:
@@ -345,9 +356,9 @@ class CutPlane(PlaneInGroup):
return None
else:
depth, height, width = self._data.shape
- return numpy.array(((0., 0., 0.),
- (width, height, depth)),
- dtype=numpy.float32)
+ return numpy.array(
+ ((0.0, 0.0, 0.0), (width, height, depth)), dtype=numpy.float32
+ )
@property
def contourVertices(self):
@@ -367,7 +378,8 @@ class CutPlane(PlaneInGroup):
boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
lineIndices = Box.getLineIndices(copy=False)
vertices = utils.boxPlaneIntersect(
- boxVertices, lineIndices, self.plane.normal, self.plane.point)
+ boxVertices, lineIndices, self.plane.normal, self.plane.point
+ )
self._cache = bounds, vertices if len(vertices) != 0 else None
@@ -385,6 +397,6 @@ class CutPlane(PlaneInGroup):
# If it is a TransformList, do not create one to enable sharing.
self._transforms = iterable
else:
- assert hasattr(iterable, '__iter__')
+ assert hasattr(iterable, "__iter__")
self._transforms = transform.TransformList(iterable)
self._transforms.addListener(self._transformChanged)
diff --git a/src/silx/gui/plot3d/scene/event.py b/src/silx/gui/plot3d/scene/event.py
index 98f8f8b..4c6dd47 100644
--- a/src/silx/gui/plot3d/scene/event.py
+++ b/src/silx/gui/plot3d/scene/event.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module provides a simple generic notification system."""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "17/07/2018"
@@ -40,6 +37,7 @@ _logger = logging.getLogger(__name__)
# Notifier ####################################################################
+
class Notifier(object):
"""Base class for object with notification mechanism."""
@@ -56,7 +54,7 @@ class Notifier(object):
if listener not in self._listeners:
self._listeners.append(listener)
else:
- _logger.warning('Ignoring addition of an already registered listener')
+ _logger.warning("Ignoring addition of an already registered listener")
def removeListener(self, listener):
"""Remove a previously registered listener.
@@ -66,7 +64,7 @@ class Notifier(object):
try:
self._listeners.remove(listener)
except ValueError:
- _logger.warning('Trying to remove a listener that is not registered')
+ _logger.warning("Trying to remove a listener that is not registered")
def notify(self, *args, **kwargs):
"""Notify all registered listeners with the given parameters.
@@ -92,19 +90,24 @@ def notifyProperty(attrName, copy=False, converter=None, doc=None):
:return: A property with getter and setter
"""
if copy:
+
def getter(self):
return getattr(self, attrName).copy()
+
else:
+
def getter(self):
return getattr(self, attrName)
if converter is None:
+
def setter(self, value):
if getattr(self, attrName) != value:
setattr(self, attrName, value)
self.notify()
else:
+
def setter(self, value):
value = converter(value)
if getattr(self, attrName) != value:
@@ -120,7 +123,7 @@ class HookList(list):
def __init__(self, iterable):
super(HookList, self).__init__(iterable)
- self._listWasChangedHook('__init__', iterable)
+ self._listWasChangedHook("__init__", iterable)
def _listWillChangeHook(self, methodName, *args, **kwargs):
"""To override. Called before modifying the list.
@@ -143,57 +146,56 @@ class HookList(list):
def _wrapper(self, methodName, *args, **kwargs):
"""Generic wrapper of list methods calling the hooks."""
self._listWillChangeHook(methodName, *args, **kwargs)
- result = getattr(super(HookList, self),
- methodName)(*args, **kwargs)
+ result = getattr(super(HookList, self), methodName)(*args, **kwargs)
self._listWasChangedHook(methodName, *args, **kwargs)
return result
# Add methods
def __iadd__(self, *args, **kwargs):
- return self._wrapper('__iadd__', *args, **kwargs)
+ return self._wrapper("__iadd__", *args, **kwargs)
def __imul__(self, *args, **kwargs):
- return self._wrapper('__imul__', *args, **kwargs)
+ return self._wrapper("__imul__", *args, **kwargs)
def append(self, *args, **kwargs):
- return self._wrapper('append', *args, **kwargs)
+ return self._wrapper("append", *args, **kwargs)
def extend(self, *args, **kwargs):
- return self._wrapper('extend', *args, **kwargs)
+ return self._wrapper("extend", *args, **kwargs)
def insert(self, *args, **kwargs):
- return self._wrapper('insert', *args, **kwargs)
+ return self._wrapper("insert", *args, **kwargs)
# Remove methods
def __delitem__(self, *args, **kwargs):
- return self._wrapper('__delitem__', *args, **kwargs)
+ return self._wrapper("__delitem__", *args, **kwargs)
def __delslice__(self, *args, **kwargs):
- return self._wrapper('__delslice__', *args, **kwargs)
+ return self._wrapper("__delslice__", *args, **kwargs)
def remove(self, *args, **kwargs):
- return self._wrapper('remove', *args, **kwargs)
+ return self._wrapper("remove", *args, **kwargs)
def pop(self, *args, **kwargs):
- return self._wrapper('pop', *args, **kwargs)
+ return self._wrapper("pop", *args, **kwargs)
# Set methods
def __setitem__(self, *args, **kwargs):
- return self._wrapper('__setitem__', *args, **kwargs)
+ return self._wrapper("__setitem__", *args, **kwargs)
def __setslice__(self, *args, **kwargs):
- return self._wrapper('__setslice__', *args, **kwargs)
+ return self._wrapper("__setslice__", *args, **kwargs)
# In place methods
def sort(self, *args, **kwargs):
- return self._wrapper('sort', *args, **kwargs)
+ return self._wrapper("sort", *args, **kwargs)
def reverse(self, *args, **kwargs):
- return self._wrapper('reverse', *args, **kwargs)
+ return self._wrapper("reverse", *args, **kwargs)
class NotifierList(HookList, Notifier):
diff --git a/src/silx/gui/plot3d/scene/function.py b/src/silx/gui/plot3d/scene/function.py
index 2deb785..cde7cad 100644
--- a/src/silx/gui/plot3d/scene/function.py
+++ b/src/silx/gui/plot3d/scene/function.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2020 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module provides functions to add to shaders."""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "17/07/2018"
@@ -47,8 +44,7 @@ _logger = logging.getLogger(__name__)
class ProgramFunction(object):
- """Class providing a function to add to a GLProgram shaders.
- """
+ """Class providing a function to add to a GLProgram shaders."""
def setupProgram(self, context, program):
"""Sets-up uniforms of a program using this shader function.
@@ -66,6 +62,7 @@ class Fog(event.Notifier, ProgramFunction):
The background of the viewport is used as fog color,
otherwise it defaults to white.
"""
+
# TODO: add more controls (set fog range), add more fog modes
_fragDecl = """
@@ -123,26 +120,29 @@ class Fog(event.Notifier, ProgramFunction):
"""
# Provide scene z extent in camera coords
bounds = viewport.camera.extrinsic.transformBounds(
- viewport.scene.bounds(transformed=True, dataBounds=True))
+ viewport.scene.bounds(transformed=True, dataBounds=True)
+ )
return bounds[:, 2]
def setupProgram(self, context, program):
if not self.isOn:
return
- far, near = context.cache(key='zExtentCamera',
- factory=self._zExtentCamera,
- viewport=context.viewport)
+ far, near = context.cache(
+ key="zExtentCamera", factory=self._zExtentCamera, viewport=context.viewport
+ )
extent = far - near
- gl.glUniform2f(program.uniforms['fogExtentInfo'],
- 0.9/extent if extent != 0. else 0.,
- near)
+ gl.glUniform2f(
+ program.uniforms["fogExtentInfo"],
+ 0.9 / extent if extent != 0.0 else 0.0,
+ near,
+ )
# Use background color as fog color
bgColor = context.viewport.background
if bgColor is None:
- bgColor = 1., 1., 1.
- gl.glUniform3f(program.uniforms['fogColor'], *bgColor[:3])
+ bgColor = 1.0, 1.0, 1.0
+ gl.glUniform3f(program.uniforms["fogColor"], *bgColor[:3])
class ClippingPlane(ProgramFunction):
@@ -186,7 +186,7 @@ class ClippingPlane(ProgramFunction):
void clipping(vec4 position) {}
"""
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 0.)):
+ def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 0.0)):
self._plane = utils.Plane(point, normal)
@property
@@ -212,7 +212,7 @@ class ClippingPlane(ProgramFunction):
It MUST be in use and using this function.
"""
if self.plane.isPlane:
- gl.glUniform4f(program.uniforms['planeEq'], *self.plane.parameters)
+ gl.glUniform4f(program.uniforms["planeEq"], *self.plane.parameters)
class DirectionalLight(event.Notifier, ProgramFunction):
@@ -282,9 +282,14 @@ class DirectionalLight(event.Notifier, ProgramFunction):
}
"""
- def __init__(self, direction=None,
- ambient=(1., 1., 1.), diffuse=(0., 0., 0.),
- specular=(1., 1., 1.), shininess=0):
+ def __init__(
+ self,
+ direction=None,
+ ambient=(1.0, 1.0, 1.0),
+ diffuse=(0.0, 0.0, 0.0),
+ specular=(1.0, 1.0, 1.0),
+ shininess=0,
+ ):
super(DirectionalLight, self).__init__()
self._direction = None
self.direction = direction # Set _direction
@@ -294,10 +299,10 @@ class DirectionalLight(event.Notifier, ProgramFunction):
self._specular = specular
self._shininess = shininess
- ambient = event.notifyProperty('_ambient')
- diffuse = event.notifyProperty('_diffuse')
- specular = event.notifyProperty('_specular')
- shininess = event.notifyProperty('_shininess')
+ ambient = event.notifyProperty("_ambient")
+ diffuse = event.notifyProperty("_diffuse")
+ specular = event.notifyProperty("_specular")
+ shininess = event.notifyProperty("_shininess")
@property
def isOn(self):
@@ -362,28 +367,29 @@ class DirectionalLight(event.Notifier, ProgramFunction):
if self.isOn and self._direction is not None:
# Transform light direction from camera space to object coords
lightdir = context.objectToCamera.transformDir(
- self._direction, direct=False)
+ self._direction, direct=False
+ )
lightdir /= numpy.linalg.norm(lightdir)
- gl.glUniform3f(program.uniforms['dLight.lightDir'], *lightdir)
+ gl.glUniform3f(program.uniforms["dLight.lightDir"], *lightdir)
# Convert view position to object coords
viewpos = context.objectToCamera.transformPoint(
- numpy.array((0., 0., 0., 1.), dtype=numpy.float32),
+ numpy.array((0.0, 0.0, 0.0, 1.0), dtype=numpy.float32),
direct=False,
- perspectiveDivide=True)[:3]
- gl.glUniform3f(program.uniforms['dLight.viewPos'], *viewpos)
+ perspectiveDivide=True,
+ )[:3]
+ gl.glUniform3f(program.uniforms["dLight.viewPos"], *viewpos)
- gl.glUniform3f(program.uniforms['dLight.ambient'], *self.ambient)
- gl.glUniform3f(program.uniforms['dLight.diffuse'], *self.diffuse)
- gl.glUniform3f(program.uniforms['dLight.specular'], *self.specular)
- gl.glUniform1f(program.uniforms['dLight.shininess'],
- self.shininess)
+ gl.glUniform3f(program.uniforms["dLight.ambient"], *self.ambient)
+ gl.glUniform3f(program.uniforms["dLight.diffuse"], *self.diffuse)
+ gl.glUniform3f(program.uniforms["dLight.specular"], *self.specular)
+ gl.glUniform1f(program.uniforms["dLight.shininess"], self.shininess)
class Colormap(event.Notifier, ProgramFunction):
-
- _declTemplate = string.Template("""
+ _declTemplate = string.Template(
+ """
uniform sampler2D cmap_texture;
uniform int cmap_normalization;
uniform float cmap_parameter;
@@ -432,7 +438,8 @@ class Colormap(event.Notifier, ProgramFunction):
}
return color;
}
- """)
+ """
+ )
_discardCode = """
if (value == 0.) {
@@ -442,13 +449,13 @@ class Colormap(event.Notifier, ProgramFunction):
call = "colormap"
- NORMS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh'
+ NORMS = "linear", "log", "sqrt", "gamma", "arcsinh"
"""Tuple of supported normalizations."""
_COLORMAP_TEXTURE_UNIT = 1
"""Texture unit to use for storing the colormap"""
- def __init__(self, colormap=None, norm='linear', gamma=0., range_=(1., 10.)):
+ def __init__(self, colormap=None, norm="linear", gamma=0.0, range_=(1.0, 10.0)):
"""Shader function to apply a colormap to a value.
:param colormap: RGB(A) color look-up table (default: gray)
@@ -462,11 +469,11 @@ class Colormap(event.Notifier, ProgramFunction):
# Init privates to default
self._colormap = None
- self._norm = 'linear'
- self._gamma = -1.
- self._range = 1., 10.
+ self._norm = "linear"
+ self._gamma = -1.0
+ self._range = 1.0, 10.0
self._displayValuesBelowMin = True
- self._nancolor = numpy.array((1., 1., 1., 0.), dtype=numpy.float32)
+ self._nancolor = numpy.array((1.0, 1.0, 1.0, 0.0), dtype=numpy.float32)
self._texture = None
self._textureToDiscard = None
@@ -474,8 +481,7 @@ class Colormap(event.Notifier, ProgramFunction):
if colormap is None:
# default colormap
colormap = numpy.empty((256, 3), dtype=numpy.uint8)
- colormap[:] = numpy.arange(256,
- dtype=numpy.uint8)[:, numpy.newaxis]
+ colormap[:] = numpy.arange(256, dtype=numpy.uint8)[:, numpy.newaxis]
# Set to values through properties to perform asserts and updates
self.colormap = colormap
@@ -487,7 +493,8 @@ class Colormap(event.Notifier, ProgramFunction):
def decl(self):
"""Source code of the function declaration"""
return self._declTemplate.substitute(
- discard="" if self.displayValuesBelowMin else self._discardCode)
+ discard="" if self.displayValuesBelowMin else self._discardCode
+ )
@property
def colormap(self):
@@ -506,17 +513,21 @@ class Colormap(event.Notifier, ProgramFunction):
data = numpy.empty(
(16, self._colormap.shape[0], self._colormap.shape[1]),
- dtype=self._colormap.dtype)
+ dtype=self._colormap.dtype,
+ )
data[:] = self._colormap
format_ = gl.GL_RGBA if data.shape[-1] == 4 else gl.GL_RGB
self._texture = _glutils.Texture(
- format_, data, format_,
+ format_,
+ data,
+ format_,
texUnit=self._COLORMAP_TEXTURE_UNIT,
minFilter=gl.GL_NEAREST,
magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
self.notify()
@@ -527,7 +538,7 @@ class Colormap(event.Notifier, ProgramFunction):
@nancolor.setter
def nancolor(self, color):
- color = numpy.clip(numpy.array(color, dtype=numpy.float32), 0., 1.)
+ color = numpy.clip(numpy.array(color, dtype=numpy.float32), 0.0, 1.0)
assert color.ndim == 1
assert len(color) == 4
if not numpy.array_equal(self._nancolor, color):
@@ -548,7 +559,7 @@ class Colormap(event.Notifier, ProgramFunction):
if norm != self._norm:
assert norm in self.NORMS
self._norm = norm
- if norm in ('log', 'sqrt'):
+ if norm in ("log", "sqrt"):
self.range_ = self.range_ # To test for positive range_
self.notify()
@@ -560,7 +571,7 @@ class Colormap(event.Notifier, ProgramFunction):
@gamma.setter
def gamma(self, gamma):
if gamma != self._gamma:
- assert gamma >= 0.
+ assert gamma >= 0.0
self._gamma = gamma
self.notify()
@@ -580,15 +591,13 @@ class Colormap(event.Notifier, ProgramFunction):
assert len(range_) == 2
range_ = float(range_[0]), float(range_[1])
- if self.norm == 'log' and (range_[0] <= 0. or range_[1] <= 0.):
- _logger.warning(
- "Log normalization and negative range: updating range.")
+ if self.norm == "log" and (range_[0] <= 0.0 or range_[1] <= 0.0):
+ _logger.warning("Log normalization and negative range: updating range.")
minPos = numpy.finfo(numpy.float32).tiny
range_ = max(range_[0], minPos), max(range_[1], minPos)
- elif self.norm == 'sqrt' and (range_[0] < 0. or range_[1] < 0.):
- _logger.warning(
- "Sqrt normalization and negative range: updating range.")
- range_ = max(range_[0], 0.), max(range_[1], 0.)
+ elif self.norm == "sqrt" and (range_[0] < 0.0 or range_[1] < 0.0):
+ _logger.warning("Sqrt normalization and negative range: updating range.")
+ range_ = max(range_[0], 0.0), max(range_[1], 0.0)
if range_ != self._range:
self._range = range_
@@ -596,8 +605,7 @@ class Colormap(event.Notifier, ProgramFunction):
@property
def displayValuesBelowMin(self):
- """True to display values below colormap min, False to discard them.
- """
+ """True to display values below colormap min, False to discard them."""
return self._displayValuesBelowMin
@displayValuesBelowMin.setter
@@ -618,33 +626,34 @@ class Colormap(event.Notifier, ProgramFunction):
self._texture.bind()
- gl.glUniform1i(program.uniforms['cmap_texture'],
- self._texture.texUnit)
+ gl.glUniform1i(program.uniforms["cmap_texture"], self._texture.texUnit)
min_, max_ = self.range_
- param = 0.
- if self._norm == 'log':
+ param = 0.0
+ if self._norm == "log":
min_, max_ = numpy.log10(min_), numpy.log10(max_)
normID = 1
- elif self._norm == 'sqrt':
+ elif self._norm == "sqrt":
min_, max_ = numpy.sqrt(min_), numpy.sqrt(max_)
normID = 2
- elif self._norm == 'gamma':
+ elif self._norm == "gamma":
# Keep min_, max_ as is
param = self._gamma
normID = 3
- elif self._norm == 'arcsinh':
+ elif self._norm == "arcsinh":
min_, max_ = numpy.arcsinh(min_), numpy.arcsinh(max_)
normID = 4
else: # Linear
normID = 0
- gl.glUniform1i(program.uniforms['cmap_normalization'], normID)
- gl.glUniform1f(program.uniforms['cmap_parameter'], param)
- gl.glUniform1f(program.uniforms['cmap_min'], min_)
- gl.glUniform1f(program.uniforms['cmap_oneOverRange'],
- (1. / (max_ - min_)) if max_ != min_ else 0.)
- gl.glUniform4f(program.uniforms['nancolor'], *self._nancolor)
+ gl.glUniform1i(program.uniforms["cmap_normalization"], normID)
+ gl.glUniform1f(program.uniforms["cmap_parameter"], param)
+ gl.glUniform1f(program.uniforms["cmap_min"], min_)
+ gl.glUniform1f(
+ program.uniforms["cmap_oneOverRange"],
+ (1.0 / (max_ - min_)) if max_ != min_ else 0.0,
+ )
+ gl.glUniform4f(program.uniforms["nancolor"], *self._nancolor)
def prepareGL2(self, context):
if self._textureToDiscard is not None:
diff --git a/src/silx/gui/plot3d/scene/interaction.py b/src/silx/gui/plot3d/scene/interaction.py
index 14a54dc..debf670 100644
--- a/src/silx/gui/plot3d/scene/interaction.py
+++ b/src/silx/gui/plot3d/scene/interaction.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module provides interaction to plug on the scene graph."""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "25/07/2016"
@@ -34,8 +31,12 @@ import logging
import numpy
from silx.gui import qt
-from silx.gui.plot.Interaction import \
- StateMachine, State, LEFT_BTN, RIGHT_BTN # , MIDDLE_BTN
+from silx.gui.plot.Interaction import (
+ StateMachine,
+ State,
+ LEFT_BTN,
+ RIGHT_BTN,
+) # , MIDDLE_BTN
from . import transform
@@ -44,35 +45,32 @@ _logger = logging.getLogger(__name__)
class ClickOrDrag(StateMachine):
- """Click or drag interaction for a given button.
+ """Click or drag interaction for a given button."""
- """
- #TODO: merge this class with silx.gui.plot.Interaction.ClickOrDrag
+ # TODO: merge this class with silx.gui.plot.Interaction.ClickOrDrag
- DRAG_THRESHOLD_SQUARE_DIST = 5 ** 2
+ DRAG_THRESHOLD_SQUARE_DIST = 5**2
class Idle(State):
def onPress(self, x, y, btn):
if btn == self.machine.button:
- self.goto('clickOrDrag', x, y)
+ self.goto("clickOrDrag", x, y)
return True
class ClickOrDrag(State):
def enterState(self, x, y):
self.initPos = x, y
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
def onMove(self, x, y):
dx = (x - self.initPos[0]) ** 2
dy = (y - self.initPos[1]) ** 2
- if (dx ** 2 + dy ** 2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('drag', self.initPos, (x, y))
+ if (dx**2 + dy**2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
+ self.goto("drag", self.initPos, (x, y))
def onRelease(self, x, y, btn):
if btn == self.machine.button:
self.machine.click(x, y)
- self.goto('idle')
+ self.goto("idle")
class Drag(State):
def enterState(self, initPos, curPos):
@@ -80,24 +78,22 @@ class ClickOrDrag(StateMachine):
self.machine.beginDrag(*initPos)
self.machine.drag(*curPos)
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
def onMove(self, x, y):
self.machine.drag(x, y)
def onRelease(self, x, y, btn):
if btn == self.machine.button:
self.machine.endDrag(self.initPos, (x, y))
- self.goto('idle')
+ self.goto("idle")
def __init__(self, button=LEFT_BTN):
self.button = button
states = {
- 'idle': ClickOrDrag.Idle,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
+ "idle": ClickOrDrag.Idle,
+ "clickOrDrag": ClickOrDrag.ClickOrDrag,
+ "drag": ClickOrDrag.Drag,
}
- super(ClickOrDrag, self).__init__(states, 'idle')
+ super(ClickOrDrag, self).__init__(states, "idle")
def click(self, x, y):
"""Called upon a left or right button click.
@@ -129,8 +125,9 @@ class ClickOrDrag(StateMachine):
class CameraSelectRotate(ClickOrDrag):
"""Camera rotation using an arcball-like interaction."""
- def __init__(self, viewport, orbitAroundCenter=True, button=RIGHT_BTN,
- selectCB=None):
+ def __init__(
+ self, viewport, orbitAroundCenter=True, button=RIGHT_BTN, selectCB=None
+ ):
self._viewport = viewport
self._orbitAroundCenter = orbitAroundCenter
self._selectCB = selectCB
@@ -147,7 +144,7 @@ class CameraSelectRotate(ClickOrDrag):
position = self._viewport._getXZYGL(x, y)
# This assume no object lie on the far plane
# Alternative, change the depth range so that far is < 1
- if ndcZ != 1. and position is not None:
+ if ndcZ != 1.0 and position is not None:
self._selectCB((x, y, ndcZ), position)
def beginDrag(self, x, y):
@@ -155,7 +152,7 @@ class CameraSelectRotate(ClickOrDrag):
if not self._orbitAroundCenter:
# Try to use picked object position as center of rotation
ndcZ = self._viewport._pickNdcZGL(x, y)
- if ndcZ != 1.:
+ if ndcZ != 1.0:
# Hit an object, use picked point as center
centerPos = self._viewport._getXZYGL(x, y) # Can return None
@@ -180,12 +177,11 @@ class CameraSelectRotate(ClickOrDrag):
position = self._startExtrinsic.position
else:
minsize = min(self._viewport.size)
- distance = numpy.sqrt(dx ** 2 + dy ** 2)
+ distance = numpy.sqrt(dx**2 + dy**2)
angle = distance / minsize * numpy.pi
# Take care of y inversion
- direction = dx * self._startExtrinsic.side - \
- dy * self._startExtrinsic.up
+ direction = dx * self._startExtrinsic.side - dy * self._startExtrinsic.up
direction /= numpy.linalg.norm(direction)
axis = numpy.cross(direction, self._startExtrinsic.direction)
axis /= numpy.linalg.norm(axis)
@@ -197,10 +193,9 @@ class CameraSelectRotate(ClickOrDrag):
up = rotation.transformDir(self._startExtrinsic.up)
# Rotate position around center
- trlist = transform.StaticTransformList((
- self._center,
- rotation,
- self._center.inverse()))
+ trlist = transform.StaticTransformList(
+ (self._center, rotation, self._center.inverse())
+ )
position = trlist.transformPoint(self._startExtrinsic.position)
camerapos = self._viewport.camera.extrinsic
@@ -226,7 +221,7 @@ class CameraSelectPan(ClickOrDrag):
position = self._viewport._getXZYGL(x, y)
# This assume no object lie on the far plane
# Alternative, change the depth range so that far is < 1
- if ndcZ != 1. and position is not None:
+ if ndcZ != 1.0 and position is not None:
self._selectCB((x, y, ndcZ), position)
def beginDrag(self, x, y):
@@ -234,8 +229,9 @@ class CameraSelectPan(ClickOrDrag):
ndcZ = self._viewport._pickNdcZGL(x, y)
# ndcZ is the panning plane
if ndc is not None and ndcZ is not None:
- self._lastPosNdc = numpy.array((ndc[0], ndc[1], ndcZ, 1.),
- dtype=numpy.float32)
+ self._lastPosNdc = numpy.array(
+ (ndc[0], ndc[1], ndcZ, 1.0), dtype=numpy.float32
+ )
else:
self._lastPosNdc = None
@@ -243,14 +239,17 @@ class CameraSelectPan(ClickOrDrag):
if self._lastPosNdc is not None:
ndc = self._viewport.windowToNdc(x, y)
if ndc is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], self._lastPosNdc[2], 1.),
- dtype=numpy.float32)
+ ndcPos = numpy.array(
+ (ndc[0], ndc[1], self._lastPosNdc[2], 1.0), dtype=numpy.float32
+ )
# Convert last and current NDC positions to scene coords
scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
+ ndcPos, direct=False, perspectiveDivide=True
+ )
lastScenePos = self._viewport.camera.transformPoint(
- self._lastPosNdc, direct=False, perspectiveDivide=True)
+ self._lastPosNdc, direct=False, perspectiveDivide=True
+ )
# Get translation in scene coords
translation = scenePos[:3] - lastScenePos[:3]
@@ -267,21 +266,21 @@ class CameraWheel(object):
"""StateMachine like class, just handling wheel events."""
# TODO choose scale of motion? Translation or Scale?
- def __init__(self, viewport, mode='center', scaleTransform=None):
- assert mode in ('center', 'position', 'scale')
+ def __init__(self, viewport, mode="center", scaleTransform=None):
+ assert mode in ("center", "position", "scale")
self._viewport = viewport
- if mode == 'center':
+ if mode == "center":
self._zoomTo = self._zoomToCenter
- elif mode == 'position':
+ elif mode == "position":
self._zoomTo = self._zoomToPosition
- elif mode == 'scale':
+ elif mode == "scale":
self._zoomTo = self._zoomByScale
self._scale = scaleTransform
else:
- raise ValueError('Unsupported mode: %s' % mode)
+ raise ValueError("Unsupported mode: %s" % mode)
def handleEvent(self, eventName, *args, **kwargs):
- if eventName == 'wheel':
+ if eventName == "wheel":
return self._zoomTo(*args, **kwargs)
def _zoomToCenter(self, x, y, angleInDegrees):
@@ -289,7 +288,7 @@ class CameraWheel(object):
Only works with perspective camera.
"""
- direction = 'forward' if angleInDegrees > 0 else 'backward'
+ direction = "forward" if angleInDegrees > 0 else "backward"
self._viewport.camera.move(direction)
return True
@@ -300,20 +299,22 @@ class CameraWheel(object):
"""
ndc = self._viewport.windowToNdc(x, y)
if ndc is not None:
- near = numpy.array((ndc[0], ndc[1], -1., 1.), dtype=numpy.float32)
+ near = numpy.array((ndc[0], ndc[1], -1.0, 1.0), dtype=numpy.float32)
nearscene = self._viewport.camera.transformPoint(
- near, direct=False, perspectiveDivide=True)
+ near, direct=False, perspectiveDivide=True
+ )
- far = numpy.array((ndc[0], ndc[1], 1., 1.), dtype=numpy.float32)
+ far = numpy.array((ndc[0], ndc[1], 1.0, 1.0), dtype=numpy.float32)
farscene = self._viewport.camera.transformPoint(
- far, direct=False, perspectiveDivide=True)
+ far, direct=False, perspectiveDivide=True
+ )
dirscene = farscene[:3] - nearscene[:3]
dirscene /= numpy.linalg.norm(dirscene)
if angleInDegrees < 0:
- dirscene *= -1.
+ dirscene *= -1.0
# TODO which scale
self._viewport.camera.extrinsic.position += dirscene
@@ -330,43 +331,43 @@ class CameraWheel(object):
if ndc is not None:
ndcz = self._viewport._pickNdcZGL(x, y)
- position = numpy.array((ndc[0], ndc[1], ndcz),
- dtype=numpy.float32)
+ position = numpy.array((ndc[0], ndc[1], ndcz), dtype=numpy.float32)
positionscene = self._viewport.camera.transformPoint(
- position, direct=False, perspectiveDivide=True)
+ position, direct=False, perspectiveDivide=True
+ )
camtopos = extrinsic.position - positionscene
- step = 0.2 * (1. if angleInDegrees < 0 else -1.)
+ step = 0.2 * (1.0 if angleInDegrees < 0 else -1.0)
extrinsic.position += step * camtopos
elif isinstance(projection, transform.Orthographic):
# For orthographic projection, change projection borders
ndcx, ndcy = self._viewport.windowToNdc(x, y, checkInside=False)
- step = 0.2 * (1. if angleInDegrees < 0 else -1.)
+ step = 0.2 * (1.0 if angleInDegrees < 0 else -1.0)
- dx = (ndcx + 1) / 2.
+ dx = (ndcx + 1) / 2.0
stepwidth = step * (projection.right - projection.left)
left = projection.left - dx * stepwidth
- right = projection.right + (1. - dx) * stepwidth
+ right = projection.right + (1.0 - dx) * stepwidth
- dy = (ndcy + 1) / 2.
+ dy = (ndcy + 1) / 2.0
stepheight = step * (projection.top - projection.bottom)
bottom = projection.bottom - dy * stepheight
- top = projection.top + (1. - dy) * stepheight
+ top = projection.top + (1.0 - dy) * stepheight
projection.setClipping(left, right, bottom, top)
else:
- raise RuntimeError('Unsupported camera', projection)
+ raise RuntimeError("Unsupported camera", projection)
return True
def _zoomByScale(self, x, y, angleInDegrees):
"""Zoom by scaling scene (do not keep pixel under mouse invariant)."""
scalefactor = 1.1
- if angleInDegrees < 0.:
- scalefactor = 1. / scalefactor
+ if angleInDegrees < 0.0:
+ scalefactor = 1.0 / scalefactor
self._scale.scale = scalefactor * self._scale.scale
self._viewport.adjustCameraDepthExtent()
@@ -379,12 +380,13 @@ class FocusManager(StateMachine):
On press an event handler can acquire focus.
By default it looses focus when all buttons are released.
"""
+
class Idle(State):
def onPress(self, x, y, btn):
for eventHandler in self.machine.currentEventHandler:
- requestFocus = eventHandler.handleEvent('press', x, y, btn)
+ requestFocus = eventHandler.handleEvent("press", x, y, btn)
if requestFocus:
- self.goto('focus', eventHandler, btn)
+ self.goto("focus", eventHandler, btn)
break
def _processEvent(self, *args):
@@ -394,47 +396,42 @@ class FocusManager(StateMachine):
break
def onMove(self, x, y):
- self._processEvent('move', x, y)
+ self._processEvent("move", x, y)
def onRelease(self, x, y, btn):
- self._processEvent('release', x, y, btn)
+ self._processEvent("release", x, y, btn)
def onWheel(self, x, y, angle):
- self._processEvent('wheel', x, y, angle)
+ self._processEvent("wheel", x, y, angle)
class Focus(State):
def enterState(self, eventHandler, btn):
self.eventHandler = eventHandler
self.focusBtns = {btn} # Set
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
def onPress(self, x, y, btn):
self.focusBtns.add(btn)
- self.eventHandler.handleEvent('press', x, y, btn)
+ self.eventHandler.handleEvent("press", x, y, btn)
def onMove(self, x, y):
- self.eventHandler.handleEvent('move', x, y)
+ self.eventHandler.handleEvent("move", x, y)
def onRelease(self, x, y, btn):
self.focusBtns.discard(btn)
- requestfocus = self.eventHandler.handleEvent('release', x, y, btn)
+ requestfocus = self.eventHandler.handleEvent("release", x, y, btn)
if len(self.focusBtns) == 0 and not requestfocus:
- self.goto('idle')
+ self.goto("idle")
def onWheel(self, x, y, angleInDegrees):
- self.eventHandler.handleEvent('wheel', x, y, angleInDegrees)
+ self.eventHandler.handleEvent("wheel", x, y, angleInDegrees)
def __init__(self, eventHandlers=(), ctrlEventHandlers=None):
self.defaultEventHandlers = eventHandlers
self.ctrlEventHandlers = ctrlEventHandlers
self.currentEventHandler = self.defaultEventHandlers
- states = {
- 'idle': FocusManager.Idle,
- 'focus': FocusManager.Focus
- }
- super(FocusManager, self).__init__(states, 'idle')
+ states = {"idle": FocusManager.Idle, "focus": FocusManager.Focus}
+ super(FocusManager, self).__init__(states, "idle")
def onKeyPress(self, key):
if key == qt.Qt.Key_Control and self.ctrlEventHandlers is not None:
@@ -453,43 +450,65 @@ class RotateCameraControl(FocusManager):
"""Combine wheel and rotate state machine for left button
and pan when ctrl is pressed
"""
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN, selectCB))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB))
+
+ def __init__(
+ self,
+ viewport,
+ orbitAroundCenter=False,
+ mode="center",
+ scaleTransform=None,
+ selectCB=None,
+ ):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectRotate(viewport, orbitAroundCenter, LEFT_BTN, selectCB),
+ )
+ ctrlHandlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectPan(viewport, LEFT_BTN, selectCB),
+ )
super(RotateCameraControl, self).__init__(handlers, ctrlHandlers)
class PanCameraControl(FocusManager):
"""Combine wheel, selectPan and rotate state machine for left button
and rotate when ctrl is pressed"""
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN, selectCB))
+
+ def __init__(
+ self,
+ viewport,
+ orbitAroundCenter=False,
+ mode="center",
+ scaleTransform=None,
+ selectCB=None,
+ ):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectPan(viewport, LEFT_BTN, selectCB),
+ )
+ ctrlHandlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectRotate(viewport, orbitAroundCenter, LEFT_BTN, selectCB),
+ )
super(PanCameraControl, self).__init__(handlers, ctrlHandlers)
class CameraControl(FocusManager):
"""Combine wheel, selectPan and rotate state machine."""
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB),
- CameraSelectRotate(
- viewport, orbitAroundCenter, RIGHT_BTN, selectCB))
+
+ def __init__(
+ self,
+ viewport,
+ orbitAroundCenter=False,
+ mode="center",
+ scaleTransform=None,
+ selectCB=None,
+ ):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectPan(viewport, LEFT_BTN, selectCB),
+ CameraSelectRotate(viewport, orbitAroundCenter, RIGHT_BTN, selectCB),
+ )
super(CameraControl, self).__init__(handlers)
@@ -535,14 +554,14 @@ class PlaneRotate(ClickOrDrag):
# Normalize x and y on a unit circle
spherecoords = (position - center) / float(radius)
- squarelength = numpy.sum(spherecoords ** 2)
+ squarelength = numpy.sum(spherecoords**2)
# Project on the unit sphere and compute z coordinates
if squarelength > 1.0: # Outside sphere: project
spherecoords /= numpy.sqrt(squarelength)
zsphere = 0.0
else: # In sphere: compute z
- zsphere = numpy.sqrt(1. - squarelength)
+ zsphere = numpy.sqrt(1.0 - squarelength)
spherecoords = numpy.append(spherecoords, zsphere)
return spherecoords
@@ -555,8 +574,7 @@ class PlaneRotate(ClickOrDrag):
# Store the plane normal
self._beginNormal = self._plane.plane.normal
- _logger.debug(
- 'Begin arcball, plane center %s', str(self._plane.center))
+ _logger.debug("Begin arcball, plane center %s", str(self._plane.center))
# Do the arcball on the screen
radius = min(self._viewport.size)
@@ -565,12 +583,15 @@ class PlaneRotate(ClickOrDrag):
else:
center = self._plane.objectToNDCTransform.transformPoint(
- self._plane.center, perspectiveDivide=True)
+ self._plane.center, perspectiveDivide=True
+ )
self._beginCenter = self._viewport.ndcToWindow(
- center[0], center[1], checkInside=False)
+ center[0], center[1], checkInside=False
+ )
self._startVector = self._sphereUnitVector(
- radius, self._beginCenter, (x, y))
+ radius, self._beginCenter, (x, y)
+ )
def drag(self, x, y):
if self._beginCenter is None:
@@ -578,24 +599,21 @@ class PlaneRotate(ClickOrDrag):
# Compute rotation: this is twice the rotation of the arcball
radius = min(self._viewport.size)
- currentvector = self._sphereUnitVector(
- radius, self._beginCenter, (x, y))
+ currentvector = self._sphereUnitVector(radius, self._beginCenter, (x, y))
crossprod = numpy.cross(self._startVector, currentvector)
dotprod = numpy.dot(self._startVector, currentvector)
quaternion = numpy.append(crossprod, dotprod)
# Rotation was computed with Y downward, but apply in NDC, invert Y
- quaternion[1] *= -1.
+ quaternion[1] *= -1.0
rotation = transform.Rotate()
rotation.quaternion = quaternion
# Convert to NDC, rotate, convert back to object
- normal = self._plane.objectToNDCTransform.transformNormal(
- self._beginNormal)
+ normal = self._plane.objectToNDCTransform.transformNormal(self._beginNormal)
normal = rotation.transformNormal(normal)
- normal = self._plane.objectToNDCTransform.transformNormal(
- normal, direct=False)
+ normal = self._plane.objectToNDCTransform.transformNormal(normal, direct=False)
self._plane.plane.normal = normal
def endDrag(self, x, y):
@@ -610,7 +628,7 @@ class PlanePan(ClickOrDrag):
self._viewport = viewport
self._beginPlanePoint = None
self._beginPos = None
- self._dragNdcZ = 0.
+ self._dragNdcZ = 0.0
super(PlanePan, self).__init__(button)
def click(self, x, y):
@@ -621,16 +639,17 @@ class PlanePan(ClickOrDrag):
ndcZ = self._viewport._pickNdcZGL(x, y)
# ndcZ is the panning plane
if ndc is not None and ndcZ is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], ndcZ, 1.),
- dtype=numpy.float32)
+ ndcPos = numpy.array((ndc[0], ndc[1], ndcZ, 1.0), dtype=numpy.float32)
scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
+ ndcPos, direct=False, perspectiveDivide=True
+ )
self._beginPos = self._plane.objectToSceneTransform.transformPoint(
- scenePos, direct=False)
+ scenePos, direct=False
+ )
self._dragNdcZ = ndcZ
else:
self._beginPos = None
- self._dragNdcZ = 0.
+ self._dragNdcZ = 0.0
self._beginPlanePoint = self._plane.plane.point
@@ -638,14 +657,17 @@ class PlanePan(ClickOrDrag):
if self._beginPos is not None:
ndc = self._viewport.windowToNdc(x, y)
if ndc is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], self._dragNdcZ, 1.),
- dtype=numpy.float32)
+ ndcPos = numpy.array(
+ (ndc[0], ndc[1], self._dragNdcZ, 1.0), dtype=numpy.float32
+ )
# Convert last and current NDC positions to scene coords
scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
+ ndcPos, direct=False, perspectiveDivide=True
+ )
curPos = self._plane.objectToSceneTransform.transformPoint(
- scenePos, direct=False)
+ scenePos, direct=False
+ )
# Get translation in scene coords
translation = curPos[:3] - self._beginPos[:3]
@@ -655,8 +677,7 @@ class PlanePan(ClickOrDrag):
# Keep plane point in bounds
bounds = self._plane.parent.bounds(dataBounds=True)
if bounds is not None:
- newPoint = numpy.clip(
- newPoint, a_min=bounds[0], a_max=bounds[1])
+ newPoint = numpy.clip(newPoint, a_min=bounds[0], a_max=bounds[1])
# Only update plane if it is in some bounds
self._plane.plane.point = newPoint
@@ -667,35 +688,45 @@ class PlanePan(ClickOrDrag):
class PlaneControl(FocusManager):
"""Combine wheel, selectPan and rotate state machine for plane control."""
- def __init__(self, viewport, plane,
- mode='center', scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN),
- PlaneRotate(viewport, plane, RIGHT_BTN))
+
+ def __init__(self, viewport, plane, mode="center", scaleTransform=None):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ PlanePan(viewport, plane, LEFT_BTN),
+ PlaneRotate(viewport, plane, RIGHT_BTN),
+ )
super(PlaneControl, self).__init__(handlers)
class PanPlaneRotateCameraControl(FocusManager):
"""Combine wheel, pan plane and camera rotate state machine."""
- def __init__(self, viewport, plane,
- mode='center', scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN),
- CameraSelectRotate(viewport,
- orbitAroundCenter=False,
- button=RIGHT_BTN))
+
+ def __init__(self, viewport, plane, mode="center", scaleTransform=None):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ PlanePan(viewport, plane, LEFT_BTN),
+ CameraSelectRotate(viewport, orbitAroundCenter=False, button=RIGHT_BTN),
+ )
super(PanPlaneRotateCameraControl, self).__init__(handlers)
class PanPlaneZoomOnWheelControl(FocusManager):
"""Combine zoom on wheel and pan plane state machines."""
- def __init__(self, viewport, plane,
- mode='center',
- orbitAroundCenter=False,
- scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN))
+
+ def __init__(
+ self,
+ viewport,
+ plane,
+ mode="center",
+ orbitAroundCenter=False,
+ scaleTransform=None,
+ ):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ PlanePan(viewport, plane, LEFT_BTN),
+ )
+ ctrlHandlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectRotate(viewport, orbitAroundCenter, LEFT_BTN),
+ )
super(PanPlaneZoomOnWheelControl, self).__init__(handlers, ctrlHandlers)
diff --git a/src/silx/gui/plot3d/scene/primitives.py b/src/silx/gui/plot3d/scene/primitives.py
index 7f35c3c..93070c3 100644
--- a/src/silx/gui/plot3d/scene/primitives.py
+++ b/src/silx/gui/plot3d/scene/primitives.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 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
@@ -23,16 +22,11 @@
#
# ###########################################################################*/
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import ctypes
from functools import reduce
import logging
@@ -56,6 +50,7 @@ _logger = logging.getLogger(__name__)
# Geometry ####################################################################
+
class Geometry(core.Elem):
"""Set of vertices with normals and colors.
@@ -68,39 +63,36 @@ class Geometry(core.Elem):
"""
_ATTR_INFO = {
- 'position': {'dims': (1, 2), 'lastDim': (2, 3, 4)},
- 'normal': {'dims': (1, 2), 'lastDim': (3,)},
- 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
+ "position": {"dims": (1, 2), "lastDim": (2, 3, 4)},
+ "normal": {"dims": (1, 2), "lastDim": (3,)},
+ "color": {"dims": (1, 2), "lastDim": (3, 4)},
}
_MODE_CHECKS = { # Min, Modulo
- 'lines': (2, 2), 'line_strip': (2, 0), 'loop': (2, 0),
- 'points': (1, 0),
- 'triangles': (3, 3), 'triangle_strip': (3, 0), 'fan': (3, 0)
+ "lines": (2, 2),
+ "line_strip": (2, 0),
+ "loop": (2, 0),
+ "points": (1, 0),
+ "triangles": (3, 3),
+ "triangle_strip": (3, 0),
+ "fan": (3, 0),
}
_MODES = {
- 'lines': gl.GL_LINES,
- 'line_strip': gl.GL_LINE_STRIP,
- 'loop': gl.GL_LINE_LOOP,
-
- 'points': gl.GL_POINTS,
-
- 'triangles': gl.GL_TRIANGLES,
- 'triangle_strip': gl.GL_TRIANGLE_STRIP,
- 'fan': gl.GL_TRIANGLE_FAN
+ "lines": gl.GL_LINES,
+ "line_strip": gl.GL_LINE_STRIP,
+ "loop": gl.GL_LINE_LOOP,
+ "points": gl.GL_POINTS,
+ "triangles": gl.GL_TRIANGLES,
+ "triangle_strip": gl.GL_TRIANGLE_STRIP,
+ "fan": gl.GL_TRIANGLE_FAN,
}
- _LINE_MODES = 'lines', 'line_strip', 'loop'
+ _LINE_MODES = "lines", "line_strip", "loop"
- _TRIANGLE_MODES = 'triangles', 'triangle_strip', 'fan'
+ _TRIANGLE_MODES = "triangles", "triangle_strip", "fan"
- def __init__(self,
- mode,
- indices=None,
- copy=True,
- attrib0='position',
- **attributes):
+ def __init__(self, mode, indices=None, copy=True, attrib0="position", **attributes):
super(Geometry, self).__init__()
self._attrib0 = str(attrib0)
@@ -149,26 +141,26 @@ class Geometry(core.Elem):
"""
# Convert single value (int, float, numpy types) to tuple
if not isinstance(array, abc.Iterable):
- array = (array, )
+ array = (array,)
# Makes sure it is an array
array = numpy.array(array, copy=False)
dtype = None
- if array.dtype.kind == 'f' and array.dtype.itemsize != 4:
+ if array.dtype.kind == "f" and array.dtype.itemsize != 4:
# Cast to float32
- _logger.info('Cast array to float32')
+ _logger.info("Cast array to float32")
dtype = numpy.float32
elif array.dtype.itemsize > 4:
# Cast (u)int64 to (u)int32
- if array.dtype.kind == 'i':
- _logger.info('Cast array to int32')
+ if array.dtype.kind == "i":
+ _logger.info("Cast array to int32")
dtype = numpy.int32
- elif array.dtype.kind == 'u':
- _logger.info('Cast array to uint32')
+ elif array.dtype.kind == "u":
+ _logger.info("Cast array to uint32")
dtype = numpy.uint32
- return numpy.array(array, dtype=dtype, order='C', copy=copy)
+ return numpy.array(array, dtype=dtype, order="C", copy=copy)
@property
def nbVertices(self):
@@ -203,17 +195,16 @@ class Geometry(core.Elem):
array = self._glReadyArray(array, copy=copy)
if name not in self._ATTR_INFO:
- _logger.debug('Not checking attribute %s dimensions', name)
+ _logger.debug("Not checking attribute %s dimensions", name)
else:
checks = self._ATTR_INFO[name]
- if (array.ndim == 1 and checks['lastDim'] == (1,) and
- len(array) > 1):
+ if array.ndim == 1 and checks["lastDim"] == (1,) and len(array) > 1:
array = array.reshape((len(array), 1))
# Checks
- assert array.ndim in checks['dims'], "Attr %s" % name
- assert array.shape[-1] in checks['lastDim'], "Attr %s" % name
+ assert array.ndim in checks["dims"], "Attr %s" % name
+ assert array.shape[-1] in checks["lastDim"], "Attr %s" % name
# Makes sure attrib0 is considered as an array of values
if name == self.attrib0 and array.ndim == 1:
@@ -280,7 +271,8 @@ class Geometry(core.Elem):
assert len(array) in (1, 2, 3, 4)
gl.glDisableVertexAttribArray(attribute)
_glVertexAttribFunc = getattr(
- _glutils.gl, 'glVertexAttrib{}f'.format(len(array)))
+ _glutils.gl, "glVertexAttrib{}f".format(len(array))
+ )
_glVertexAttribFunc(attribute, *array)
else:
# TODO As is this is a never event, remove?
@@ -291,7 +283,8 @@ class Geometry(core.Elem):
_glutils.numpyToGLType(array.dtype),
gl.GL_FALSE,
0,
- array)
+ array,
+ )
def setIndices(self, indices, copy=True):
"""Set the primitive indices to use.
@@ -300,13 +293,13 @@ class Geometry(core.Elem):
:param bool copy: True (default) to copy the data, False to use as is
"""
# Trigger garbage collection of previous indices VBO if any
- self._vbos.pop('__indices__', None)
+ self._vbos.pop("__indices__", None)
if indices is None:
self._indices = None
else:
indices = self._glReadyArray(indices, copy=copy).ravel()
- assert indices.dtype.name in ('uint8', 'uint16', 'uint32')
+ assert indices.dtype.name in ("uint8", "uint16", "uint32")
if _logger.getEffectiveLevel() <= logging.DEBUG:
# This might be a costy check
assert indices.max() < self.nbVertices
@@ -367,19 +360,22 @@ class Geometry(core.Elem):
min_ = numpy.nanmin(attribute, axis=0)
max_ = numpy.nanmax(attribute, axis=0)
else:
- min_, max_ = numpy.zeros((2, attribute.shape[1]), dtype=numpy.float32)
+ min_, max_ = numpy.zeros(
+ (2, attribute.shape[1]), dtype=numpy.float32
+ )
- toCopy = min(len(min_), 3-index)
+ toCopy = min(len(min_), 3 - index)
if toCopy != len(min_):
- _logger.error("Attribute defining bounds"
- " has too many dimensions")
+ _logger.error(
+ "Attribute defining bounds" " has too many dimensions"
+ )
- self.__bounds[0, index:index+toCopy] = min_[:toCopy]
- self.__bounds[1, index:index+toCopy] = max_[:toCopy]
+ self.__bounds[0, index : index + toCopy] = min_[:toCopy]
+ self.__bounds[1, index : index + toCopy] = max_[:toCopy]
index += toCopy
- self.__bounds[numpy.isnan(self.__bounds)] = 0. # Avoid NaNs
+ self.__bounds[numpy.isnan(self.__bounds)] = 0.0 # Avoid NaNs
return self.__bounds.copy()
@@ -392,11 +388,13 @@ class Geometry(core.Elem):
self._vbos[name] = ctx.glCtx.makeVboAttrib(array)
self._unsyncAttributes = []
- if self._indices is not None and '__indices__' not in self._vbos:
- vbo = ctx.glCtx.makeVbo(self._indices,
- usage=gl.GL_STATIC_DRAW,
- target=gl.GL_ELEMENT_ARRAY_BUFFER)
- self._vbos['__indices__'] = vbo
+ if self._indices is not None and "__indices__" not in self._vbos:
+ vbo = ctx.glCtx.makeVbo(
+ self._indices,
+ usage=gl.GL_STATIC_DRAW,
+ target=gl.GL_ELEMENT_ARRAY_BUFFER,
+ )
+ self._vbos["__indices__"] = vbo
def _draw(self, program=None, nbVertices=None):
"""Perform OpenGL draw calls.
@@ -416,18 +414,23 @@ class Geometry(core.Elem):
else:
if nbVertices is None:
nbVertices = self._indices.size
- with self._vbos['__indices__']:
- gl.glDrawElements(self._MODES[self._mode],
- nbVertices,
- _glutils.numpyToGLType(self._indices.dtype),
- ctypes.c_void_p(0))
+ with self._vbos["__indices__"]:
+ gl.glDrawElements(
+ self._MODES[self._mode],
+ nbVertices,
+ _glutils.numpyToGLType(self._indices.dtype),
+ ctypes.c_void_p(0),
+ )
# Lines #######################################################################
+
class Lines(Geometry):
"""A set of segments"""
- _shaders = ("""
+
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
@@ -449,7 +452,8 @@ class Lines(Geometry):
vColor = color;
}
""",
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -464,33 +468,43 @@ class Lines(Geometry):
gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
$scenePostCall(vCameraPosition);
}
- """))
-
- def __init__(self, positions, normals=None, colors=(1., 1., 1., 1.),
- indices=None, mode='lines', width=1.):
- if mode == 'strip':
- mode = 'line_strip'
+ """
+ ),
+ )
+
+ def __init__(
+ self,
+ positions,
+ normals=None,
+ colors=(1.0, 1.0, 1.0, 1.0),
+ indices=None,
+ mode="lines",
+ width=1.0,
+ ):
+ if mode == "strip":
+ mode = "line_strip"
assert mode in self._LINE_MODES
self._width = width
self._smooth = True
- super(Lines, self).__init__(mode, indices,
- position=positions,
- normal=normals,
- color=colors)
+ super(Lines, self).__init__(
+ mode, indices, position=positions, normal=normals, color=colors
+ )
- width = event.notifyProperty('_width', converter=float,
- doc="Width of the line in pixels.")
+ width = event.notifyProperty(
+ "_width", converter=float, doc="Width of the line in pixels."
+ )
smooth = event.notifyProperty(
- '_smooth',
+ "_smooth",
converter=bool,
- doc="Smooth line rendering enabled (bool, default: True)")
+ doc="Smooth line rendering enabled (bool, default: True)",
+ )
def renderGL2(self, ctx):
# Prepare program
- isnormals = 'normal' in self._attributes
+ isnormals = "normal" in self._attributes
if isnormals:
fraglightfunction = ctx.viewport.light.fragmentDef
else:
@@ -501,7 +515,8 @@ class Lines(Geometry):
scenePreCall=ctx.fragCallPre,
scenePostCall=ctx.fragCallPost,
lightingFunction=fraglightfunction,
- lightingCall=ctx.viewport.light.fragmentCall)
+ lightingCall=ctx.viewport.light.fragmentCall,
+ )
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -510,10 +525,8 @@ class Lines(Geometry):
gl.glLineWidth(self.width)
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ prog.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ prog.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(prog)
@@ -527,7 +540,8 @@ class DashedLines(Lines):
This MUST be defined as a set of lines (no strip or loop).
"""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 origin;
attribute vec3 normal;
@@ -557,7 +571,8 @@ class DashedLines(Lines):
vOriginFragCoord = (ndcOrigin.xy + vec2(1.0, 1.0)) * 0.5 * viewportSize + vec2(0.5, 0.5);
}
""", # noqa
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -582,16 +597,19 @@ class DashedLines(Lines):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
- def __init__(self, positions, colors=(1., 1., 1., 1.),
- indices=None, width=1.):
+ def __init__(self, positions, colors=(1.0, 1.0, 1.0, 1.0), indices=None, width=1.0):
self._dash = 1, 0
- super(DashedLines, self).__init__(positions=positions,
- colors=colors,
- indices=indices,
- mode='lines',
- width=width)
+ super(DashedLines, self).__init__(
+ positions=positions,
+ colors=colors,
+ indices=indices,
+ mode="lines",
+ width=width,
+ )
@property
def dash(self):
@@ -612,7 +630,7 @@ class DashedLines(Lines):
:returns: Coordinates of lines
:rtype: numpy.ndarray of float32 of shape (N, 2, Ndim)
"""
- return self.getAttribute('position', copy=copy)
+ return self.getAttribute("position", copy=copy)
def setPositions(self, positions, copy=True):
"""Set line coordinates.
@@ -620,27 +638,27 @@ class DashedLines(Lines):
:param positions: Array of line coordinates
:param bool copy: True to copy input array, False to use as is
"""
- self.setAttribute('position', positions, copy=copy)
+ self.setAttribute("position", positions, copy=copy)
# Update line origins from given positions
- origins = numpy.array(positions, copy=True, order='C')
+ origins = numpy.array(positions, copy=True, order="C")
origins[1::2] = origins[::2]
- self.setAttribute('origin', origins, copy=False)
+ self.setAttribute("origin", origins, copy=False)
def renderGL2(self, context):
# Prepare program
- isnormals = 'normal' in self._attributes
+ isnormals = "normal" in self._attributes
if isnormals:
fraglightfunction = context.viewport.light.fragmentDef
else:
- fraglightfunction = \
- context.viewport.light.fragmentShaderFunctionNoop
+ fraglightfunction = context.viewport.light.fragmentShaderFunctionNoop
fragment = self._shaders[1].substitute(
sceneDecl=context.fragDecl,
scenePreCall=context.fragCallPre,
scenePostCall=context.fragCallPost,
lightingFunction=fraglightfunction,
- lightingCall=context.viewport.light.fragmentCall)
+ lightingCall=context.viewport.light.fragmentCall,
+ )
program = context.glCtx.prog(self._shaders[0], fragment)
program.use()
@@ -649,14 +667,13 @@ class DashedLines(Lines):
gl.glLineWidth(self.width)
- program.setUniformMatrix('matrix', context.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- context.objectToCamera.matrix,
- safe=True)
+ program.setUniformMatrix("matrix", context.objectToNDC.matrix)
+ program.setUniformMatrix(
+ "transformMat", context.objectToCamera.matrix, safe=True
+ )
- gl.glUniform2f(
- program.uniforms['viewportSize'], *context.viewport.size)
- gl.glUniform2f(program.uniforms['dash'], *self.dash)
+ gl.glUniform2f(program.uniforms["viewportSize"], *context.viewport.size)
+ gl.glUniform2f(program.uniforms["dash"], *self.dash)
context.setupProgram(program)
@@ -666,42 +683,64 @@ class DashedLines(Lines):
class Box(core.PrivateGroup):
"""Rectangular box"""
- _lineIndices = numpy.array((
- (0, 1), (1, 2), (2, 3), (3, 0), # Lines with z=0
- (0, 4), (1, 5), (2, 6), (3, 7), # Lines from z=0 to z=1
- (4, 5), (5, 6), (6, 7), (7, 4)), # Lines with z=1
- dtype=numpy.uint8)
+ _lineIndices = numpy.array(
+ (
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 0), # Lines with z=0
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7), # Lines from z=0 to z=1
+ (4, 5),
+ (5, 6),
+ (6, 7),
+ (7, 4),
+ ), # Lines with z=1
+ dtype=numpy.uint8,
+ )
_faceIndices = numpy.array(
- (0, 3, 1, 2, 5, 6, 4, 7, 7, 6, 6, 2, 7, 3, 4, 0, 5, 1),
- dtype=numpy.uint8)
-
- _vertices = numpy.array((
- # Corners with z=0
- (0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
- # Corners with z=1
- (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
- dtype=numpy.float32)
-
- def __init__(self, stroke=(1., 1., 1., 1.), fill=(1., 1., 1., 0.)):
+ (0, 3, 1, 2, 5, 6, 4, 7, 7, 6, 6, 2, 7, 3, 4, 0, 5, 1), dtype=numpy.uint8
+ )
+
+ _vertices = numpy.array(
+ (
+ # Corners with z=0
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (1.0, 1.0, 0.0),
+ (0.0, 1.0, 0.0),
+ # Corners with z=1
+ (0.0, 0.0, 1.0),
+ (1.0, 0.0, 1.0),
+ (1.0, 1.0, 1.0),
+ (0.0, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ def __init__(self, stroke=(1.0, 1.0, 1.0, 1.0), fill=(1.0, 1.0, 1.0, 0.0)):
super(Box, self).__init__()
- self._fill = Mesh3D(self._vertices,
- colors=rgba(fill),
- mode='triangle_strip',
- indices=self._faceIndices)
- self._fill.visible = self.fillColor[-1] != 0.
+ self._fill = Mesh3D(
+ self._vertices,
+ colors=rgba(fill),
+ mode="triangle_strip",
+ indices=self._faceIndices,
+ )
+ self._fill.visible = self.fillColor[-1] != 0.0
- self._stroke = Lines(self._vertices,
- indices=self._lineIndices,
- colors=rgba(stroke),
- mode='lines')
- self._stroke.visible = self.strokeColor[-1] != 0.
- self.strokeWidth = 1.
+ self._stroke = Lines(
+ self._vertices, indices=self._lineIndices, colors=rgba(stroke), mode="lines"
+ )
+ self._stroke.visible = self.strokeColor[-1] != 0.0
+ self.strokeWidth = 1.0
self._children = [self._stroke, self._fill]
- self._size = 1., 1., 1.
+ self._size = 1.0, 1.0, 1.0
@classmethod
def getLineIndices(cls, copy=True):
@@ -735,11 +774,11 @@ class Box(core.PrivateGroup):
if size != self.size:
self._size = size
self._fill.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
+ "position", self._vertices * numpy.array(size, dtype=numpy.float32)
+ )
self._stroke.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
+ "position", self._vertices * numpy.array(size, dtype=numpy.float32)
+ )
self.notify()
@property
@@ -769,29 +808,29 @@ class Box(core.PrivateGroup):
@property
def strokeColor(self):
"""RGBA color of the box lines (4-tuple of float in [0, 1])"""
- return tuple(self._stroke.getAttribute('color', copy=False))
+ return tuple(self._stroke.getAttribute("color", copy=False))
@strokeColor.setter
def strokeColor(self, color):
color = rgba(color)
if color != self.strokeColor:
- self._stroke.setAttribute('color', color)
+ self._stroke.setAttribute("color", color)
# Fully transparent = hidden
- self._stroke.visible = color[-1] != 0.
+ self._stroke.visible = color[-1] != 0.0
self.notify()
@property
def fillColor(self):
"""RGBA color of the box faces (4-tuple of float in [0, 1])"""
- return tuple(self._fill.getAttribute('color', copy=False))
+ return tuple(self._fill.getAttribute("color", copy=False))
@fillColor.setter
def fillColor(self, color):
color = rgba(color)
if color != self.fillColor:
- self._fill.setAttribute('color', color)
+ self._fill.setAttribute("color", color)
# Fully transparent = hidden
- self._fill.visible = color[-1] != 0.
+ self._fill.visible = color[-1] != 0.0
self.notify()
@property
@@ -805,21 +844,34 @@ class Box(core.PrivateGroup):
class Axes(Lines):
"""3D RGB orthogonal axes"""
- _vertices = numpy.array(((0., 0., 0.), (1., 0., 0.),
- (0., 0., 0.), (0., 1., 0.),
- (0., 0., 0.), (0., 0., 1.)),
- dtype=numpy.float32)
- _colors = numpy.array(((255, 0, 0, 255), (255, 0, 0, 255),
- (0, 255, 0, 255), (0, 255, 0, 255),
- (0, 0, 255, 255), (0, 0, 255, 255)),
- dtype=numpy.uint8)
+ _vertices = numpy.array(
+ (
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ _colors = numpy.array(
+ (
+ (255, 0, 0, 255),
+ (255, 0, 0, 255),
+ (0, 255, 0, 255),
+ (0, 255, 0, 255),
+ (0, 0, 255, 255),
+ (0, 0, 255, 255),
+ ),
+ dtype=numpy.uint8,
+ )
def __init__(self):
- super(Axes, self).__init__(self._vertices,
- colors=self._colors,
- width=3.)
- self._size = 1., 1., 1.
+ super(Axes, self).__init__(self._vertices, colors=self._colors, width=3.0)
+ self._size = 1.0, 1.0, 1.0
@property
def size(self):
@@ -833,8 +885,8 @@ class Axes(Lines):
if size != self.size:
self._size = size
self.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
+ "position", self._vertices * numpy.array(size, dtype=numpy.float32)
+ )
self.notify()
@@ -844,39 +896,67 @@ class BoxWithAxes(Lines):
:param color: RGBA color of the box
"""
- _vertices = numpy.array((
- # Axes corners
- (0., 0., 0.), (1., 0., 0.),
- (0., 0., 0.), (0., 1., 0.),
- (0., 0., 0.), (0., 0., 1.),
- # Box corners with z=0
- (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
- # Box corners with z=1
- (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
- dtype=numpy.float32)
-
- _axesColors = numpy.array(((1., 0., 0., 1.), (1., 0., 0., 1.),
- (0., 1., 0., 1.), (0., 1., 0., 1.),
- (0., 0., 1., 1.), (0., 0., 1., 1.)),
- dtype=numpy.float32)
-
- _lineIndices = numpy.array((
- (0, 1), (2, 3), (4, 5), # Axes lines
- (6, 7), (7, 8), # Box lines with z=0
- (6, 10), (7, 11), (8, 12), # Box lines from z=0 to z=1
- (9, 10), (10, 11), (11, 12), (12, 9)), # Box lines with z=1
- dtype=numpy.uint8)
-
- def __init__(self, color=(1., 1., 1., 1.)):
- self._color = (1., 1., 1., 1.)
+ _vertices = numpy.array(
+ (
+ # Axes corners
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0),
+ # Box corners with z=0
+ (1.0, 0.0, 0.0),
+ (1.0, 1.0, 0.0),
+ (0.0, 1.0, 0.0),
+ # Box corners with z=1
+ (0.0, 0.0, 1.0),
+ (1.0, 0.0, 1.0),
+ (1.0, 1.0, 1.0),
+ (0.0, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ _axesColors = numpy.array(
+ (
+ (1.0, 0.0, 0.0, 1.0),
+ (1.0, 0.0, 0.0, 1.0),
+ (0.0, 1.0, 0.0, 1.0),
+ (0.0, 1.0, 0.0, 1.0),
+ (0.0, 0.0, 1.0, 1.0),
+ (0.0, 0.0, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ _lineIndices = numpy.array(
+ (
+ (0, 1),
+ (2, 3),
+ (4, 5), # Axes lines
+ (6, 7),
+ (7, 8), # Box lines with z=0
+ (6, 10),
+ (7, 11),
+ (8, 12), # Box lines from z=0 to z=1
+ (9, 10),
+ (10, 11),
+ (11, 12),
+ (12, 9),
+ ), # Box lines with z=1
+ dtype=numpy.uint8,
+ )
+
+ def __init__(self, color=(1.0, 1.0, 1.0, 1.0)):
+ self._color = (1.0, 1.0, 1.0, 1.0)
colors = numpy.ones((len(self._vertices), 4), dtype=numpy.float32)
- colors[:len(self._axesColors), :] = self._axesColors
+ colors[: len(self._axesColors), :] = self._axesColors
- super(BoxWithAxes, self).__init__(self._vertices,
- indices=self._lineIndices,
- colors=colors,
- width=2.)
- self._size = 1., 1., 1.
+ super(BoxWithAxes, self).__init__(
+ self._vertices, indices=self._lineIndices, colors=colors, width=2.0
+ )
+ self._size = 1.0, 1.0, 1.0
self.color = color
@property
@@ -890,9 +970,9 @@ class BoxWithAxes(Lines):
if color != self._color:
self._color = color
colors = numpy.empty((len(self._vertices), 4), dtype=numpy.float32)
- colors[:len(self._axesColors), :] = self._axesColors
- colors[len(self._axesColors):, :] = self._color
- self.setAttribute('color', colors) # Do the notification
+ colors[: len(self._axesColors), :] = self._axesColors
+ colors[len(self._axesColors) :, :] = self._color
+ self.setAttribute("color", colors) # Do the notification
@property
def size(self):
@@ -906,8 +986,8 @@ class BoxWithAxes(Lines):
if size != self.size:
self._size = size
self.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
+ "position", self._vertices * numpy.array(size, dtype=numpy.float32)
+ )
self.notify()
@@ -919,29 +999,29 @@ class PlaneInGroup(core.PrivateGroup):
Cannot set the transform attribute of this primitive.
This primitive never has any bounds.
"""
+
# TODO inherit from Lines directly?, make sure the plane remains visible?
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
+ def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0)):
super(PlaneInGroup, self).__init__()
self._cache = None, None # Store bounds, vertices
self._outline = None
self._color = None
- self.color = 1., 1., 1., 1. # Set _color
- self._width = 2.
+ self.color = 1.0, 1.0, 1.0, 1.0 # Set _color
+ self._width = 2.0
self._strokeVisible = True
self._plane = utils.Plane(point, normal)
self._plane.addListener(self._planeChanged)
def moveToCenter(self):
- """Place the plane at the center of the data, not changing orientation.
- """
+ """Place the plane at the center of the data, not changing orientation."""
if self.parent is not None:
bounds = self.parent.bounds(dataBounds=True)
if bounds is not None:
- center = (bounds[0] + bounds[1]) / 2.
- _logger.debug('Moving plane to center: %s', str(center))
+ center = (bounds[0] + bounds[1]) / 2.0
+ _logger.debug("Moving plane to center: %s", str(center))
self.plane.point = center
@property
@@ -953,7 +1033,7 @@ class PlaneInGroup(core.PrivateGroup):
def color(self, color):
self._color = numpy.array(color, copy=True, dtype=numpy.float32)
if self._outline is not None:
- self._outline.setAttribute('color', self._color)
+ self._outline.setAttribute("color", self._color)
self.notify() # This is OK as Lines are rebuild for each rendering
@property
@@ -1022,7 +1102,8 @@ class PlaneInGroup(core.PrivateGroup):
boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
lineIndices = Box.getLineIndices(copy=False)
vertices = utils.boxPlaneIntersect(
- boxVertices, lineIndices, self.plane.normal, self.plane.point)
+ boxVertices, lineIndices, self.plane.normal, self.plane.point
+ )
self._cache = bounds, vertices if len(vertices) != 0 else None
@@ -1044,15 +1125,15 @@ class PlaneInGroup(core.PrivateGroup):
def prepareGL2(self, ctx):
if self.isValid:
if self._outline is None: # Init outline
- self._outline = Lines(self.contourVertices,
- mode='loop',
- colors=self.color)
+ self._outline = Lines(
+ self.contourVertices, mode="loop", colors=self.color
+ )
self._outline.width = self._width
self._outline.visible = self._strokeVisible
self._children.append(self._outline)
# Update vertices, TODO only when necessary
- self._outline.setAttribute('position', self.contourVertices)
+ self._outline.setAttribute("position", self.contourVertices)
super(PlaneInGroup, self).prepareGL2(ctx)
@@ -1097,28 +1178,36 @@ class BoundedGroup(core.Group):
def _bounds(self, dataBounds=False):
if dataBounds and self.size is not None:
- return numpy.array(((0., 0., 0.), self.size),
- dtype=numpy.float32)
+ return numpy.array(((0.0, 0.0, 0.0), self.size), dtype=numpy.float32)
else:
return super(BoundedGroup, self)._bounds(dataBounds)
# Points ######################################################################
+
class _Points(Geometry):
"""Base class to render a set of points."""
- DIAMOND = 'd'
- CIRCLE = 'o'
- SQUARE = 's'
- PLUS = '+'
- X_MARKER = 'x'
- ASTERISK = '*'
- H_LINE = '_'
- V_LINE = '|'
-
- SUPPORTED_MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS,
- X_MARKER, ASTERISK, H_LINE, V_LINE)
+ DIAMOND = "d"
+ CIRCLE = "o"
+ SQUARE = "s"
+ PLUS = "+"
+ X_MARKER = "x"
+ ASTERISK = "*"
+ H_LINE = "_"
+ V_LINE = "|"
+
+ SUPPORTED_MARKERS = (
+ DIAMOND,
+ CIRCLE,
+ SQUARE,
+ PLUS,
+ X_MARKER,
+ ASTERISK,
+ H_LINE,
+ V_LINE,
+ )
"""List of supported markers:
- 'd' diamond
@@ -1207,10 +1296,12 @@ class _Points(Geometry):
return 0.0;
}
}
- """
+ """,
}
- _shaders = (string.Template("""
+ _shaders = (
+ string.Template(
+ """
#version 120
attribute float x;
@@ -1237,8 +1328,10 @@ class _Points(Geometry):
gl_PointSize = size;
vSize = size;
}
- """),
- string.Template("""
+ """
+ ),
+ string.Template(
+ """
#version 120
varying vec4 vCameraPosition;
@@ -1263,25 +1356,23 @@ class _Points(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
_ATTR_INFO = {
- 'x': {'dims': (1, 2), 'lastDim': (1,)},
- 'y': {'dims': (1, 2), 'lastDim': (1,)},
- 'z': {'dims': (1, 2), 'lastDim': (1,)},
- 'size': {'dims': (1, 2), 'lastDim': (1,)},
+ "x": {"dims": (1, 2), "lastDim": (1,)},
+ "y": {"dims": (1, 2), "lastDim": (1,)},
+ "z": {"dims": (1, 2), "lastDim": (1,)},
+ "size": {"dims": (1, 2), "lastDim": (1,)},
}
- def __init__(self, x, y, z, value, size=1., indices=None):
- super(_Points, self).__init__('points', indices,
- x=x,
- y=y,
- z=z,
- value=value,
- size=size,
- attrib0='x')
- self.boundsAttributeNames = 'x', 'y', 'z'
- self._marker = 'o'
+ def __init__(self, x, y, z, value, size=1.0, indices=None):
+ super(_Points, self).__init__(
+ "points", indices, x=x, y=y, z=z, value=value, size=size, attrib0="x"
+ )
+ self.boundsAttributeNames = "x", "y", "z"
+ self._marker = "o"
@property
def marker(self):
@@ -1300,20 +1391,16 @@ class _Points(Geometry):
self.notify()
def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- raise NotImplementedError(
- "This method must be implemented in subclass")
+ """Type definition, fragment shader declaration, fragment shader call"""
+ raise NotImplementedError("This method must be implemented in subclass")
def _renderGL2PreDrawHook(self, ctx, program):
"""Override in subclass to run code before calling gl draw"""
pass
def renderGL2(self, ctx):
- valueType, valueToColorDecl, valueToColorCall = \
- self._shaderValueDefinition()
- vertexShader = self._shaders[0].substitute(
- valueType=valueType)
+ valueType, valueToColorDecl, valueToColorCall = self._shaderValueDefinition()
+ vertexShader = self._shaders[0].substitute(valueType=valueType)
fragmentShader = self._shaders[1].substitute(
sceneDecl=ctx.fragDecl,
scenePreCall=ctx.fragCallPre,
@@ -1321,19 +1408,17 @@ class _Points(Geometry):
valueType=valueType,
valueToColorDecl=valueToColorDecl,
valueToColorCall=valueToColorCall,
- alphaSymbolDecl=self._MARKER_FUNCTIONS[self.marker])
- program = ctx.glCtx.prog(vertexShader, fragmentShader,
- attrib0=self.attrib0)
+ alphaSymbolDecl=self._MARKER_FUNCTIONS[self.marker],
+ )
+ program = ctx.glCtx.prog(vertexShader, fragmentShader, attrib0=self.attrib0)
program.use()
gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
# gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ program.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ program.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(program)
@@ -1346,16 +1431,12 @@ class Points(_Points):
"""A set of data points with an associated value and size."""
_ATTR_INFO = _Points._ATTR_INFO.copy()
- _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (1,)}})
+ _ATTR_INFO.update({"value": {"dims": (1, 2), "lastDim": (1,)}})
- def __init__(self, x, y, z, value=0., size=1.,
- indices=None, colormap=None):
- super(Points, self).__init__(x=x,
- y=y,
- z=z,
- indices=indices,
- size=size,
- value=value)
+ def __init__(self, x, y, z, value=0.0, size=1.0, indices=None, colormap=None):
+ super(Points, self).__init__(
+ x=x, y=y, z=z, indices=indices, size=size, value=value
+ )
self._colormap = colormap or Colormap() # Default colormap
self._colormap.addListener(self._cmapChanged)
@@ -1370,9 +1451,8 @@ class Points(_Points):
self.notify(*args, **kwargs)
def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- return 'float', self.colormap.decl, self.colormap.call
+ """Type definition, fragment shader declaration, fragment shader call"""
+ return "float", self.colormap.decl, self.colormap.call
def _renderGL2PreDrawHook(self, ctx, program):
"""Set-up colormap before calling gl draw"""
@@ -1383,21 +1463,16 @@ class ColorPoints(_Points):
"""A set of points with an associated color and size."""
_ATTR_INFO = _Points._ATTR_INFO.copy()
- _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (3, 4)}})
+ _ATTR_INFO.update({"value": {"dims": (1, 2), "lastDim": (3, 4)}})
- def __init__(self, x, y, z, color=(1., 1., 1., 1.), size=1.,
- indices=None):
- super(ColorPoints, self).__init__(x=x,
- y=y,
- z=z,
- indices=indices,
- size=size,
- value=color)
+ def __init__(self, x, y, z, color=(1.0, 1.0, 1.0, 1.0), size=1.0, indices=None):
+ super(ColorPoints, self).__init__(
+ x=x, y=y, z=z, indices=indices, size=size, value=color
+ )
def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- return 'vec4', '', ''
+ """Type definition, fragment shader declaration, fragment shader call"""
+ return "vec4", "", ""
def setColor(self, color, copy=True):
"""Set colors
@@ -1407,7 +1482,7 @@ class ColorPoints(_Points):
:param bool copy: True to copy colors (default),
False to use provided array (Do not modify!)
"""
- self.setAttribute('value', color, copy=copy)
+ self.setAttribute("value", color, copy=copy)
def getColor(self, copy=True):
"""Returns the color or array of colors of the points.
@@ -1417,13 +1492,14 @@ class ColorPoints(_Points):
:return: Color or array of colors
:rtype: numpy.ndarray
"""
- return self.getAttribute('value', copy=copy)
+ return self.getAttribute("value", copy=copy)
class GridPoints(Geometry):
# GLSL 1.30 !
"""Data points on a regular grid with an associated value and size."""
- _shaders = ("""
+ _shaders = (
+ """
#version 130
in float value;
@@ -1481,7 +1557,8 @@ class GridPoints(Geometry):
gl_PointSize = size;
}
""",
- string.Template("""
+ string.Template(
+ """
#version 130
in vec4 vCameraPosition;
@@ -1498,18 +1575,27 @@ class GridPoints(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
_ATTR_INFO = {
- 'value': {'dims': (1, 2), 'lastDim': (1,)},
- 'size': {'dims': (1, 2), 'lastDim': (1,)}
+ "value": {"dims": (1, 2), "lastDim": (1,)},
+ "size": {"dims": (1, 2), "lastDim": (1,)},
}
# TODO Add colormap, shape?
# TODO could also use a texture to store values
- def __init__(self, values=0., shape=None, sizes=1., indices=None,
- minValue=None, maxValue=None):
+ def __init__(
+ self,
+ values=0.0,
+ shape=None,
+ sizes=1.0,
+ indices=None,
+ minValue=None,
+ maxValue=None,
+ ):
if isinstance(values, abc.Iterable):
values = numpy.array(values, copy=False)
@@ -1525,16 +1611,14 @@ class GridPoints(Geometry):
assert len(self._shape) in (1, 2, 3)
- super(GridPoints, self).__init__('points', indices,
- value=values,
- size=sizes)
+ super(GridPoints, self).__init__("points", indices, value=values, size=sizes)
- data = self.getAttribute('value', copy=False)
+ data = self.getAttribute("value", copy=False)
self._minValue = data.min() if minValue is None else minValue
self._maxValue = data.max() if maxValue is None else maxValue
- minValue = event.notifyProperty('_minValue')
- maxValue = event.notifyProperty('_maxValue')
+ minValue = event.notifyProperty("_minValue")
+ maxValue = event.notifyProperty("_maxValue")
def _bounds(self, dataBounds=False):
# Get bounds from values shape
@@ -1547,7 +1631,8 @@ class GridPoints(Geometry):
fragment = self._shaders[1].substitute(
sceneDecl=ctx.fragDecl,
scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost)
+ scenePostCall=ctx.fragCallPost,
+ )
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -1555,25 +1640,26 @@ class GridPoints(Geometry):
gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
# gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ prog.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ prog.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(prog)
- gl.glUniform3i(prog.uniforms['gridDims'],
- self._shape[2] if len(self._shape) == 3 else 1,
- self._shape[1] if len(self._shape) >= 2 else 1,
- self._shape[0])
+ gl.glUniform3i(
+ prog.uniforms["gridDims"],
+ self._shape[2] if len(self._shape) == 3 else 1,
+ self._shape[1] if len(self._shape) >= 2 else 1,
+ self._shape[0],
+ )
- gl.glUniform2f(prog.uniforms['valRange'], self.minValue, self.maxValue)
+ gl.glUniform2f(prog.uniforms["valRange"], self.minValue, self.maxValue)
self._draw(prog, nbVertices=reduce(lambda a, b: a * b, self._shape))
# Spheres #####################################################################
+
class Spheres(Geometry):
"""A set of spheres.
@@ -1584,6 +1670,7 @@ class Spheres(Geometry):
- Do not render distorion by perspective projection.
- If the sphere center is clipped, the whole sphere is not displayed.
"""
+
# TODO check those links
# Accounting for perspective projection
# http://iquilezles.org/www/articles/sphereproj/sphereproj.htm
@@ -1596,7 +1683,8 @@ class Spheres(Geometry):
# TODO some issues with small scaling and regular grid or due to sampling
- _shaders = ("""
+ _shaders = (
+ """
#version 120
attribute vec3 position;
@@ -1635,7 +1723,8 @@ class Spheres(Geometry):
vViewDepth = vCameraPosition.z;
}
""",
- string.Template("""
+ string.Template(
+ """
# version 120
uniform mat4 projMat;
@@ -1675,20 +1764,21 @@ class Spheres(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
_ATTR_INFO = {
- 'position': {'dims': (2, ), 'lastDim': (2, 3, 4)},
- 'radius': {'dims': (1, 2), 'lastDim': (1, )},
- 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
+ "position": {"dims": (2,), "lastDim": (2, 3, 4)},
+ "radius": {"dims": (1, 2), "lastDim": (1,)},
+ "color": {"dims": (1, 2), "lastDim": (3, 4)},
}
- def __init__(self, positions, radius=1., colors=(1., 1., 1., 1.)):
+ def __init__(self, positions, radius=1.0, colors=(1.0, 1.0, 1.0, 1.0)):
self.__bounds = None
- super(Spheres, self).__init__('points', None,
- position=positions,
- radius=radius,
- color=colors)
+ super(Spheres, self).__init__(
+ "points", None, position=positions, radius=radius, color=colors
+ )
def renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
@@ -1696,7 +1786,8 @@ class Spheres(Geometry):
scenePreCall=ctx.fragCallPre,
scenePostCall=ctx.fragCallPost,
lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall)
+ lightingCall=ctx.viewport.light.fragmentCall,
+ )
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -1706,14 +1797,12 @@ class Spheres(Geometry):
gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
# gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- prog.setUniformMatrix('projMat', ctx.projection.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ prog.setUniformMatrix("projMat", ctx.projection.matrix)
+ prog.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(prog)
- gl.glUniform2f(prog.uniforms['screenSize'], *ctx.viewport.size)
+ gl.glUniform2f(prog.uniforms["screenSize"], *ctx.viewport.size)
self._draw(prog)
@@ -1721,21 +1810,25 @@ class Spheres(Geometry):
if self.__bounds is None:
self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
# Support vertex with to 2 to 4 coordinates
- positions = self._attributes['position']
- radius = self._attributes['radius']
- self.__bounds[0, :positions.shape[1]] = \
- (positions - radius).min(axis=0)[:3]
- self.__bounds[1, :positions.shape[1]] = \
- (positions + radius).max(axis=0)[:3]
+ positions = self._attributes["position"]
+ radius = self._attributes["radius"]
+ self.__bounds[0, : positions.shape[1]] = (positions - radius).min(axis=0)[
+ :3
+ ]
+ self.__bounds[1, : positions.shape[1]] = (positions + radius).max(axis=0)[
+ :3
+ ]
return self.__bounds.copy()
# Meshes ######################################################################
+
class Mesh3D(Geometry):
"""A conventional 3D mesh"""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
@@ -1759,7 +1852,8 @@ class Mesh3D(Geometry):
gl_Position = matrix * vec4(position, 1.0);
}
""",
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -1776,21 +1870,17 @@ class Mesh3D(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
-
- def __init__(self,
- positions,
- colors,
- normals=None,
- mode='triangles',
- indices=None,
- copy=True):
+ """
+ ),
+ )
+
+ def __init__(
+ self, positions, colors, normals=None, mode="triangles", indices=None, copy=True
+ ):
assert mode in self._TRIANGLE_MODES
- super(Mesh3D, self).__init__(mode, indices,
- position=positions,
- normal=normals,
- color=colors,
- copy=copy)
+ super(Mesh3D, self).__init__(
+ mode, indices, position=positions, normal=normals, color=colors, copy=copy
+ )
self._culling = None
@@ -1804,13 +1894,13 @@ class Mesh3D(Geometry):
@culling.setter
def culling(self, culling):
- assert culling in ('back', 'front', None)
+ assert culling in ("back", "front", None)
if culling != self._culling:
self._culling = culling
self.notify()
def renderGL2(self, ctx):
- isnormals = 'normal' in self._attributes
+ isnormals = "normal" in self._attributes
if isnormals:
fragLightFunction = ctx.viewport.light.fragmentDef
else:
@@ -1821,7 +1911,8 @@ class Mesh3D(Geometry):
scenePreCall=ctx.fragCallPre,
scenePostCall=ctx.fragCallPost,
lightingFunction=fragLightFunction,
- lightingCall=ctx.viewport.light.fragmentCall)
+ lightingCall=ctx.viewport.light.fragmentCall,
+ )
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -1829,14 +1920,12 @@ class Mesh3D(Geometry):
ctx.viewport.light.setupProgram(ctx, prog)
if self.culling is not None:
- cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
+ cullFace = gl.GL_FRONT if self.culling == "front" else gl.GL_BACK
gl.glCullFace(cullFace)
gl.glEnable(gl.GL_CULL_FACE)
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ prog.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ prog.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(prog)
@@ -1849,7 +1938,8 @@ class Mesh3D(Geometry):
class ColormapMesh3D(Geometry):
"""A 3D mesh with color computed from a colormap"""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 normal;
attribute float value;
@@ -1873,7 +1963,8 @@ class ColormapMesh3D(Geometry):
gl_Position = matrix * vec4(position, 1.0);
}
""",
- string.Template("""
+ string.Template(
+ """
uniform float alpha;
varying vec4 vCameraPosition;
@@ -1895,21 +1986,23 @@ class ColormapMesh3D(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
-
- def __init__(self,
- position,
- value,
- colormap=None,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
- super(ColormapMesh3D, self).__init__(mode, indices,
- position=position,
- normal=normal,
- value=value,
- copy=copy)
+ """
+ ),
+ )
+
+ def __init__(
+ self,
+ position,
+ value,
+ colormap=None,
+ normal=None,
+ mode="triangles",
+ indices=None,
+ copy=True,
+ ):
+ super(ColormapMesh3D, self).__init__(
+ mode, indices, position=position, normal=normal, value=value, copy=copy
+ )
self._alpha = 1.0
self._lineWidth = 1.0
@@ -1918,17 +2011,19 @@ class ColormapMesh3D(Geometry):
self._colormap = colormap or Colormap() # Default colormap
self._colormap.addListener(self._cmapChanged)
- lineWidth = event.notifyProperty('_lineWidth', converter=float,
- doc="Width of the line in pixels.")
+ lineWidth = event.notifyProperty(
+ "_lineWidth", converter=float, doc="Width of the line in pixels."
+ )
lineSmooth = event.notifyProperty(
- '_lineSmooth',
+ "_lineSmooth",
converter=bool,
- doc="Smooth line rendering enabled (bool, default: True)")
+ doc="Smooth line rendering enabled (bool, default: True)",
+ )
alpha = event.notifyProperty(
- '_alpha', converter=float,
- doc="Transparency of the mesh, float in [0, 1]")
+ "_alpha", converter=float, doc="Transparency of the mesh, float in [0, 1]"
+ )
@property
def culling(self):
@@ -1940,7 +2035,7 @@ class ColormapMesh3D(Geometry):
@culling.setter
def culling(self, culling):
- assert culling in ('back', 'front', None)
+ assert culling in ("back", "front", None)
if culling != self._culling:
self._culling = culling
self.notify()
@@ -1955,7 +2050,7 @@ class ColormapMesh3D(Geometry):
self.notify(*args, **kwargs)
def renderGL2(self, ctx):
- if 'normal' in self._attributes:
+ if "normal" in self._attributes:
self._renderGL2(ctx)
else: # Disable lighting
with self.viewport.light.turnOff():
@@ -1969,7 +2064,8 @@ class ColormapMesh3D(Geometry):
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call)
+ colormapCall=self.colormap.call,
+ )
program = ctx.glCtx.prog(self._shaders[0], fragment)
program.use()
@@ -1978,15 +2074,13 @@ class ColormapMesh3D(Geometry):
self.colormap.setupProgram(ctx, program)
if self.culling is not None:
- cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
+ cullFace = gl.GL_FRONT if self.culling == "front" else gl.GL_BACK
gl.glCullFace(cullFace)
gl.glEnable(gl.GL_CULL_FACE)
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
+ program.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ program.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
+ gl.glUniform1f(program.uniforms["alpha"], self._alpha)
if self.drawMode in self._LINE_MODES:
gl.glLineWidth(self.lineWidth)
@@ -2001,10 +2095,12 @@ class ColormapMesh3D(Geometry):
# ImageData ##################################################################
+
class _Image(Geometry):
"""Base class for ImageData and ImageRgba"""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec2 position;
uniform mat4 matrix;
@@ -2025,7 +2121,8 @@ class _Image(Geometry):
gl_Position = matrix * positionVec4;
}
""",
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec2 vTexCoords;
@@ -2051,22 +2148,24 @@ class _Image(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
- _UNIT_SQUARE = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32)
+ _UNIT_SQUARE = numpy.array(
+ ((0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)), dtype=numpy.float32
+ )
def __init__(self, data, copy=True):
- super(_Image, self).__init__(mode='triangle_strip',
- position=self._UNIT_SQUARE)
+ super(_Image, self).__init__(mode="triangle_strip", position=self._UNIT_SQUARE)
self._texture = None
self._update_texture = True
self._update_texture_filter = False
self._data = None
self.setData(data, copy)
- self._alpha = 1.
- self._interpolation = 'linear'
+ self._alpha = 1.0
+ self._interpolation = "linear"
self.isBackfaceVisible = True
@@ -2080,7 +2179,9 @@ class _Image(Geometry):
self._update_texture = True
# By updating the position rather than always using a unit square
# we benefit from Geometry bounds handling
- self.setAttribute('position', self._UNIT_SQUARE * (self._data.shape[1], self._data.shape[0]))
+ self.setAttribute(
+ "position", self._UNIT_SQUARE * (self._data.shape[1], self._data.shape[0])
+ )
self.notify()
def getData(self, copy=True):
@@ -2093,7 +2194,7 @@ class _Image(Geometry):
@interpolation.setter
def interpolation(self, interpolation):
- assert interpolation in ('linear', 'nearest')
+ assert interpolation in ("linear", "nearest")
self._interpolation = interpolation
self._update_texture_filter = True
self.notify()
@@ -2113,15 +2214,14 @@ class _Image(Geometry):
:return: 2-tuple of gl flags (internalFormat, format)
"""
- raise NotImplementedError(
- "This method must be implemented in a subclass")
+ raise NotImplementedError("This method must be implemented in a subclass")
def prepareGL2(self, ctx):
if self._texture is None or self._update_texture:
if self._texture is not None:
self._texture.discard()
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
filter_ = gl.GL_NEAREST
else:
filter_ = gl.GL_LINEAR
@@ -2137,11 +2237,12 @@ class _Image(Geometry):
format_,
minFilter=filter_,
magFilter=filter_,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
if self._update_texture_filter and self._texture is not None:
self._update_texture_filter = False
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
filter_ = gl.GL_NEAREST
else:
filter_ = gl.GL_LINEAR
@@ -2163,8 +2264,7 @@ class _Image(Geometry):
def _shaderImageColorDecl(self):
"""Returns fragment shader imageColor function declaration"""
- raise NotImplementedError(
- "This method must be implemented in a subclass")
+ raise NotImplementedError("This method must be implemented in a subclass")
def _renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
@@ -2173,8 +2273,8 @@ class _Image(Geometry):
scenePostCall=ctx.fragCallPost,
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
- imageDecl=self._shaderImageColorDecl()
- )
+ imageDecl=self._shaderImageColorDecl(),
+ )
program = ctx.glCtx.prog(self._shaders[0], fragment)
program.use()
@@ -2184,16 +2284,14 @@ class _Image(Geometry):
gl.glCullFace(gl.GL_BACK)
gl.glEnable(gl.GL_CULL_FACE)
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
+ program.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ program.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
+ gl.glUniform1f(program.uniforms["alpha"], self._alpha)
shape = self._data.shape
- gl.glUniform2f(program.uniforms['dataScale'], 1./shape[1], 1./shape[0])
+ gl.glUniform2f(program.uniforms["dataScale"], 1.0 / shape[1], 1.0 / shape[0])
- gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
+ gl.glUniform1i(program.uniforms["data"], self._texture.texUnit)
ctx.setupProgram(program)
@@ -2210,7 +2308,8 @@ class _Image(Geometry):
class ImageData(_Image):
"""Display a 2x2 data array with a texture."""
- _imageDecl = string.Template("""
+ _imageDecl = string.Template(
+ """
$colormapDecl
vec4 imageColor(sampler2D data, vec2 texCoords) {
@@ -2218,7 +2317,8 @@ class ImageData(_Image):
vec4 color = $colormapCall(value);
return color;
}
- """)
+ """
+ )
def __init__(self, data, copy=True, colormap=None):
super(ImageData, self).__init__(data, copy=copy)
@@ -2227,7 +2327,7 @@ class ImageData(_Image):
self._colormap.addListener(self._cmapChanged)
def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C', dtype=numpy.float32)
+ data = numpy.array(data, copy=copy, order="C", dtype=numpy.float32)
# TODO support (u)int8|16
assert data.ndim == 2
@@ -2250,12 +2350,13 @@ class ImageData(_Image):
def _shaderImageColorDecl(self):
return self._imageDecl.substitute(
- colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call)
+ colormapDecl=self.colormap.decl, colormapCall=self.colormap.call
+ )
# ImageRgba ##################################################################
+
class ImageRgba(_Image):
"""Display a 2x2 RGBA image with a texture.
@@ -2273,10 +2374,10 @@ class ImageRgba(_Image):
super(ImageRgba, self).__init__(data, copy=copy)
def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
assert data.ndim == 3
assert data.shape[2] in (3, 4)
- if data.dtype.kind == 'f':
+ if data.dtype.kind == "f":
if data.dtype != numpy.dtype(numpy.float32):
_logger.warning("Converting image data to float32")
data = numpy.array(data, dtype=numpy.float32, copy=False)
@@ -2298,6 +2399,7 @@ class ImageRgba(_Image):
# TODO lighting, clipping as groups?
# group composition?
+
class GroupDepthOffset(core.Group):
"""A group using 2-pass rendering and glDepthRange to avoid Z-fighting"""
@@ -2309,7 +2411,7 @@ class GroupDepthOffset(core.Group):
def prepareGL2(self, ctx):
if self._epsilon is None:
depthbits = gl.glGetInteger(gl.GL_DEPTH_BITS)
- self._epsilon = 1. / (1 << (depthbits - 1))
+ self._epsilon = 1.0 / (1 << (depthbits - 1))
def renderGL2(self, ctx):
if self.isDepthRangeOn:
@@ -2322,38 +2424,34 @@ class GroupDepthOffset(core.Group):
with gl.enabled(gl.GL_CULL_FACE):
gl.glCullFace(gl.GL_BACK)
for child in self.children:
- gl.glColorMask(
- gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
+ gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(self._epsilon, 1.)
+ gl.glDepthRange(self._epsilon, 1.0)
child.render(ctx)
- gl.glColorMask(
- gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
+ gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_FALSE)
- gl.glDepthRange(0., 1. - self._epsilon)
+ gl.glDepthRange(0.0, 1.0 - self._epsilon)
child.render(ctx)
gl.glCullFace(gl.GL_FRONT)
for child in reversed(self.children):
- gl.glColorMask(
- gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
+ gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(self._epsilon, 1.)
+ gl.glDepthRange(self._epsilon, 1.0)
child.render(ctx)
- gl.glColorMask(
- gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
+ gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_FALSE)
- gl.glDepthRange(0., 1. - self._epsilon)
+ gl.glDepthRange(0.0, 1.0 - self._epsilon)
child.render(ctx)
gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(0., 1.)
+ gl.glDepthRange(0.0, 1.0)
# gl.glDepthFunc(gl.GL_LEQUAL)
# TODO use epsilon for all rendering?
# TODO issue with picking in depth buffer!
@@ -2385,7 +2483,7 @@ class GroupNoDepth(core.Group):
class GroupBBox(core.PrivateGroup):
"""A group displaying a bounding box around the children."""
- def __init__(self, children=(), color=(1., 1., 1., 1.)):
+ def __init__(self, children=(), color=(1.0, 1.0, 1.0, 1.0)):
super(GroupBBox, self).__init__()
self._group = core.Group(children)
@@ -2397,7 +2495,7 @@ class GroupBBox(core.PrivateGroup):
self._boxWithAxes.smooth = False
self._boxWithAxes.transforms = self._boxTransforms
- self._box = Box(stroke=color, fill=(1., 1., 1., 0.))
+ self._box = Box(stroke=color, fill=(1.0, 1.0, 1.0, 0.0))
self._box.strokeSmooth = False
self._box.transforms = self._boxTransforms
self._box.visible = False
@@ -2407,7 +2505,7 @@ class GroupBBox(core.PrivateGroup):
self._axes.transforms = self._boxTransforms
self._axes.visible = False
- self.strokeWidth = 2.
+ self.strokeWidth = 2.0
self._children = [self._boxWithAxes, self._box, self._axes, self._group]
@@ -2418,7 +2516,7 @@ class GroupBBox(core.PrivateGroup):
origin = bounds[0]
size = bounds[1] - bounds[0]
else:
- origin, size = (0., 0., 0.), (1., 1., 1.)
+ origin, size = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0)
self._boxTransforms[0].translation = origin
@@ -2487,8 +2585,9 @@ class GroupBBox(core.PrivateGroup):
@axesVisible.setter
def axesVisible(self, visible):
- self._updateBoxAndAxesVisibility(axesVisible=bool(visible),
- boxVisible=self.boxVisible)
+ self._updateBoxAndAxesVisibility(
+ axesVisible=bool(visible), boxVisible=self.boxVisible
+ )
@property
def boxVisible(self):
@@ -2497,12 +2596,14 @@ class GroupBBox(core.PrivateGroup):
@boxVisible.setter
def boxVisible(self, visible):
- self._updateBoxAndAxesVisibility(axesVisible=self.axesVisible,
- boxVisible=bool(visible))
+ self._updateBoxAndAxesVisibility(
+ axesVisible=self.axesVisible, boxVisible=bool(visible)
+ )
# Clipping Plane ##############################################################
+
class ClipPlane(PlaneInGroup):
"""A clipping plane attached to a box"""
@@ -2513,8 +2614,9 @@ class ClipPlane(PlaneInGroup):
# Set-up clipping plane for following brothers
# No need of perspective divide, no projection
- point = ctx.objectToCamera.transformPoint(self.plane.point,
- perspectiveDivide=False)
+ point = ctx.objectToCamera.transformPoint(
+ self.plane.point, perspectiveDivide=False
+ )
normal = ctx.objectToCamera.transformNormal(self.plane.normal)
ctx.setClipPlane(point, normal)
diff --git a/src/silx/gui/plot3d/scene/test/__init__.py b/src/silx/gui/plot3d/scene/test/__init__.py
index 3bb978e..4bdcc18 100644
--- a/src/silx/gui/plot3d/scene/test/__init__.py
+++ b/src/silx/gui/plot3d/scene/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot3d/scene/test/test_transform.py b/src/silx/gui/plot3d/scene/test/test_transform.py
index 69e991b..cba384d 100644
--- a/src/silx/gui/plot3d/scene/test/test_transform.py
+++ b/src/silx/gui/plot3d/scene/test/test_transform.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
@@ -23,8 +22,6 @@
#
# ###########################################################################*/
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "05/01/2017"
@@ -37,7 +34,6 @@ from silx.gui.plot3d.scene import transform
class TestTransformList(unittest.TestCase):
-
def assertSameArrays(self, a, b):
return self.assertTrue(numpy.allclose(a, b, atol=1e-06))
@@ -48,25 +44,36 @@ class TestTransformList(unittest.TestCase):
self.assertSameArrays(refmatrix, transforms.matrix)
# Append translate
- transforms.append(transform.Translate(1., 1., 1.))
- refmatrix = numpy.array(((1., 0., 0., 1.),
- (0., 1., 0., 1.),
- (0., 0., 1., 1.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ transforms.append(transform.Translate(1.0, 1.0, 1.0))
+ refmatrix = numpy.array(
+ (
+ (1.0, 0.0, 0.0, 1.0),
+ (0.0, 1.0, 0.0, 1.0),
+ (0.0, 0.0, 1.0, 1.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
self.assertSameArrays(refmatrix, transforms.matrix)
# Extend scale
- transforms.extend([transform.Scale(0.1, 2., 1.)])
- refmatrix = numpy.dot(refmatrix,
- numpy.array(((0.1, 0., 0., 0.),
- (0., 2., 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)),
- dtype=numpy.float32))
+ transforms.extend([transform.Scale(0.1, 2.0, 1.0)])
+ refmatrix = numpy.dot(
+ refmatrix,
+ numpy.array(
+ (
+ (0.1, 0.0, 0.0, 0.0),
+ (0.0, 2.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ ),
+ )
self.assertSameArrays(refmatrix, transforms.matrix)
# Insert rotate
- transforms.insert(0, transform.Rotate(360.))
+ transforms.insert(0, transform.Rotate(360.0))
self.assertSameArrays(refmatrix, transforms.matrix)
# Update translate and check for listener called
@@ -74,6 +81,7 @@ class TestTransformList(unittest.TestCase):
def listener(source):
self._callCount += 1
+
transforms.addListener(listener)
transforms[1].tx += 1
diff --git a/src/silx/gui/plot3d/scene/test/test_utils.py b/src/silx/gui/plot3d/scene/test/test_utils.py
index 65d0ce0..81f99d6 100644
--- a/src/silx/gui/plot3d/scene/test/test_utils.py
+++ b/src/silx/gui/plot3d/scene/test/test_utils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
@@ -23,14 +22,11 @@
#
# ###########################################################################*/
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "17/01/2018"
-import unittest
from silx.utils.testutils import ParametricTestCase
import numpy
@@ -40,34 +36,35 @@ from silx.gui.plot3d.scene import utils
# angleBetweenVectors #########################################################
-class TestAngleBetweenVectors(ParametricTestCase):
+class TestAngleBetweenVectors(ParametricTestCase):
TESTS = { # name: (refvector, vectors, norm, refangles)
- 'single vector':
- ((1., 0., 0.), (1., 0., 0.), (0., 0., 1.), 0.),
- 'single vector, no norm':
- ((1., 0., 0.), (1., 0., 0.), None, 0.),
-
- 'with orthogonal norm':
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- (0., 0., 1.),
- (0., 90., 180., 270.)),
-
- 'with coplanar norm': # = similar to no norm
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- (1., 0., 0.),
- (0., 90., 180., 90.)),
-
- 'without norm':
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- None,
- (0., 90., 180., 90.)),
-
- 'not unit vectors':
- ((2., 2., 0.), ((1., 1., 0.), (1., -1., 0.)), None, (0., 90.)),
+ "single vector": ((1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0), 0.0),
+ "single vector, no norm": ((1.0, 0.0, 0.0), (1.0, 0.0, 0.0), None, 0.0),
+ "with orthogonal norm": (
+ (1.0, 0.0, 0.0),
+ ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)),
+ (0.0, 0.0, 1.0),
+ (0.0, 90.0, 180.0, 270.0),
+ ),
+ "with coplanar norm": ( # = similar to no norm
+ (1.0, 0.0, 0.0),
+ ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)),
+ (1.0, 0.0, 0.0),
+ (0.0, 90.0, 180.0, 90.0),
+ ),
+ "without norm": (
+ (1.0, 0.0, 0.0),
+ ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)),
+ None,
+ (0.0, 90.0, 180.0, 90.0),
+ ),
+ "not unit vectors": (
+ (2.0, 2.0, 0.0),
+ ((1.0, 1.0, 0.0), (1.0, -1.0, 0.0)),
+ None,
+ (0.0, 90.0),
+ ),
}
def testAngleBetweenVectorsFunction(self):
@@ -81,15 +78,14 @@ class TestAngleBetweenVectors(ParametricTestCase):
if norm is not None:
norm = numpy.array(norm)
- testangles = utils.angleBetweenVectors(
- refvector, vectors, norm)
+ testangles = utils.angleBetweenVectors(refvector, vectors, norm)
- self.assertTrue(
- numpy.allclose(testangles, refangles, atol=1e-5))
+ self.assertTrue(numpy.allclose(testangles, refangles, atol=1e-5))
# Plane #######################################################################
+
class AssertNotificationContext(object):
"""Context that checks if an event.Notifier is sending events."""
@@ -121,9 +117,9 @@ class TestPlaneParameters(ParametricTestCase):
"""Test Plane.parameters read/write and notifications."""
PARAMETERS = {
- 'unit normal': (1., 0., 0., 1.),
- 'not unit normal': (1., 1., 0., 1.),
- 'd = 0': (1., 0., 0., 0.)
+ "unit normal": (1.0, 0.0, 0.0, 1.0),
+ "not unit normal": (1.0, 1.0, 0.0, 1.0),
+ "d = 0": (1.0, 0.0, 0.0, 0.0),
}
def testParameters(self):
@@ -139,12 +135,9 @@ class TestPlaneParameters(ParametricTestCase):
normparams = parameters / numpy.linalg.norm(parameters[:3])
self.assertTrue(numpy.allclose(plane.parameters, normparams))
- ZEROS_PARAMETERS = (
- (0., 0., 0., 0.),
- (0., 0., 0., 1.)
- )
+ ZEROS_PARAMETERS = ((0.0, 0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))
- ZEROS = 0., 0., 0., 0.
+ ZEROS = 0.0, 0.0, 0.0, 0.0
def testParametersNoPlane(self):
"""Test Plane.parameters with ||normal|| == 0 ."""
@@ -155,24 +148,25 @@ class TestPlaneParameters(ParametricTestCase):
with self.subTest(parameters=parameters):
with AssertNotificationContext(plane, count=0):
plane.parameters = parameters
- self.assertTrue(
- numpy.allclose(plane.parameters, self.ZEROS, 0., 0.))
+ self.assertTrue(numpy.allclose(plane.parameters, self.ZEROS, 0.0, 0.0))
# unindexArrays ###############################################################
+
class TestUnindexArrays(ParametricTestCase):
"""Test unindexArrays function."""
def testBasicModes(self):
"""Test for modes: points, lines and triangles"""
indices = numpy.array((1, 2, 0))
- arrays = (numpy.array((0., 1., 2.)),
- numpy.array(((0, 0), (1, 1), (2, 2))))
- refresults = (numpy.array((1., 2., 0.)),
- numpy.array(((1, 1), (2, 2), (0, 0))))
+ arrays = (numpy.array((0.0, 1.0, 2.0)), numpy.array(((0, 0), (1, 1), (2, 2))))
+ refresults = (
+ numpy.array((1.0, 2.0, 0.0)),
+ numpy.array(((1, 1), (2, 2), (0, 0))),
+ )
- for mode in ('points', 'lines', 'triangles'):
+ for mode in ("points", "lines", "triangles"):
with self.subTest(mode=mode):
testresults = utils.unindexArrays(mode, indices, *arrays)
for ref, test in zip(refresults, testresults):
@@ -181,15 +175,16 @@ class TestUnindexArrays(ParametricTestCase):
def testPackedLines(self):
"""Test for modes: line_strip, loop"""
indices = numpy.array((1, 2, 0))
- arrays = (numpy.array((0., 1., 2.)),
- numpy.array(((0, 0), (1, 1), (2, 2))))
+ arrays = (numpy.array((0.0, 1.0, 2.0)), numpy.array(((0, 0), (1, 1), (2, 2))))
results = {
- 'line_strip': (
- numpy.array((1., 2., 2., 0.)),
- numpy.array(((1, 1), (2, 2), (2, 2), (0, 0)))),
- 'loop': (
- numpy.array((1., 2., 2., 0., 0., 1.)),
- numpy.array(((1, 1), (2, 2), (2, 2), (0, 0), (0, 0), (1, 1)))),
+ "line_strip": (
+ numpy.array((1.0, 2.0, 2.0, 0.0)),
+ numpy.array(((1, 1), (2, 2), (2, 2), (0, 0))),
+ ),
+ "loop": (
+ numpy.array((1.0, 2.0, 2.0, 0.0, 0.0, 1.0)),
+ numpy.array(((1, 1), (2, 2), (2, 2), (0, 0), (0, 0), (1, 1))),
+ ),
}
for mode, refresults in results.items():
@@ -201,15 +196,19 @@ class TestUnindexArrays(ParametricTestCase):
def testPackedTriangles(self):
"""Test for modes: triangle_strip, fan"""
indices = numpy.array((1, 2, 0, 3))
- arrays = (numpy.array((0., 1., 2., 3.)),
- numpy.array(((0, 0), (1, 1), (2, 2), (3, 3))))
+ arrays = (
+ numpy.array((0.0, 1.0, 2.0, 3.0)),
+ numpy.array(((0, 0), (1, 1), (2, 2), (3, 3))),
+ )
results = {
- 'triangle_strip': (
- numpy.array((1., 2., 0., 2., 0., 3.)),
- numpy.array(((1, 1), (2, 2), (0, 0), (2, 2), (0, 0), (3, 3)))),
- 'fan': (
- numpy.array((1., 2., 0., 1., 0., 3.)),
- numpy.array(((1, 1), (2, 2), (0, 0), (1, 1), (0, 0), (3, 3)))),
+ "triangle_strip": (
+ numpy.array((1.0, 2.0, 0.0, 2.0, 0.0, 3.0)),
+ numpy.array(((1, 1), (2, 2), (0, 0), (2, 2), (0, 0), (3, 3))),
+ ),
+ "fan": (
+ numpy.array((1.0, 2.0, 0.0, 1.0, 0.0, 3.0)),
+ numpy.array(((1, 1), (2, 2), (0, 0), (1, 1), (0, 0), (3, 3))),
+ ),
}
for mode, refresults in results.items():
@@ -224,35 +223,49 @@ class TestUnindexArrays(ParametricTestCase):
# negative indices
with self.assertRaises(AssertionError):
- utils.unindexArrays('points', (-1, 0), *arrays)
+ utils.unindexArrays("points", (-1, 0), *arrays)
# Too high indices
with self.assertRaises(AssertionError):
- utils.unindexArrays('points', (0, 10), *arrays)
+ utils.unindexArrays("points", (0, 10), *arrays)
# triangleNormals #############################################################
+
class TestTriangleNormals(ParametricTestCase):
"""Test triangleNormals function."""
def test(self):
"""Test for modes: points, lines and triangles"""
positions = numpy.array(
- ((0., 0., 0.), (1., 0., 0.), (0., 1., 0.), # normal = Z
- (1., 1., 1.), (1., 2., 3.), (4., 5., 6.), # Random triangle
- # Degenerated triangles:
- (0., 0., 0.), (1., 0., 0.), (2., 0., 0.), # Colinear points
- (1., 1., 1.), (1., 1., 1.), (1., 1., 1.), # All same point
- ),
- dtype='float32')
+ (
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0), # normal = Z
+ (1.0, 1.0, 1.0),
+ (1.0, 2.0, 3.0),
+ (4.0, 5.0, 6.0), # Random triangle
+ # Degenerated triangles:
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (2.0, 0.0, 0.0), # Colinear points
+ (1.0, 1.0, 1.0),
+ (1.0, 1.0, 1.0),
+ (1.0, 1.0, 1.0), # All same point
+ ),
+ dtype="float32",
+ )
normals = numpy.array(
- ((0., 0., 1.),
- (-0.40824829, 0.81649658, -0.40824829),
- (0., 0., 0.),
- (0., 0., 0.)),
- dtype='float32')
+ (
+ (0.0, 0.0, 1.0),
+ (-0.40824829, 0.81649658, -0.40824829),
+ (0.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0),
+ ),
+ dtype="float32",
+ )
testnormals = utils.trianglesNormal(positions)
self.assertTrue(numpy.allclose(testnormals, normals))
diff --git a/src/silx/gui/plot3d/scene/text.py b/src/silx/gui/plot3d/scene/text.py
index bacc2e6..79cdb13 100644
--- a/src/silx/gui/plot3d/scene/text.py
+++ b/src/silx/gui/plot3d/scene/text.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""Primitive displaying a text field in the scene."""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
@@ -36,7 +33,7 @@ import numpy
from silx.gui.colors import rgba
-from ... import _glutils
+from ... import _glutils, qt
from ..._glutils import gl
from ..._glutils import font as _font
@@ -65,24 +62,18 @@ class Font(event.Notifier):
super(Font, self).__init__()
name = event.notifyProperty(
- '_name',
- doc="""Name of the font (str)""",
- converter=str)
+ "_name", doc="""Name of the font (str)""", converter=str
+ )
size = event.notifyProperty(
- '_size',
- doc="""Font size in points (int)""",
- converter=int)
+ "_size", doc="""Font size in points (int)""", converter=int
+ )
- weight = event.notifyProperty(
- '_weight',
- doc="""Font size in points (int)""",
- converter=int)
+ weight = event.notifyProperty("_weight", doc="""Font weight (int)""", converter=int)
italic = event.notifyProperty(
- '_italic',
- doc="""True for italic (bool)""",
- converter=bool)
+ "_italic", doc="""True for italic (bool)""", converter=bool
+ )
class Text2D(primitives.Geometry):
@@ -93,14 +84,14 @@ class Text2D(primitives.Geometry):
"""
# Text anchor values
- CENTER = 'center'
+ CENTER = "center"
- LEFT = 'left'
- RIGHT = 'right'
+ LEFT = "left"
+ RIGHT = "right"
- TOP = 'top'
- BASELINE = 'baseline'
- BOTTOM = 'bottom'
+ TOP = "top"
+ BASELINE = "baseline"
+ BOTTOM = "bottom"
_ALIGN = LEFT, CENTER, RIGHT
_VALIGN = TOP, BASELINE, CENTER, BOTTOM
@@ -109,30 +100,31 @@ class Text2D(primitives.Geometry):
"""Internal cache storing already rasterized text"""
# TODO limit cache size and discard least recent used
- def __init__(self, text='', font=None):
+ def __init__(self, text="", font=None):
self._dirtyTexture = True
self._dirtyAlign = True
self._baselineOffset = 0
self._text = text
self._font = font if font is not None else Font()
- self._foreground = 1., 1., 1., 1.
- self._background = 0., 0., 0., 0.
+ self._foreground = 1.0, 1.0, 1.0, 1.0
+ self._background = 0.0, 0.0, 0.0, 0.0
self._overlay = False
- self._align = 'left'
- self._valign = 'baseline'
- self._devicePixelRatio = 1.0 # Store it to check for changes
+ self._align = "left"
+ self._valign = "baseline"
+ self._dotsPerInch = 96.0 # Store it to check for changes
self._texture = None
self._textureDirty = True
super(Text2D, self).__init__(
- 'triangle_strip',
+ "triangle_strip",
copy=False,
# Keep an array for position as it is bound to attr 0 and MUST
# be active and an array at least on Mac OS X
position=numpy.zeros((4, 3), dtype=numpy.float32),
- vertexID=numpy.arange(4., dtype=numpy.float32).reshape(4, 1),
- offsetInViewportCoords=(0., 0.))
+ vertexID=numpy.arange(4.0, dtype=numpy.float32).reshape(4, 1),
+ offsetInViewportCoords=(0.0, 0.0),
+ )
@property
def text(self):
@@ -165,18 +157,22 @@ class Text2D(primitives.Geometry):
self.notify()
foreground = event.notifyProperty(
- '_foreground', doc="""RGBA color of the text: 4 float in [0, 1]""",
- converter=rgba)
+ "_foreground",
+ doc="""RGBA color of the text: 4 float in [0, 1]""",
+ converter=rgba,
+ )
background = event.notifyProperty(
- '_background',
+ "_background",
doc="RGBA background color of the text field: 4 float in [0, 1]",
- converter=rgba)
+ converter=rgba,
+ )
overlay = event.notifyProperty(
- '_overlay',
+ "_overlay",
doc="True to always display text on top of the scene (default: False)",
- converter=bool)
+ converter=bool,
+ )
def _setAlign(self, align):
assert align in self._ALIGN
@@ -189,7 +185,8 @@ class Text2D(primitives.Geometry):
_setAlign,
doc="""Horizontal anchor position of the text field (str).
- Either 'left' (default), 'center' or 'right'.""")
+ Either 'left' (default), 'center' or 'right'.""",
+ )
def _setVAlign(self, valign):
assert valign in self._VALIGN
@@ -202,37 +199,45 @@ class Text2D(primitives.Geometry):
_setVAlign,
doc="""Vertical anchor position of the text field (str).
- Either 'top', 'baseline' (default), 'center' or 'bottom'""")
+ Either 'top', 'baseline' (default), 'center' or 'bottom'""",
+ )
- def _raster(self, devicePixelRatio):
+ def _raster(self, dotsPerInch: float):
"""Raster current primitive to a bitmap
- :param float devicePixelRatio:
- The ratio between device and device-independent pixels
+ :param dotsPerInch: Screen resolution in pixels per inch
:return: Corresponding image in grayscale and baseline offset from top
:rtype: (HxW numpy.ndarray of uint8, int)
"""
- params = (self.text,
- self.font.name,
- self.font.size,
- self.font.weight,
- self.font.italic,
- devicePixelRatio)
-
- if params not in self._rasterTextCache: # Add to cache
- self._rasterTextCache[params] = _font.rasterText(*params)
-
- array, offset = self._rasterTextCache[params]
+ key = (
+ self.text,
+ self.font.name,
+ self.font.size,
+ self.font.weight,
+ self.font.italic,
+ dotsPerInch,
+ )
+
+ if key not in self._rasterTextCache: # Add to cache
+ font = qt.QFont(
+ self.font.name,
+ self.font.size,
+ self.font.weight,
+ self.font.italic,
+ )
+ self._rasterTextCache[key] = _font.rasterText(self.text, font, dotsPerInch)
+
+ array, offset = self._rasterTextCache[key]
return array.copy(), offset
def _bounds(self, dataBounds=False):
return None
def prepareGL2(self, context):
- # Check if devicePixelRatio has changed since last rendering
- devicePixelRatio = context.glCtx.devicePixelRatio
- if self._devicePixelRatio != devicePixelRatio:
- self._devicePixelRatio = devicePixelRatio
+ # Check if dotsPerInch has changed since last rendering
+ dotsPerInch = context.glCtx.dotsPerInch
+ if self._dotsPerInch != dotsPerInch:
+ self._dotsPerInch = dotsPerInch
self._dirtyTexture = True
if self._dirtyTexture:
@@ -244,13 +249,15 @@ class Text2D(primitives.Geometry):
self._baselineOffset = 0
if self.text:
- image, self._baselineOffset = self._raster(
- self._devicePixelRatio)
+ image, self._baselineOffset = self._raster(dotsPerInch)
self._texture = _glutils.Texture(
- gl.GL_R8, image, gl.GL_RED,
+ gl.GL_R8,
+ image,
+ gl.GL_RED,
minFilter=gl.GL_NEAREST,
magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
self._texture.prepare()
self._dirtyAlign = True # To force update of offset
@@ -260,32 +267,33 @@ class Text2D(primitives.Geometry):
if self._texture is not None:
height, width = self._texture.shape
- if self._align == 'left':
- ox = 0.
- elif self._align == 'center':
- ox = - width // 2
- elif self._align == 'right':
- ox = - width
+ if self._align == "left":
+ ox = 0.0
+ elif self._align == "center":
+ ox = -width // 2
+ elif self._align == "right":
+ ox = -width
else:
_logger.error("Unsupported align: %s", self._align)
- ox = 0.
+ ox = 0.0
- if self._valign == 'top':
- oy = 0.
- elif self._valign == 'baseline':
+ if self._valign == "top":
+ oy = 0.0
+ elif self._valign == "baseline":
oy = self._baselineOffset
- elif self._valign == 'center':
+ elif self._valign == "center":
oy = height // 2
- elif self._valign == 'bottom':
+ elif self._valign == "bottom":
oy = height
else:
_logger.error("Unsupported valign: %s", self._valign)
- oy = 0.
+ oy = 0.0
offsets = (ox, oy) + numpy.array(
- ((0., 0.), (width, 0.), (0., -height), (width, -height)),
- dtype=numpy.float32)
- self.setAttribute('offsetInViewportCoords', offsets)
+ ((0.0, 0.0), (width, 0.0), (0.0, -height), (width, -height)),
+ dtype=numpy.float32,
+ )
+ self.setAttribute("offsetInViewportCoords", offsets)
super(Text2D, self).prepareGL2(context)
@@ -296,14 +304,12 @@ class Text2D(primitives.Geometry):
program = context.glCtx.prog(*self._shaders)
program.use()
- program.setUniformMatrix('matrix', context.objectToNDC.matrix)
- gl.glUniform2f(
- program.uniforms['viewportSize'], *context.viewport.size)
- gl.glUniform4f(program.uniforms['foreground'], *self.foreground)
- gl.glUniform4f(program.uniforms['background'], *self.background)
- gl.glUniform1i(program.uniforms['texture'], self._texture.texUnit)
- gl.glUniform1i(program.uniforms['isOverlay'],
- 1 if self._overlay else 0)
+ program.setUniformMatrix("matrix", context.objectToNDC.matrix)
+ gl.glUniform2f(program.uniforms["viewportSize"], *context.viewport.size)
+ gl.glUniform4f(program.uniforms["foreground"], *self.foreground)
+ gl.glUniform4f(program.uniforms["background"], *self.background)
+ gl.glUniform1i(program.uniforms["texture"], self._texture.texUnit)
+ gl.glUniform1i(program.uniforms["isOverlay"], 1 if self._overlay else 0)
self._texture.bind()
@@ -354,7 +360,6 @@ class Text2D(primitives.Geometry):
vertexID < 1.5 ? 0.0 : 1.0);
}
""", # noqa
-
"""
varying vec2 texCoords;
@@ -376,12 +381,12 @@ class Text2D(primitives.Geometry):
}
}
}
- """)
+ """,
+ )
class LabelledAxes(primitives.GroupBBox):
- """A group displaying a bounding box with axes labels around its children.
- """
+ """A group displaying a bounding box with axes labels around its children."""
def __init__(self):
super(LabelledAxes, self).__init__()
@@ -392,26 +397,23 @@ class LabelledAxes(primitives.GroupBBox):
# TODO offset labels from anchor in pixels
self._xlabel = Text2D(font=self._font)
- self._xlabel.align = 'center'
- self._xlabel.transforms = [self._boxTransforms,
- transform.Translate(tx=0.5)]
+ self._xlabel.align = "center"
+ self._xlabel.transforms = [self._boxTransforms, transform.Translate(tx=0.5)]
self._children.append(self._xlabel)
self._ylabel = Text2D(font=self._font)
- self._ylabel.align = 'center'
- self._ylabel.transforms = [self._boxTransforms,
- transform.Translate(ty=0.5)]
+ self._ylabel.align = "center"
+ self._ylabel.transforms = [self._boxTransforms, transform.Translate(ty=0.5)]
self._children.append(self._ylabel)
self._zlabel = Text2D(font=self._font)
- self._zlabel.align = 'center'
- self._zlabel.transforms = [self._boxTransforms,
- transform.Translate(tz=0.5)]
+ self._zlabel.align = "center"
+ self._zlabel.transforms = [self._boxTransforms, transform.Translate(tz=0.5)]
self._children.append(self._zlabel)
self._tickLines = primitives.Lines( # Init tick lines with dummy pos
- positions=((0., 0., 0.), (0., 0., 0.)),
- mode='lines')
+ positions=((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)), mode="lines"
+ )
self._tickLines.visible = False
self._children.append(self._tickLines)
@@ -468,13 +470,14 @@ class LabelledAxes(primitives.GroupBBox):
self._tickLines.visible = False
self._tickLabels.children = [] # Reset previous labels
- elif (self._ticksForBounds is None or
- not numpy.all(numpy.equal(bounds, self._ticksForBounds))):
+ elif self._ticksForBounds is None or not numpy.all(
+ numpy.equal(bounds, self._ticksForBounds)
+ ):
self._ticksForBounds = bounds
# Update ticks
# TODO make ticks having a constant length on the screen
- ticklength = numpy.abs(bounds[1] - bounds[0]) / 20.
+ ticklength = numpy.abs(bounds[1] - bounds[0]) / 20.0
xticks, xlabels = ticklayout.ticks(*bounds[:, 0])
yticks, ylabels = ticklayout.ticks(*bounds[:, 1])
@@ -482,26 +485,26 @@ class LabelledAxes(primitives.GroupBBox):
# Update tick lines
coords = numpy.empty(
- ((len(xticks) + len(yticks) + len(zticks)), 4, 3),
- dtype=numpy.float32)
+ ((len(xticks) + len(yticks) + len(zticks)), 4, 3), dtype=numpy.float32
+ )
coords[:, :, :] = bounds[0, :] # account for offset from origin
- xcoords = coords[:len(xticks)]
+ xcoords = coords[: len(xticks)]
xcoords[:, :, 0] = numpy.asarray(xticks)[:, numpy.newaxis]
xcoords[:, 1, 1] += ticklength[1] # X ticks on XY plane
xcoords[:, 3, 2] += ticklength[2] # X ticks on XZ plane
- ycoords = coords[len(xticks):len(xticks) + len(yticks)]
+ ycoords = coords[len(xticks) : len(xticks) + len(yticks)]
ycoords[:, :, 1] = numpy.asarray(yticks)[:, numpy.newaxis]
ycoords[:, 1, 0] += ticklength[0] # Y ticks on XY plane
ycoords[:, 3, 2] += ticklength[2] # Y ticks on YZ plane
- zcoords = coords[len(xticks) + len(yticks):]
+ zcoords = coords[len(xticks) + len(yticks) :]
zcoords[:, :, 2] = numpy.asarray(zticks)[:, numpy.newaxis]
zcoords[:, 1, 0] += ticklength[0] # Z ticks on XZ plane
zcoords[:, 3, 1] += ticklength[1] # Z ticks on YZ plane
- self._tickLines.setAttribute('position', coords.reshape(-1, 3))
+ self._tickLines.setAttribute("position", coords.reshape(-1, 3))
self._tickLines.visible = True
# Update labels
@@ -509,23 +512,26 @@ class LabelledAxes(primitives.GroupBBox):
labels = []
for tick, label in zip(xticks, xlabels):
text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=tick, ty=offsets[1], tz=offsets[2])]
+ text.align = "center"
+ text.transforms = [
+ transform.Translate(tx=tick, ty=offsets[1], tz=offsets[2])
+ ]
labels.append(text)
for tick, label in zip(yticks, ylabels):
text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=offsets[0], ty=tick, tz=offsets[2])]
+ text.align = "center"
+ text.transforms = [
+ transform.Translate(tx=offsets[0], ty=tick, tz=offsets[2])
+ ]
labels.append(text)
for tick, label in zip(zticks, zlabels):
text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=offsets[0], ty=offsets[1], tz=tick)]
+ text.align = "center"
+ text.transforms = [
+ transform.Translate(tx=offsets[0], ty=offsets[1], tz=tick)
+ ]
labels.append(text)
self._tickLabels.children = labels # Reset previous labels
diff --git a/src/silx/gui/plot3d/scene/transform.py b/src/silx/gui/plot3d/scene/transform.py
index 43b739b..20e2453 100644
--- a/src/silx/gui/plot3d/scene/transform.py
+++ b/src/silx/gui/plot3d/scene/transform.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2020 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module provides 4x4 matrix operation and classes to handle them."""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "25/07/2016"
@@ -41,6 +38,7 @@ from . import event
# Projections
+
def mat4LookAtDir(position, direction, up):
"""Creates matrix to look in direction from position.
@@ -57,24 +55,22 @@ def mat4LookAtDir(position, direction, up):
direction = numpy.array(direction, copy=True, dtype=numpy.float32)
dirnorm = numpy.linalg.norm(direction)
- assert dirnorm != 0.
+ assert dirnorm != 0.0
direction /= dirnorm
- side = numpy.cross(direction,
- numpy.array(up, copy=False, dtype=numpy.float32))
+ side = numpy.cross(direction, numpy.array(up, copy=False, dtype=numpy.float32))
sidenorm = numpy.linalg.norm(side)
- assert sidenorm != 0.
+ assert sidenorm != 0.0
up = numpy.cross(side / sidenorm, direction)
upnorm = numpy.linalg.norm(up)
- assert upnorm != 0.
+ assert upnorm != 0.0
up /= upnorm
matrix = numpy.identity(4, dtype=numpy.float32)
matrix[0, :3] = side
matrix[1, :3] = up
matrix[2, :3] = -direction
- return numpy.dot(matrix,
- mat4Translate(-position[0], -position[1], -position[2]))
+ return numpy.dot(matrix, mat4Translate(-position[0], -position[1], -position[2]))
def mat4LookAt(position, center, up):
@@ -100,11 +96,15 @@ def mat4Frustum(left, right, bottom, top, near, far):
See glFrustum.
"""
- return numpy.array((
- (2.*near / (right-left), 0., (right+left) / (right-left), 0.),
- (0., 2.*near / (top-bottom), (top+bottom) / (top-bottom), 0.),
- (0., 0., -(far+near) / (far-near), -2.*far*near / (far-near)),
- (0., 0., -1., 0.)), dtype=numpy.float32)
+ return numpy.array(
+ (
+ (2.0 * near / (right - left), 0.0, (right + left) / (right - left), 0.0),
+ (0.0, 2.0 * near / (top - bottom), (top + bottom) / (top - bottom), 0.0),
+ (0.0, 0.0, -(far + near) / (far - near), -2.0 * far * near / (far - near)),
+ (0.0, 0.0, -1.0, 0.0),
+ ),
+ dtype=numpy.float32,
+ )
def mat4Perspective(fovy, width, height, near, far):
@@ -123,15 +123,19 @@ def mat4Perspective(fovy, width, height, near, far):
assert fovy != 0
assert height != 0
assert width != 0
- assert near > 0.
+ assert near > 0.0
assert far > near
aspectratio = width / height
- f = 1. / numpy.tan(numpy.radians(fovy) / 2.)
- return numpy.array((
- (f / aspectratio, 0., 0., 0.),
- (0., f, 0., 0.),
- (0., 0., (far + near) / (near - far), 2. * far * near / (near - far)),
- (0., 0., -1., 0.)), dtype=numpy.float32)
+ f = 1.0 / numpy.tan(numpy.radians(fovy) / 2.0)
+ return numpy.array(
+ (
+ (f / aspectratio, 0.0, 0.0, 0.0),
+ (0.0, f, 0.0, 0.0),
+ (0.0, 0.0, (far + near) / (near - far), 2.0 * far * near / (near - far)),
+ (0.0, 0.0, -1.0, 0.0),
+ ),
+ dtype=numpy.float32,
+ )
def mat4Orthographic(left, right, bottom, top, near, far):
@@ -139,34 +143,47 @@ def mat4Orthographic(left, right, bottom, top, near, far):
See glOrtho.
"""
- return numpy.array((
- (2. / (right - left), 0., 0., - (right + left) / (right - left)),
- (0., 2. / (top - bottom), 0., - (top + bottom) / (top - bottom)),
- (0., 0., -2. / (far - near), - (far + near) / (far - near)),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ return numpy.array(
+ (
+ (2.0 / (right - left), 0.0, 0.0, -(right + left) / (right - left)),
+ (0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / (top - bottom)),
+ (0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near)),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
# Affine
+
def mat4Translate(tx, ty, tz):
"""4x4 translation matrix."""
- return numpy.array((
- (1., 0., 0., tx),
- (0., 1., 0., ty),
- (0., 0., 1., tz),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ return numpy.array(
+ (
+ (1.0, 0.0, 0.0, tx),
+ (0.0, 1.0, 0.0, ty),
+ (0.0, 0.0, 1.0, tz),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
def mat4Scale(sx, sy, sz):
"""4x4 scale matrix."""
- return numpy.array((
- (sx, 0., 0., 0.),
- (0., sy, 0., 0.),
- (0., 0., sz, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-def mat4RotateFromAngleAxis(angle, x=0., y=0., z=1.):
+ return numpy.array(
+ (
+ (sx, 0.0, 0.0, 0.0),
+ (0.0, sy, 0.0, 0.0),
+ (0.0, 0.0, sz, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+
+def mat4RotateFromAngleAxis(angle, x=0.0, y=0.0, z=1.0):
"""4x4 rotation matrix from angle and axis.
:param float angle: The rotation angle in radians.
@@ -176,11 +193,30 @@ def mat4RotateFromAngleAxis(angle, x=0., y=0., z=1.):
"""
ca = numpy.cos(angle)
sa = numpy.sin(angle)
- return numpy.array((
- ((1.-ca) * x*x + ca, (1.-ca) * x*y - sa*z, (1.-ca) * x*z + sa*y, 0.),
- ((1.-ca) * x*y + sa*z, (1.-ca) * y*y + ca, (1.-ca) * y*z - sa*x, 0.),
- ((1.-ca) * x*z - sa*y, (1.-ca) * y*z + sa*x, (1.-ca) * z*z + ca, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ return numpy.array(
+ (
+ (
+ (1.0 - ca) * x * x + ca,
+ (1.0 - ca) * x * y - sa * z,
+ (1.0 - ca) * x * z + sa * y,
+ 0.0,
+ ),
+ (
+ (1.0 - ca) * x * y + sa * z,
+ (1.0 - ca) * y * y + ca,
+ (1.0 - ca) * y * z - sa * x,
+ 0.0,
+ ),
+ (
+ (1.0 - ca) * x * z - sa * y,
+ (1.0 - ca) * y * z + sa * x,
+ (1.0 - ca) * z * z + ca,
+ 0.0,
+ ),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
def mat4RotateFromQuaternion(quaternion):
@@ -192,14 +228,33 @@ def mat4RotateFromQuaternion(quaternion):
quaternion /= numpy.linalg.norm(quaternion)
qx, qy, qz, qw = quaternion
- return numpy.array((
- (1. - 2.*(qy**2 + qz**2), 2.*(qx*qy - qw*qz), 2.*(qx*qz + qw*qy), 0.),
- (2.*(qx*qy + qw*qz), 1. - 2.*(qx**2 + qz**2), 2.*(qy*qz - qw*qx), 0.),
- (2.*(qx*qz - qw*qy), 2.*(qy*qz + qw*qx), 1. - 2.*(qx**2 + qy**2), 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-def mat4Shear(axis, sx=0., sy=0., sz=0.):
+ return numpy.array(
+ (
+ (
+ 1.0 - 2.0 * (qy**2 + qz**2),
+ 2.0 * (qx * qy - qw * qz),
+ 2.0 * (qx * qz + qw * qy),
+ 0.0,
+ ),
+ (
+ 2.0 * (qx * qy + qw * qz),
+ 1.0 - 2.0 * (qx**2 + qz**2),
+ 2.0 * (qy * qz - qw * qx),
+ 0.0,
+ ),
+ (
+ 2.0 * (qx * qz - qw * qy),
+ 2.0 * (qy * qz + qw * qx),
+ 1.0 - 2.0 * (qx**2 + qy**2),
+ 0.0,
+ ),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+
+def mat4Shear(axis, sx=0.0, sy=0.0, sz=0.0):
"""4x4 shear matrix: Skew two axes relative to a third fixed one.
shearFactor = tan(shearAngle)
@@ -210,22 +265,22 @@ def mat4Shear(axis, sx=0., sy=0., sz=0.):
:param float sy: The shear factor for the Y axis relative to axis.
:param float sz: The shear factor for the Z axis relative to axis.
"""
- assert axis in ('x', 'y', 'z')
+ assert axis in ("x", "y", "z")
matrix = numpy.identity(4, dtype=numpy.float32)
# Make the shear column
- index = 'xyz'.find(axis)
- shearcolumn = numpy.array((sx, sy, sz, 0.), dtype=numpy.float32)
- shearcolumn[index] = 1.
+ index = "xyz".find(axis)
+ shearcolumn = numpy.array((sx, sy, sz, 0.0), dtype=numpy.float32)
+ shearcolumn[index] = 1.0
matrix[:, index] = shearcolumn
return matrix
# Transforms ##################################################################
-class Transform(event.Notifier):
+class Transform(event.Notifier):
def __init__(self, static=False):
"""Base class for (row-major) 4x4 matrix transforms.
@@ -239,8 +294,7 @@ class Transform(event.Notifier):
self.addListener(self._changed) # Listening self for changes
def __repr__(self):
- return '%s(%s)' % (self.__class__.__init__,
- repr(self.getMatrix(copy=False)))
+ return "%s(%s)" % (self.__class__.__init__, repr(self.getMatrix(copy=False)))
def inverse(self):
"""Return the Transform of the inverse.
@@ -293,8 +347,8 @@ class Transform(event.Notifier):
return self._inverse
inverseMatrix = property(
- getInverseMatrix,
- doc="The 4x4 matrix of the inverse of this transform.")
+ getInverseMatrix, doc="The 4x4 matrix of the inverse of this transform."
+ )
# Listener
@@ -331,14 +385,13 @@ class Transform(event.Notifier):
if dimension == 3: # Add 4th coordinate
points = numpy.append(
- points,
- numpy.ones((1, points.shape[1]), dtype=points.dtype),
- axis=0)
+ points, numpy.ones((1, points.shape[1]), dtype=points.dtype), axis=0
+ )
result = numpy.transpose(numpy.dot(matrix, points))
if perspectiveDivide:
- mask = result[:, 3] != 0.
+ mask = result[:, 3] != 0.0
result[mask] /= result[mask, 3][:, numpy.newaxis]
return result[:, :3] if dimension == 3 else result
@@ -367,9 +420,9 @@ class Transform(event.Notifier):
matrix = self.getMatrix(copy=False)
else:
matrix = self.getInverseMatrix(copy=False)
- result = numpy.dot(matrix, self._prepareVector(point, 1.))
+ result = numpy.dot(matrix, self._prepareVector(point, 1.0))
- if perspectiveDivide and result[3] != 0.:
+ if perspectiveDivide and result[3] != 0.0:
result /= result[3]
if len(point) == 3:
@@ -407,8 +460,9 @@ class Transform(event.Notifier):
matrix = self.getMatrix(copy=False).T
return numpy.dot(matrix[:3, :3], normal[:3])
- _CUBE_CORNERS = numpy.array(list(itertools.product((0., 1.), repeat=3)),
- dtype=numpy.float32)
+ _CUBE_CORNERS = numpy.array(
+ list(itertools.product((0.0, 1.0), repeat=3)), dtype=numpy.float32
+ )
"""Unit cube corners used by :meth:`transformBounds`"""
def transformBounds(self, bounds, direct=True):
@@ -422,8 +476,7 @@ class Transform(event.Notifier):
:rtype: 2x3 numpy.ndarray of float32
"""
corners = numpy.ones((8, 4), dtype=numpy.float32)
- corners[:, :3] = bounds[0] + \
- self._CUBE_CORNERS * (bounds[1] - bounds[0])
+ corners[:, :3] = bounds[0] + self._CUBE_CORNERS * (bounds[1] - bounds[0])
if direct:
matrix = self.getMatrix(copy=False)
@@ -505,8 +558,8 @@ class StaticTransformList(Transform):
# Affine ######################################################################
-class Matrix(Transform):
+class Matrix(Transform):
def __init__(self, matrix=None):
"""4x4 Matrix.
@@ -531,16 +584,17 @@ class Matrix(Transform):
self.notify()
# Redefined here to add a setter
- matrix = property(Transform.getMatrix, setMatrix,
- doc="The 4x4 matrix of this transform.")
+ matrix = property(
+ Transform.getMatrix, setMatrix, doc="The 4x4 matrix of this transform."
+ )
class Translate(Transform):
"""4x4 translation matrix."""
- def __init__(self, tx=0., ty=0., tz=0.):
+ def __init__(self, tx=0.0, ty=0.0, tz=0.0):
super(Translate, self).__init__()
- self._tx, self._ty, self._tz = 0., 0., 0.
+ self._tx, self._ty, self._tz = 0.0, 0.0, 0.0
self.setTranslate(tx, ty, tz)
def _makeMatrix(self):
@@ -595,16 +649,16 @@ class Translate(Transform):
class Scale(Transform):
"""4x4 scale matrix."""
- def __init__(self, sx=1., sy=1., sz=1.):
+ def __init__(self, sx=1.0, sy=1.0, sz=1.0):
super(Scale, self).__init__()
- self._sx, self._sy, self._sz = 0., 0., 0.
+ self._sx, self._sy, self._sz = 0.0, 0.0, 0.0
self.setScale(sx, sy, sz)
def _makeMatrix(self):
return mat4Scale(self.sx, self.sy, self.sz)
def _makeInverse(self):
- return mat4Scale(1. / self.sx, 1. / self.sy, 1. / self.sz)
+ return mat4Scale(1.0 / self.sx, 1.0 / self.sy, 1.0 / self.sz)
@property
def sx(self):
@@ -641,20 +695,19 @@ class Scale(Transform):
def setScale(self, sx=None, sy=None, sz=None):
if sx is not None:
- assert sx != 0.
+ assert sx != 0.0
self._sx = sx
if sy is not None:
- assert sy != 0.
+ assert sy != 0.0
self._sy = sy
if sz is not None:
- assert sz != 0.
+ assert sz != 0.0
self._sz = sz
self.notify()
class Rotate(Transform):
-
- def __init__(self, angle=0., ax=0., ay=0., az=1.):
+ def __init__(self, angle=0.0, ax=0.0, ay=0.0, az=1.0):
"""4x4 rotation matrix.
:param float angle: The rotation angle in degrees.
@@ -663,7 +716,7 @@ class Rotate(Transform):
:param float az: The z coordinate of the rotation axis.
"""
super(Rotate, self).__init__()
- self._angle = 0.
+ self._angle = 0.0
self._axis = None
self.setAngleAxis(angle, (ax, ay, az))
@@ -698,9 +751,9 @@ class Rotate(Transform):
axis = numpy.array(axis, copy=True, dtype=numpy.float32)
assert axis.size == 3
norm = numpy.linalg.norm(axis)
- if norm == 0.: # No axis, set rotation angle to 0.
- self._angle = 0.
- self._axis = numpy.array((0., 0., 1.), dtype=numpy.float32)
+ if norm == 0.0: # No axis, set rotation angle to 0.
+ self._angle = 0.0
+ self._axis = numpy.array((0.0, 0.0, 1.0), dtype=numpy.float32)
else:
self._axis = axis / norm
@@ -713,8 +766,8 @@ class Rotate(Transform):
Where: ||(x, y, z)|| = sin(angle/2), w = cos(angle/2).
"""
- if numpy.linalg.norm(self._axis) == 0.:
- return numpy.array((0., 0., 0., 1.), dtype=numpy.float32)
+ if numpy.linalg.norm(self._axis) == 0.0:
+ return numpy.array((0.0, 0.0, 0.0, 1.0), dtype=numpy.float32)
else:
quaternion = numpy.empty((4,), dtype=numpy.float32)
@@ -734,7 +787,7 @@ class Rotate(Transform):
# Get angle
sinhalfangle = numpy.linalg.norm(quaternion[0:3])
coshalfangle = quaternion[3]
- angle = 2. * numpy.arctan2(sinhalfangle, coshalfangle)
+ angle = 2.0 * numpy.arctan2(sinhalfangle, coshalfangle)
# Axis will be normalized in setAngleAxis
self.setAngleAxis(numpy.degrees(angle), quaternion[0:3])
@@ -744,14 +797,16 @@ class Rotate(Transform):
return mat4RotateFromAngleAxis(angle, *self.axis)
def _makeInverse(self):
- return numpy.array(self.getMatrix(copy=False).transpose(),
- copy=True, order='C',
- dtype=numpy.float32)
+ return numpy.array(
+ self.getMatrix(copy=False).transpose(),
+ copy=True,
+ order="C",
+ dtype=numpy.float32,
+ )
class Shear(Transform):
-
- def __init__(self, axis, sx=0., sy=0., sz=0.):
+ def __init__(self, axis, sx=0.0, sy=0.0, sz=0.0):
"""4x4 shear/skew matrix of 2 axes relative to the third one.
:param str axis: The axis to keep fixed, in 'x', 'y', 'z'
@@ -759,7 +814,7 @@ class Shear(Transform):
:param float sy: The shear factor for the y axis.
:param float sz: The shear factor for the z axis.
"""
- assert axis in ('x', 'y', 'z')
+ assert axis in ("x", "y", "z")
super(Shear, self).__init__()
self._axis = axis
self._factors = sx, sy, sz
@@ -784,6 +839,7 @@ class Shear(Transform):
# Projection ##################################################################
+
class _Projection(Transform):
"""Base class for projection matrix.
@@ -798,12 +854,12 @@ class _Projection(Transform):
:type size: 2-tuple of float
"""
- def __init__(self, near, far, checkDepthExtent=False, size=(1., 1.)):
+ def __init__(self, near, far, checkDepthExtent=False, size=(1.0, 1.0)):
super(_Projection, self).__init__()
self._checkDepthExtent = checkDepthExtent
self._depthExtent = 1, 10
self.setDepthExtent(near, far) # set _depthExtent
- self._size = 1., 1.
+ self._size = 1.0, 1.0
self.size = size # set _size
def setDepthExtent(self, near=None, far=None):
@@ -816,7 +872,7 @@ class _Projection(Transform):
far = float(far) if far is not None else self._depthExtent[1]
if self._checkDepthExtent:
- assert near > 0.
+ assert near > 0.0
assert far > near
self._depthExtent = near, far
@@ -877,18 +933,27 @@ class Orthographic(_Projection):
True (default) to keep aspect ratio, False otherwise.
"""
- def __init__(self, left=0., right=1., bottom=1., top=0., near=-1., far=1.,
- size=(1., 1.), keepaspect=True):
+ def __init__(
+ self,
+ left=0.0,
+ right=1.0,
+ bottom=1.0,
+ top=0.0,
+ near=-1.0,
+ far=1.0,
+ size=(1.0, 1.0),
+ keepaspect=True,
+ ):
self._left, self._right = left, right
self._bottom, self._top = bottom, top
self._keepaspect = bool(keepaspect)
- super(Orthographic, self).__init__(near, far, checkDepthExtent=False,
- size=size)
+ super(Orthographic, self).__init__(near, far, checkDepthExtent=False, size=size)
# _update called when setting size
def _makeMatrix(self):
return mat4Orthographic(
- self.left, self.right, self.bottom, self.top, self.near, self.far)
+ self.left, self.right, self.bottom, self.top, self.near, self.far
+ )
def _update(self, left, right, bottom, top):
if self.keepaspect:
@@ -898,14 +963,12 @@ class Orthographic(_Projection):
orthoaspect = abs(left - right) / abs(bottom - top)
if orthoaspect >= aspect: # Keep width, enlarge height
- newheight = \
- numpy.sign(top - bottom) * abs(left - right) / aspect
+ newheight = numpy.sign(top - bottom) * abs(left - right) / aspect
bottom = 0.5 * (bottom + top) - 0.5 * newheight
top = bottom + newheight
else: # Keep height, enlarge width
- newwidth = \
- numpy.sign(right - left) * abs(bottom - top) * aspect
+ newwidth = numpy.sign(right - left) * abs(bottom - top) * aspect
left = 0.5 * (left + right) - 0.5 * newwidth
right = left + newwidth
@@ -932,17 +995,15 @@ class Orthographic(_Projection):
self._update(left, right, bottom, top)
self.notify()
- left = property(lambda self: self._left,
- doc="Coord of the left clipping plane.")
+ left = property(lambda self: self._left, doc="Coord of the left clipping plane.")
- right = property(lambda self: self._right,
- doc="Coord of the right clipping plane.")
+ right = property(lambda self: self._right, doc="Coord of the right clipping plane.")
- bottom = property(lambda self: self._bottom,
- doc="Coord of the bottom clipping plane.")
+ bottom = property(
+ lambda self: self._bottom, doc="Coord of the bottom clipping plane."
+ )
- top = property(lambda self: self._top,
- doc="Coord of the top clipping plane.")
+ top = property(lambda self: self._top, doc="Coord of the top clipping plane.")
@property
def size(self):
@@ -985,13 +1046,12 @@ class Ortho2DWidget(_Projection):
:type size: 2-tuple of float
"""
- def __init__(self, near=-1., far=1., size=(1., 1.)):
-
+ def __init__(self, near=-1.0, far=1.0, size=(1.0, 1.0)):
super(Ortho2DWidget, self).__init__(near, far, size)
def _makeMatrix(self):
width, height = self.size
- return mat4Orthographic(0., width, height, 0., self.near, self.far)
+ return mat4Orthographic(0.0, width, height, 0.0, self.near, self.far)
class Perspective(_Projection):
@@ -1005,10 +1065,9 @@ class Perspective(_Projection):
:type size: 2-tuple of float
"""
- def __init__(self, fovy=90., near=0.1, far=1., size=(1., 1.)):
-
+ def __init__(self, fovy=90.0, near=0.1, far=1.0, size=(1.0, 1.0)):
super(Perspective, self).__init__(near, far, checkDepthExtent=True)
- self._fovy = 90.
+ self._fovy = 90.0
self.fovy = fovy # Set _fovy
self.size = size # Set _ size
diff --git a/src/silx/gui/plot3d/scene/utils.py b/src/silx/gui/plot3d/scene/utils.py
index c6cd129..c856f15 100644
--- a/src/silx/gui/plot3d/scene/utils.py
+++ b/src/silx/gui/plot3d/scene/utils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2020 European Synchrotron Radiation Facility
@@ -27,8 +26,6 @@ This module provides functions to generate indices, to check intersection
and to handle planes.
"""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "25/07/2016"
@@ -45,6 +42,7 @@ _logger = logging.getLogger(__name__)
# numpy #######################################################################
+
def _uniqueAlongLastAxis(a):
"""Numpy unique on the last axis of a 2D array
@@ -60,12 +58,12 @@ def _uniqueAlongLastAxis(a):
assert len(a.shape) == 2
# Construct a type over last array dimension to run unique on a 1D array
- if a.dtype.char in numpy.typecodes['AllInteger']:
+ if a.dtype.char in numpy.typecodes["AllInteger"]:
# Bit-wise comparison of the 2 indices of a line at once
# Expect a C contiguous array of shape N, 2
uniquedt = numpy.dtype((numpy.void, a.itemsize * a.shape[-1]))
- elif a.dtype.char in numpy.typecodes['Float']:
- uniquedt = [('f{i}'.format(i=i), a.dtype) for i in range(a.shape[-1])]
+ elif a.dtype.char in numpy.typecodes["Float"]:
+ uniquedt = [("f{i}".format(i=i), a.dtype) for i in range(a.shape[-1])]
else:
raise TypeError("Unsupported type {dtype}".format(dtype=a.dtype))
@@ -75,6 +73,7 @@ def _uniqueAlongLastAxis(a):
# conversions #################################################################
+
def triangleToLineIndices(triangleIndices, unicity=False):
"""Generates lines indices from triangle indices.
@@ -91,8 +90,7 @@ def triangleToLineIndices(triangleIndices, unicity=False):
triangleIndices = triangleIndices.reshape(-1, 3)
# Pack line indices by triangle and by edge
- lineindices = numpy.empty((len(triangleIndices), 3, 2),
- dtype=triangleIndices.dtype)
+ lineindices = numpy.empty((len(triangleIndices), 3, 2), dtype=triangleIndices.dtype)
lineindices[:, 0] = triangleIndices[:, :2] # edge = t0, t1
lineindices[:, 1] = triangleIndices[:, 1:] # edge =t1, t2
lineindices[:, 2] = triangleIndices[:, ::2] # edge = t0, t2
@@ -106,7 +104,7 @@ def triangleToLineIndices(triangleIndices, unicity=False):
return lineindices
-def verticesNormalsToLines(vertices, normals, scale=1.):
+def verticesNormalsToLines(vertices, normals, scale=1.0):
"""Return vertices of lines representing normals at given positions.
:param vertices: Positions of the points.
@@ -140,13 +138,19 @@ def unindexArrays(mode, indices, *arrays):
"""
indices = numpy.array(indices, copy=False)
- assert mode in ('points',
- 'lines', 'line_strip', 'loop',
- 'triangles', 'triangle_strip', 'fan')
-
- if mode in ('lines', 'line_strip', 'loop'):
+ assert mode in (
+ "points",
+ "lines",
+ "line_strip",
+ "loop",
+ "triangles",
+ "triangle_strip",
+ "fan",
+ )
+
+ if mode in ("lines", "line_strip", "loop"):
assert len(indices) >= 2
- elif mode in ('triangles', 'triangle_strip', 'fan'):
+ elif mode in ("triangles", "triangle_strip", "fan"):
assert len(indices) >= 3
assert indices.min() >= 0
@@ -154,27 +158,27 @@ def unindexArrays(mode, indices, *arrays):
for data in arrays:
assert len(data) >= max_index
- if mode == 'line_strip':
+ if mode == "line_strip":
unpacked = numpy.empty((2 * (len(indices) - 1),), dtype=indices.dtype)
unpacked[0::2] = indices[:-1]
unpacked[1::2] = indices[1:]
indices = unpacked
- elif mode == 'loop':
+ elif mode == "loop":
unpacked = numpy.empty((2 * len(indices),), dtype=indices.dtype)
unpacked[0::2] = indices
unpacked[1:-1:2] = indices[1:]
unpacked[-1] = indices[0]
indices = unpacked
- elif mode == 'triangle_strip':
+ elif mode == "triangle_strip":
unpacked = numpy.empty((3 * (len(indices) - 2),), dtype=indices.dtype)
unpacked[0::3] = indices[:-2]
unpacked[1::3] = indices[1:-1]
unpacked[2::3] = indices[2:]
indices = unpacked
- elif mode == 'fan':
+ elif mode == "fan":
unpacked = numpy.empty((3 * (len(indices) - 2),), dtype=indices.dtype)
unpacked[0::3] = indices[0]
unpacked[1::3] = indices[1:-1]
@@ -223,8 +227,9 @@ def trianglesNormal(positions):
positions = numpy.array(positions, copy=False).reshape(-1, 3, 3)
- normals = numpy.cross(positions[:, 1] - positions[:, 0],
- positions[:, 2] - positions[:, 0])
+ normals = numpy.cross(
+ positions[:, 1] - positions[:, 0], positions[:, 2] - positions[:, 0]
+ )
# Normalize normals
norms = numpy.linalg.norm(normals, axis=1)
@@ -235,6 +240,7 @@ def trianglesNormal(positions):
# grid ########################################################################
+
def gridVertices(dim0Array, dim1Array, dtype):
"""Generate an array of 2D positions from 2 arrays of 1D coordinates.
@@ -311,29 +317,28 @@ def linesGridIndices(dim0, dim1):
nbsegmentalongdim1 = 2 * (dim1 - 1)
nbsegmentalongdim0 = 2 * (dim0 - 1)
- indices = numpy.empty(nbsegmentalongdim1 * dim0 +
- nbsegmentalongdim0 * dim1,
- dtype=numpy.uint32)
+ indices = numpy.empty(
+ nbsegmentalongdim1 * dim0 + nbsegmentalongdim0 * dim1, dtype=numpy.uint32
+ )
# Line indices over dim0
- onedim1line = (numpy.arange(nbsegmentalongdim1,
- dtype=numpy.uint32) + 1) // 2
- indices[:dim0 * nbsegmentalongdim1] = \
- (dim1 * numpy.arange(dim0, dtype=numpy.uint32)[:, None] +
- onedim1line[None, :]).ravel()
+ onedim1line = (numpy.arange(nbsegmentalongdim1, dtype=numpy.uint32) + 1) // 2
+ indices[: dim0 * nbsegmentalongdim1] = (
+ dim1 * numpy.arange(dim0, dtype=numpy.uint32)[:, None] + onedim1line[None, :]
+ ).ravel()
# Line indices over dim1
- onedim0line = (numpy.arange(nbsegmentalongdim0,
- dtype=numpy.uint32) + 1) // 2
- indices[dim0 * nbsegmentalongdim1:] = \
- (numpy.arange(dim1, dtype=numpy.uint32)[:, None] +
- dim1 * onedim0line[None, :]).ravel()
+ onedim0line = (numpy.arange(nbsegmentalongdim0, dtype=numpy.uint32) + 1) // 2
+ indices[dim0 * nbsegmentalongdim1 :] = (
+ numpy.arange(dim1, dtype=numpy.uint32)[:, None] + dim1 * onedim0line[None, :]
+ ).ravel()
return indices
# intersection ################################################################
+
def angleBetweenVectors(refVector, vectors, norm=None):
"""Return the angle between 2 vectors.
@@ -360,10 +365,10 @@ def angleBetweenVectors(refVector, vectors, norm=None):
vectors = numpy.array([v / numpy.linalg.norm(v) for v in vectors])
dots = numpy.sum(refVector * vectors, axis=-1)
- angles = numpy.arccos(numpy.clip(dots, -1., 1.))
+ angles = numpy.arccos(numpy.clip(dots, -1.0, 1.0))
if norm is not None:
- signs = numpy.sum(norm * numpy.cross(refVector, vectors), axis=-1) < 0.
- angles[signs] = numpy.pi * 2. - angles[signs]
+ signs = numpy.sum(norm * numpy.cross(refVector, vectors), axis=-1) < 0.0
+ angles[signs] = numpy.pi * 2.0 - angles[signs]
return angles[0] if singlevector else angles
@@ -394,8 +399,8 @@ def segmentPlaneIntersect(s0, s1, planeNorm, planePt):
else: # No intersection
return []
- alpha = - numpy.dot(planeNorm, s0 - planePt) / dotnormseg
- if 0. <= alpha <= 1.: # Intersection with segment
+ alpha = -numpy.dot(planeNorm, s0 - planePt) / dotnormseg
+ if 0.0 <= alpha <= 1.0: # Intersection with segment
return [s0 + alpha * segdir]
else: # intersection outside segment
return []
@@ -462,8 +467,9 @@ def clipSegmentToBounds(segment, bounds):
points.shape = -1, 3 # Set back to 2D array
# Find intersection points that are included in the volume
- mask = numpy.logical_and(numpy.all(bounds[0] <= points, axis=1),
- numpy.all(points <= bounds[1], axis=1))
+ mask = numpy.logical_and(
+ numpy.all(bounds[0] <= points, axis=1), numpy.all(points <= bounds[1], axis=1)
+ )
intersections = numpy.unique(offsets[mask])
if len(intersections) != 2:
return None
@@ -522,12 +528,12 @@ def segmentVolumeIntersect(segment, nbins):
# Get corresponding line parameters
t = []
if numpy.all(0 <= p0) and numpy.all(p0 <= nbins):
- t.append([0.]) # p0 within volume, add it
+ t.append([0.0]) # p0 within volume, add it
t += [(edgesByDim[i] - p0[i]) / delta[i] for i in range(dim) if delta[i] != 0]
if numpy.all(0 <= p1) and numpy.all(p1 <= nbins):
- t.append([1.]) # p1 within volume, add it
+ t.append([1.0]) # p1 within volume, add it
t = numpy.concatenate(t)
- t.sort(kind='mergesort')
+ t.sort(kind="mergesort")
# Remove duplicates
unique = numpy.ones((len(t),), dtype=bool)
@@ -539,13 +545,14 @@ def segmentVolumeIntersect(segment, nbins):
# bin edges/line intersection points
points = t.reshape(-1, 1) * delta + p0
- centers = (points[:-1] + points[1:]) / 2.
+ centers = (points[:-1] + points[1:]) / 2.0
bins = numpy.floor(centers).astype(numpy.int64)
return bins
# Plane #######################################################################
+
class Plane(event.Notifier):
"""Object handling a plane and notifying plane changes.
@@ -555,7 +562,7 @@ class Plane(event.Notifier):
:type normal: 3-tuple of float.
"""
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
+ def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0)):
super(Plane, self).__init__()
assert len(point) == 3
@@ -586,7 +593,7 @@ class Plane(event.Notifier):
normal = numpy.array(normal, copy=True, dtype=numpy.float32)
norm = numpy.linalg.norm(normal)
- if norm != 0.:
+ if norm != 0.0:
normal /= norm
if not numpy.all(numpy.equal(self._normal, normal)):
@@ -594,8 +601,11 @@ class Plane(event.Notifier):
planechanged = True
if planechanged:
- _logger.debug('Plane updated:\n\tpoint: %s\n\tnormal: %s',
- str(self._point), str(self._normal))
+ _logger.debug(
+ "Plane updated:\n\tpoint: %s\n\tnormal: %s",
+ str(self._point),
+ str(self._normal),
+ )
self.notify()
@property
@@ -619,8 +629,7 @@ class Plane(event.Notifier):
@property
def parameters(self):
"""Plane equation parameters: a*x + b*y + c*z + d = 0."""
- return numpy.append(self._normal,
- - numpy.dot(self._point, self._normal))
+ return numpy.append(self._normal, -numpy.dot(self._point, self._normal))
@parameters.setter
def parameters(self, parameters):
@@ -633,13 +642,13 @@ class Plane(event.Notifier):
parameters /= norm
normal = parameters[:3]
- point = - parameters[3] * normal
+ point = -parameters[3] * normal
self.setPlane(point, normal)
@property
def isPlane(self):
"""True if a plane is defined (i.e., ||normal|| != 0)."""
- return numpy.any(self.normal != 0.)
+ return numpy.any(self.normal != 0.0)
def move(self, step):
"""Move the plane of step along the normal."""
diff --git a/src/silx/gui/plot3d/scene/viewport.py b/src/silx/gui/plot3d/scene/viewport.py
index 6de640e..c39d3ef 100644
--- a/src/silx/gui/plot3d/scene/viewport.py
+++ b/src/silx/gui/plot3d/scene/viewport.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
@@ -29,8 +28,6 @@ The attribute :attr:`scene` is the root group of the scene tree.
:class:`RenderContext` handles the current state during rendering.
"""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
@@ -62,17 +59,19 @@ class RenderContext(object):
:param Context glContext: The operating system OpenGL context in use.
"""
- _FRAGMENT_SHADER_SRC = string.Template("""
+ _FRAGMENT_SHADER_SRC = string.Template(
+ """
void scene_post(vec4 cameraPosition) {
gl_FragColor = $fogCall(gl_FragColor, cameraPosition);
}
- """)
+ """
+ )
def __init__(self, viewport, glContext):
self._viewport = viewport
self._glContext = glContext
self._transformStack = [viewport.camera.extrinsic]
- self._clipPlane = ClippingPlane(normal=(0., 0., 0.))
+ self._clipPlane = ClippingPlane(normal=(0.0, 0.0, 0.0))
# cache
self.__cache = {}
@@ -121,8 +120,7 @@ class RenderContext(object):
Do not modify.
"""
- return transform.StaticTransformList(
- (self.projection, self.objectToCamera))
+ return transform.StaticTransformList((self.projection, self.objectToCamera))
def pushTransform(self, transform_, multiply=True):
"""Push a :class:`Transform` on the transform stack.
@@ -135,7 +133,8 @@ class RenderContext(object):
if multiply:
assert len(self._transformStack) >= 1
transform_ = transform.StaticTransformList(
- (self._transformStack[-1], transform_))
+ (self._transformStack[-1], transform_)
+ )
self._transformStack.append(transform_)
@@ -152,7 +151,7 @@ class RenderContext(object):
"""The current clipping plane (ClippingPlane)"""
return self._clipPlane
- def setClipPlane(self, point=(0., 0., 0.), normal=(0., 0., 0.)):
+ def setClipPlane(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 0.0)):
"""Set the clipping plane to use
For now only handles a single clipping plane.
@@ -176,11 +175,15 @@ class RenderContext(object):
@property
def fragDecl(self):
"""Fragment shader declaration for scene shader functions"""
- return '\n'.join((
- self.clipper.fragDecl,
- self.viewport.fog.fragDecl,
- self._FRAGMENT_SHADER_SRC.substitute(
- fogCall=self.viewport.fog.fragCall)))
+ return "\n".join(
+ (
+ self.clipper.fragDecl,
+ self.viewport.fog.fragDecl,
+ self._FRAGMENT_SHADER_SRC.substitute(
+ fogCall=self.viewport.fog.fragCall
+ ),
+ )
+ )
@property
def fragCallPre(self):
@@ -207,6 +210,7 @@ class Viewport(event.Notifier):
def __init__(self, framebuffer=0):
from . import Group # Here to avoid cyclic import
+
super(Viewport, self).__init__()
self._dirty = True
self._origin = 0, 0
@@ -215,15 +219,16 @@ class Viewport(event.Notifier):
self.scene = Group() # The stuff to render, add overlaid scenes?
self.scene._setParent(self)
self.scene.addListener(self._changed)
- self._background = 0., 0., 0., 1.
- self._camera = camera.Camera(fovy=30., near=1., far=100.,
- position=(0., 0., 12.))
+ self._background = 0.0, 0.0, 0.0, 1.0
+ self._camera = camera.Camera(
+ fovy=30.0, near=1.0, far=100.0, position=(0.0, 0.0, 12.0)
+ )
self._camera.addListener(self._changed)
self._transforms = transform.TransformList([self._camera])
- self._light = DirectionalLight(direction=(0., 0., -1.),
- ambient=(0.3, 0.3, 0.3),
- diffuse=(0.7, 0.7, 0.7))
+ self._light = DirectionalLight(
+ direction=(0.0, 0.0, -1.0), ambient=(0.3, 0.3, 0.3), diffuse=(0.7, 0.7, 0.7)
+ )
self._light.addListener(self._changed)
self._fog = Fog()
self._fog.isOn = False
@@ -355,7 +360,7 @@ class Viewport(event.Notifier):
gl.glEnable(gl.GL_DEPTH_TEST)
gl.glDepthFunc(gl.GL_LEQUAL)
- gl.glDepthRange(0., 1.)
+ gl.glDepthRange(0.0, 1.0)
# gl.glEnable(gl.GL_POLYGON_OFFSET_FILL)
# gl.glPolygonOffset(1., 1.)
@@ -364,15 +369,16 @@ class Viewport(event.Notifier):
gl.glEnable(gl.GL_LINE_SMOOTH)
if self.background is None:
- gl.glClear(gl.GL_STENCIL_BUFFER_BIT |
- gl.GL_DEPTH_BUFFER_BIT)
+ gl.glClear(gl.GL_STENCIL_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
else:
gl.glClearColor(*self.background)
# Prepare OpenGL
- gl.glClear(gl.GL_COLOR_BUFFER_BIT |
- gl.GL_STENCIL_BUFFER_BIT |
- gl.GL_DEPTH_BUFFER_BIT)
+ gl.glClear(
+ gl.GL_COLOR_BUFFER_BIT
+ | gl.GL_STENCIL_BUFFER_BIT
+ | gl.GL_DEPTH_BUFFER_BIT
+ )
ctx = RenderContext(self, glContext)
self.scene.render(ctx)
@@ -387,15 +393,16 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(
+ ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), dtype=numpy.float32
+ )
bounds = self.camera.extrinsic.transformBounds(bounds)
if isinstance(self.camera.intrinsic, transform.Perspective):
# This needs to be reworked
- zbounds = - bounds[:, 2]
+ zbounds = -bounds[:, 2]
zextent = max(numpy.fabs(zbounds[0] - zbounds[1]), 0.0001)
- near = max(zextent / 1000., 0.95 * zbounds[1])
+ near = max(zextent / 1000.0, 0.95 * zbounds[1])
far = max(near + 0.1, 1.05 * zbounds[0])
self.camera.intrinsic.setDepthExtent(near, far)
@@ -404,7 +411,7 @@ class Viewport(event.Notifier):
border = max(abs(bounds[:, 2]))
self.camera.intrinsic.setDepthExtent(-border, border)
else:
- raise RuntimeError('Unsupported camera', self.camera.intrinsic)
+ raise RuntimeError("Unsupported camera", self.camera.intrinsic)
def resetCamera(self):
"""Change camera to have the whole scene in the viewing frustum.
@@ -414,11 +421,12 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(
+ ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), dtype=numpy.float32
+ )
self.camera.resetCamera(bounds)
- def orbitCamera(self, direction, angle=1.):
+ def orbitCamera(self, direction, angle=1.0):
"""Rotate the camera around center of the scene.
:param str direction: Direction of movement relative to image plane.
@@ -427,8 +435,9 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(
+ ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), dtype=numpy.float32
+ )
center = 0.5 * (bounds[0] + bounds[1])
self.camera.orbit(direction, center, angle)
@@ -442,35 +451,36 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(
+ ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), dtype=numpy.float32
+ )
bounds = self.camera.extrinsic.transformBounds(bounds)
center = 0.5 * (bounds[0] + bounds[1])
- ndcCenter = self.camera.intrinsic.transformPoint(
- center, perspectiveDivide=True)
+ ndcCenter = self.camera.intrinsic.transformPoint(center, perspectiveDivide=True)
- step *= 2. # NDC has size 2
+ step *= 2.0 # NDC has size 2
- if direction == 'up':
+ if direction == "up":
ndcCenter[1] -= step
- elif direction == 'down':
+ elif direction == "down":
ndcCenter[1] += step
- elif direction == 'right':
+ elif direction == "right":
ndcCenter[0] -= step
- elif direction == 'left':
+ elif direction == "left":
ndcCenter[0] += step
- elif direction == 'forward':
+ elif direction == "forward":
ndcCenter[2] += step
- elif direction == 'backward':
+ elif direction == "backward":
ndcCenter[2] -= step
else:
- raise ValueError('Unsupported direction: %s' % direction)
+ raise ValueError("Unsupported direction: %s" % direction)
newCenter = self.camera.intrinsic.transformPoint(
- ndcCenter, direct=False, perspectiveDivide=True)
+ ndcCenter, direct=False, perspectiveDivide=True
+ )
self.camera.move(direction, numpy.linalg.norm(newCenter - center))
@@ -498,11 +508,11 @@ class Viewport(event.Notifier):
x, y = winX - ox, winY - oy
- if checkInside and (x < 0. or x > width or y < 0. or y > height):
+ if checkInside and (x < 0.0 or x > width or y < 0.0 or y > height):
return None # Out of viewport
- ndcx = 2. * x / float(width) - 1.
- ndcy = 1. - 2. * y / float(height)
+ ndcx = 2.0 * x / float(width) - 1.0
+ ndcy = 1.0 - 2.0 * y / float(height)
return ndcx, ndcy
def ndcToWindow(self, ndcX, ndcY, checkInside=True):
@@ -515,15 +525,14 @@ class Viewport(event.Notifier):
:return: (x, y) window coordinates or None.
Origin top-left, x to the right, y goes downward.
"""
- if (checkInside and
- (ndcX < -1. or ndcX > 1. or ndcY < -1. or ndcY > 1.)):
+ if checkInside and (ndcX < -1.0 or ndcX > 1.0 or ndcY < -1.0 or ndcY > 1.0):
return None # Outside viewport
ox, oy = self._origin
width, height = self.size
- winx = ox + width * 0.5 * (ndcX + 1.)
- winy = oy + height * 0.5 * (1. - ndcY)
+ winx = ox + width * 0.5 * (ndcX + 1.0)
+ winy = oy + height * 0.5 * (1.0 - ndcY)
return winx, winy
def _pickNdcZGL(self, x, y, offset=0):
@@ -553,20 +562,19 @@ class Viewport(event.Notifier):
if offset == 0: # Fast path
# glReadPixels is not GL|ES friendly
- depth = gl.glReadPixels(
- x, y, 1, 1, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)[0]
+ depthPatch = gl.glReadPixels(x, y, 1, 1, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)
+ depth = numpy.ravel(depthPatch)[0]
else:
offset = abs(int(offset))
- size = 2*offset + 1
+ size = 2 * offset + 1
depthPatch = gl.glReadPixels(
- x - offset, y - offset,
- size, size,
- gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)
+ x - offset, y - offset, size, size, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT
+ )
depthPatch = depthPatch.ravel() # Work in 1D
# TODO cache sortedIndices to avoid computing it each time
# Compute distance of each pixels to the center of the patch
- offsetToCenter = numpy.arange(- offset, offset + 1, dtype=numpy.float32) ** 2
+ offsetToCenter = numpy.arange(-offset, offset + 1, dtype=numpy.float32) ** 2
sqDistToCenter = numpy.add.outer(offsetToCenter, offsetToCenter)
# Use distance to center to sort values from the patch
@@ -574,26 +582,26 @@ class Viewport(event.Notifier):
sortedValues = depthPatch[sortedIndices]
# Take first depth that is not 1 in the sorted values
- hits = sortedValues[sortedValues != 1.]
- depth = 1. if len(hits) == 0 else hits[0]
+ hits = sortedValues[sortedValues != 1.0]
+ depth = 1.0 if len(hits) == 0 else hits[0]
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
# Z in NDC in [-1., 1.]
- return float(depth) * 2. - 1.
+ return float(depth) * 2.0 - 1.0
def _getXZYGL(self, x, y):
ndc = self.windowToNdc(x, y)
if ndc is None:
return None # Outside viewport
ndcz = self._pickNdcZGL(x, y)
- ndcpos = numpy.array((ndc[0], ndc[1], ndcz, 1.), dtype=numpy.float32)
+ ndcpos = numpy.array((ndc[0], ndc[1], ndcz, 1.0), dtype=numpy.float32)
camerapos = self.camera.intrinsic.transformPoint(
- ndcpos, direct=False, perspectiveDivide=True)
+ ndcpos, direct=False, perspectiveDivide=True
+ )
- scenepos = self.camera.extrinsic.transformPoint(camerapos,
- direct=False)
+ scenepos = self.camera.extrinsic.transformPoint(camerapos, direct=False)
return scenepos[:3]
def pick(self, x, y):
diff --git a/src/silx/gui/plot3d/scene/window.py b/src/silx/gui/plot3d/scene/window.py
index b92c404..2a6d93b 100644
--- a/src/silx/gui/plot3d/scene/window.py
+++ b/src/silx/gui/plot3d/scene/window.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
@@ -32,8 +31,6 @@ The :class:`Context` and :class:`ContextGL2` represent the operating system
OpenGL context and handle OpenGL resources.
"""
-from __future__ import absolute_import, division, unicode_literals
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "10/01/2017"
@@ -61,6 +58,7 @@ class Context(object):
self._context = glContextHandle
self._isCurrent = False
self._devicePixelRatio = 1.0
+ self._dotsPerInch = 96.0
@property
def isCurrent(self):
@@ -78,6 +76,16 @@ class Context(object):
self._isCurrent = bool(isCurrent)
@property
+ def dotsPerInch(self) -> float:
+ """Number of physical dots per inch on the screen"""
+ return self._dotsPerInch
+
+ @dotsPerInch.setter
+ def dotsPerInch(self, dpi: float):
+ assert dpi > 0.0
+ self._dotsPerInch = float(dpi)
+
+ @property
def devicePixelRatio(self):
"""Ratio between device and device independent pixels (float)
@@ -115,6 +123,7 @@ class ContextGL2(Context):
:param glContextHandle: System specific OpenGL context handle.
"""
+
def __init__(self, glContextHandle):
super(ContextGL2, self).__init__(glContextHandle)
@@ -124,7 +133,7 @@ class ContextGL2(Context):
# programs
- def prog(self, vertexShaderSrc, fragmentShaderSrc, attrib0='position'):
+ def prog(self, vertexShaderSrc, fragmentShaderSrc, attrib0="position"):
"""Cache program within context.
WARNING: No clean-up.
@@ -141,14 +150,14 @@ class ContextGL2(Context):
program = self._programs.get(key, None)
if program is None:
program = _glutils.Program(
- vertexShaderSrc, fragmentShaderSrc, attrib0=attrib0)
+ vertexShaderSrc, fragmentShaderSrc, attrib0=attrib0
+ )
self._programs[key] = program
return program
# VBOs
- def makeVbo(self, data=None, sizeInBytes=None,
- usage=None, target=None):
+ def makeVbo(self, data=None, sizeInBytes=None, usage=None, target=None):
"""Create a VBO in this context with the data.
Current limitations:
@@ -196,7 +205,8 @@ class ContextGL2(Context):
size=data.shape[0],
dimension=dimension,
offset=0,
- stride=0)
+ stride=0,
+ )
def _deadVbo(self, vboRef):
"""Callback handling dead VBOAttribs."""
@@ -231,13 +241,18 @@ class Window(event.Notifier):
update the texture only when needed.
"""
- _position = numpy.array(((-1., -1., 0., 0.),
- (1., -1., 1., 0.),
- (-1., 1., 0., 1.),
- (1., 1., 1., 1.)),
- dtype=numpy.float32)
-
- _shaders = ("""
+ _position = numpy.array(
+ (
+ (-1.0, -1.0, 0.0, 0.0),
+ (1.0, -1.0, 1.0, 0.0),
+ (-1.0, 1.0, 0.0, 1.0),
+ (1.0, 1.0, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ _shaders = (
+ """
attribute vec4 position;
varying vec2 textureCoord;
@@ -246,7 +261,7 @@ class Window(event.Notifier):
textureCoord = position.zw;
}
""",
- """
+ """
uniform sampler2D texture;
varying vec2 textureCoord;
@@ -254,9 +269,10 @@ class Window(event.Notifier):
gl_FragColor = texture2D(texture, textureCoord);
gl_FragColor.a = 1.0;
}
- """)
+ """,
+ )
- def __init__(self, mode='framebuffer'):
+ def __init__(self, mode="framebuffer"):
super(Window, self).__init__()
self._dirty = True
self._size = 0, 0
@@ -266,8 +282,8 @@ class Window(event.Notifier):
self._framebufferid = 0
self._framebuffers = {} # Cache of framebuffers
- assert mode in ('direct', 'framebuffer')
- self._isframebuffer = mode == 'framebuffer'
+ assert mode in ("direct", "framebuffer")
+ self._isframebuffer = mode == "framebuffer"
@property
def dirty(self):
@@ -319,8 +335,9 @@ class Window(event.Notifier):
self._dirty = True
self.notify(*args, **kwargs)
- framebufferid = property(lambda self: self._framebufferid,
- doc="Framebuffer ID used to perform rendering")
+ framebufferid = property(
+ lambda self: self._framebufferid, doc="Framebuffer ID used to perform rendering"
+ )
def grab(self, glcontext):
"""Returns the raster of the scene as an RGB numpy array
@@ -335,21 +352,21 @@ class Window(event.Notifier):
previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.framebufferid)
gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
- gl.glReadPixels(
- 0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, image)
+ gl.glReadPixels(0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, image)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer)
# glReadPixels gives bottom to top,
# while images are stored as top to bottom
image = numpy.flipud(image)
- return numpy.array(image, copy=False, order='C')
+ return numpy.array(image, copy=False, order="C")
- def render(self, glcontext, devicePixelRatio):
+ def render(self, glcontext, dotsPerInch: float, devicePixelRatio: float):
"""Perform the rendering of attached viewports
:param glcontext: System identifier of the OpenGL context
- :param float devicePixelRatio:
+ :param dotsPerInch: Screen physical resolution in pixels per inch
+ :param devicePixelRatio:
Ratio between device and device-independent pixels
"""
if self.size == (0, 0):
@@ -359,6 +376,7 @@ class Window(event.Notifier):
self._contexts[glcontext] = ContextGL2(glcontext) # New context
with self._contexts[glcontext] as context:
+ context.dotsPerInch = dotsPerInch
context.devicePixelRatio = devicePixelRatio
if self._isframebuffer:
self._renderWithOffscreenFramebuffer(context)
@@ -387,18 +405,22 @@ class Window(event.Notifier):
if self.dirty or context not in self._framebuffers:
# Need to redraw framebuffer content
- if (context not in self._framebuffers or
- self._framebuffers[context].shape != self.shape):
+ if (
+ context not in self._framebuffers
+ or self._framebuffers[context].shape != self.shape
+ ):
# Need to rebuild framebuffer
if context in self._framebuffers:
self._framebuffers[context].discard()
- fbo = _glutils.FramebufferTexture(gl.GL_RGBA,
- shape=self.shape,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ fbo = _glutils.FramebufferTexture(
+ gl.GL_RGBA,
+ shape=self.shape,
+ minFilter=gl.GL_NEAREST,
+ magFilter=gl.GL_NEAREST,
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
self._framebuffers[context] = fbo
self._framebufferid = fbo.name
@@ -418,16 +440,18 @@ class Window(event.Notifier):
gl.glDisable(gl.GL_DEPTH_TEST)
gl.glDisable(gl.GL_SCISSOR_TEST)
# gl.glScissor(0, 0, width, height)
- gl.glClearColor(0., 0., 0., 0.)
+ gl.glClearColor(0.0, 0.0, 0.0, 0.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
- gl.glUniform1i(program.uniforms['texture'], fbo.texture.texUnit)
- gl.glEnableVertexAttribArray(program.attributes['position'])
- gl.glVertexAttribPointer(program.attributes['position'],
- 4,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._position)
+ gl.glUniform1i(program.uniforms["texture"], fbo.texture.texUnit)
+ gl.glEnableVertexAttribArray(program.attributes["position"])
+ gl.glVertexAttribPointer(
+ program.attributes["position"],
+ 4,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ self._position,
+ )
fbo.texture.bind()
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._position))
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
diff --git a/src/silx/gui/plot3d/setup.py b/src/silx/gui/plot3d/setup.py
deleted file mode 100644
index 59c0230..0000000
--- a/src/silx/gui/plot3d/setup.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('plot3d', parent_package, top_path)
- config.add_subpackage('_model')
- config.add_subpackage('actions')
- config.add_subpackage('items')
- config.add_subpackage('scene')
- config.add_subpackage('scene.test')
- config.add_subpackage('tools')
- config.add_subpackage('tools.test')
- config.add_subpackage('test')
- config.add_subpackage('utils')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
-
- setup(configuration=configuration)
diff --git a/src/silx/gui/plot3d/test/__init__.py b/src/silx/gui/plot3d/test/__init__.py
index 83491ad..f8afa83 100644
--- a/src/silx/gui/plot3d/test/__init__.py
+++ b/src/silx/gui/plot3d/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot3d/test/testGL.py b/src/silx/gui/plot3d/test/testGL.py
index a7309a9..a2627eb 100644
--- a/src/silx/gui/plot3d/test/testGL.py
+++ b/src/silx/gui/plot3d/test/testGL.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017 European Synchrotron Radiation Facility
@@ -29,7 +28,7 @@ __date__ = "10/08/2017"
import logging
-import unittest
+import pytest
from silx.gui._glutils import gl, OpenGLWidget
from silx.gui.utils.testutils import TestCaseQt
@@ -39,6 +38,7 @@ from silx.gui import qt
_logger = logging.getLogger(__name__)
+@pytest.mark.usefixtures("use_opengl")
class TestOpenGL(TestCaseQt):
"""Tests of OpenGL widget."""
@@ -53,14 +53,18 @@ class TestOpenGL(TestCaseQt):
"""Perform the rendering and logging"""
if not self._dump:
self._dump = True
- _logger.info('OpenGL info:')
- _logger.info('\tQt OpenGL context version: %d.%d', *self.getOpenGLVersion())
- _logger.info('\tGL_VERSION: %s' % gl.glGetString(gl.GL_VERSION))
- _logger.info('\tGL_SHADING_LANGUAGE_VERSION: %s' %
- gl.glGetString(gl.GL_SHADING_LANGUAGE_VERSION))
- _logger.debug('\tGL_EXTENSIONS: %s' % gl.glGetString(gl.GL_EXTENSIONS))
+ _logger.info("OpenGL info:")
+ _logger.info(
+ "\tQt OpenGL context version: %d.%d", *self.getOpenGLVersion()
+ )
+ _logger.info("\tGL_VERSION: %s" % gl.glGetString(gl.GL_VERSION))
+ _logger.info(
+ "\tGL_SHADING_LANGUAGE_VERSION: %s"
+ % gl.glGetString(gl.GL_SHADING_LANGUAGE_VERSION)
+ )
+ _logger.debug("\tGL_EXTENSIONS: %s" % gl.glGetString(gl.GL_EXTENSIONS))
- gl.glClearColor(1., 1., 1., 1.)
+ gl.glClearColor(1.0, 1.0, 1.0, 1.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
def testOpenGL(self):
diff --git a/src/silx/gui/plot3d/test/testScalarFieldView.py b/src/silx/gui/plot3d/test/testScalarFieldView.py
index e6535fc..f81b985 100644
--- a/src/silx/gui/plot3d/test/testScalarFieldView.py
+++ b/src/silx/gui/plot3d/test/testScalarFieldView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
@@ -29,7 +28,7 @@ __date__ = "17/01/2018"
import logging
-import unittest
+import pytest
import numpy
@@ -44,6 +43,7 @@ from silx.gui.plot3d.SFViewParamTree import TreeView
_logger = logging.getLogger(__name__)
+@pytest.mark.usefixtures("use_opengl")
class TestScalarFieldView(TestCaseQt, ParametricTestCase):
"""Tests of ScalarFieldView widget."""
@@ -83,8 +83,8 @@ class TestScalarFieldView(TestCaseQt, ParametricTestCase):
data = self._buildData(size=32)
self.widget.setData(data)
- self.widget.addIsosurface(0.5, (1., 0., 0., 0.5))
- self.widget.addIsosurface(0.7, qt.QColor('green'))
+ self.widget.addIsosurface(0.5, (1.0, 0.0, 0.0, 0.5))
+ self.widget.addIsosurface(0.7, qt.QColor("green"))
self.qapp.processEvents()
def testNotFinite(self):
@@ -94,9 +94,9 @@ class TestScalarFieldView(TestCaseQt, ParametricTestCase):
data = self._buildData(size=32)
data[8, :, :] = numpy.nan
data[16, :, :] = numpy.inf
- data[24, :, :] = - numpy.inf
+ data[24, :, :] = -numpy.inf
- self.widget.addIsosurface(0.5, 'red')
+ self.widget.addIsosurface(0.5, "red")
self.widget.setData(data, copy=True)
self.qapp.processEvents()
self.widget.setData(None)
@@ -114,13 +114,13 @@ class TestScalarFieldView(TestCaseQt, ParametricTestCase):
data = self._buildData(size=32)
self.widget.setData(data)
- self.widget.addIsosurface(0.5, (1., 0., 0., 0.5))
- self.widget.addIsosurface(0.7, qt.QColor('green'))
+ self.widget.addIsosurface(0.5, (1.0, 0.0, 0.0, 0.5))
+ self.widget.addIsosurface(0.7, qt.QColor("green"))
self.qapp.processEvents()
# Add a second TreeView
paramTreeWidget = TreeView(self.widget)
- paramTreeWidget.setIsoLevelSliderNormalization('arcsinh')
+ paramTreeWidget.setIsoLevelSliderNormalization("arcsinh")
paramTreeWidget.setSfView(self.widget)
dock = qt.QDockWidget()
diff --git a/src/silx/gui/plot3d/test/testSceneWidget.py b/src/silx/gui/plot3d/test/testSceneWidget.py
index fc96781..cb3767c 100644
--- a/src/silx/gui/plot3d/test/testSceneWidget.py
+++ b/src/silx/gui/plot3d/test/testSceneWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2019 European Synchrotron Radiation Facility
@@ -28,7 +27,7 @@ __license__ = "MIT"
__date__ = "06/03/2019"
-import unittest
+import pytest
import numpy
@@ -39,6 +38,7 @@ from silx.gui import qt
from silx.gui.plot3d.SceneWidget import SceneWidget
+@pytest.mark.usefixtures("use_opengl")
class TestSceneWidget(TestCaseQt, ParametricTestCase):
"""Tests SceneWidget picking feature"""
@@ -62,7 +62,7 @@ class TestSceneWidget(TestCaseQt, ParametricTestCase):
scatter.setTranslation(10, 10)
scatter.setScale(10, 10, 10)
- self.widget.resetZoom('front')
+ self.widget.resetZoom("front")
self.qapp.processEvents()
self.widget.setFogMode(self.widget.FogMode.LINEAR)
diff --git a/src/silx/gui/plot3d/test/testSceneWidgetPicking.py b/src/silx/gui/plot3d/test/testSceneWidgetPicking.py
index d4d8db7..1c32899 100644
--- a/src/silx/gui/plot3d/test/testSceneWidgetPicking.py
+++ b/src/silx/gui/plot3d/test/testSceneWidgetPicking.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
@@ -28,7 +27,7 @@ __license__ = "MIT"
__date__ = "03/10/2018"
-import unittest
+import pytest
import numpy
@@ -39,6 +38,7 @@ from silx.gui import qt
from silx.gui.plot3d.SceneWidget import SceneWidget, items
+@pytest.mark.usefixtures("use_opengl")
class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
"""Tests SceneWidget picking feature"""
@@ -67,15 +67,14 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
imageData.setData(numpy.arange(100).reshape(10, 10))
imageRgba = items.ImageRgba()
- imageRgba.setData(
- numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3))
+ imageRgba.setData(numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3))
for item in (imageData, imageRgba):
with self.subTest(item=item.__class__.__name__):
# Add item
self.widget.clearItems()
self.widget.addItem(item)
- self.widget.resetZoom('front')
+ self.widget.resetZoom("front")
self.qapp.processEvents()
# Picking on data (at widget center)
@@ -83,12 +82,12 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
self.assertEqual(len(picking), 1)
self.assertIs(picking[0].getItem(), item)
- self.assertEqual(picking[0].getPositions('ndc').shape, (1, 3))
+ self.assertEqual(picking[0].getPositions("ndc").shape, (1, 3))
data = picking[0].getData()
self.assertEqual(len(data), 1)
- self.assertTrue(numpy.array_equal(
- data,
- item.getData()[picking[0].getIndices()]))
+ self.assertTrue(
+ numpy.array_equal(data, item.getData()[picking[0].getIndices()])
+ )
# Picking outside data
picking = list(self.widget.pickItems(1, 1))
@@ -109,7 +108,7 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
# Add item
self.widget.clearItems()
self.widget.addItem(item)
- self.widget.resetZoom('front')
+ self.widget.resetZoom("front")
self.qapp.processEvents()
# Picking on data (at widget center)
@@ -117,12 +116,14 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
self.assertEqual(len(picking), 1)
self.assertIs(picking[0].getItem(), item)
- nbPos = len(picking[0].getPositions('ndc'))
+ nbPos = len(picking[0].getPositions("ndc"))
data = picking[0].getData()
self.assertEqual(nbPos, len(data))
- self.assertTrue(numpy.array_equal(
- data,
- item.getValueData()[picking[0].getIndices()]))
+ self.assertTrue(
+ numpy.array_equal(
+ data, item.getValueData()[picking[0].getIndices()]
+ )
+ )
# Picking outside data
picking = list(self.widget.pickItems(1, 1))
@@ -137,7 +138,7 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
if dtype == numpy.complex64:
volume.setComplexMode(volume.ComplexMode.REAL)
refData = numpy.real(refData)
- self.widget.resetZoom('front')
+ self.widget.resetZoom("front")
cutplane = volume.getCutPlanes()[0]
if dtype == numpy.complex64:
@@ -159,13 +160,12 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
data = picking[0].getData()
self.assertEqual(len(data), 1)
self.assertEqual(picking[0].getPositions().shape, (1, 3))
- self.assertTrue(numpy.array_equal(
- data,
- refData[picking[0].getIndices()]))
+ self.assertTrue(
+ numpy.array_equal(data, refData[picking[0].getIndices()])
+ )
# Picking on data with an isosurface
- isosurface = volume.addIsosurface(
- level=500, color=(1., 0., 0., .5))
+ isosurface = volume.addIsosurface(level=500, color=(1.0, 0.0, 0.0, 0.5))
picking = list(self.widget.pickItems(*self._widgetCenter()))
self.assertEqual(len(picking), 2)
self.assertIs(picking[0].getItem(), cutplane)
@@ -173,9 +173,9 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
self.assertEqual(picking[1].getPositions().shape, (1, 3))
data = picking[1].getData()
self.assertEqual(len(data), 1)
- self.assertTrue(numpy.array_equal(
- data,
- refData[picking[1].getIndices()]))
+ self.assertTrue(
+ numpy.array_equal(data, refData[picking[1].getIndices()])
+ )
# Picking outside data
picking = list(self.widget.pickItems(1, 1))
@@ -188,27 +188,29 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
triangles = items.Mesh()
triangles.setData(
- position=((0, 0, 0), (1, 0, 0), (1, 1, 0),
- (0, 0, 0), (1, 1, 0), (0, 1, 0)),
+ position=((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 0, 0), (1, 1, 0), (0, 1, 0)),
color=(1, 0, 0, 1),
- mode='triangles')
+ mode="triangles",
+ )
triangleStrip = items.Mesh()
triangleStrip.setData(
position=(((1, 0, 0), (0, 0, 0), (1, 1, 0), (0, 1, 0))),
color=(0, 1, 0, 1),
- mode='triangle_strip')
+ mode="triangle_strip",
+ )
triangleFan = items.Mesh()
triangleFan.setData(
position=((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)),
color=(0, 0, 1, 1),
- mode='fan')
+ mode="fan",
+ )
for item in (triangles, triangleStrip, triangleFan):
with self.subTest(mode=item.getDrawMode()):
# Add item
self.widget.clearItems()
self.widget.addItem(item)
- self.widget.resetZoom('front')
+ self.widget.resetZoom("front")
self.qapp.processEvents()
# Picking on data (at widget center)
@@ -219,9 +221,11 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
nbPos = len(picking[0].getPositions())
data = picking[0].getData()
self.assertEqual(nbPos, len(data))
- self.assertTrue(numpy.array_equal(
- data,
- item.getPositionData()[picking[0].getIndices()]))
+ self.assertTrue(
+ numpy.array_equal(
+ data, item.getPositionData()[picking[0].getIndices()]
+ )
+ )
# Picking outside data
picking = list(self.widget.pickItems(1, 1))
@@ -235,29 +239,35 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
color=(1, 0, 0, 1),
indices=numpy.array( # dummy triangles and square
- (0, 0, 1, 0, 1, 2, 1, 2, 3), dtype=numpy.uint8),
- mode='triangles')
+ (0, 0, 1, 0, 1, 2, 1, 2, 3), dtype=numpy.uint8
+ ),
+ mode="triangles",
+ )
triangleStrip = items.Mesh()
triangleStrip.setData(
position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
color=(0, 1, 0, 1),
indices=numpy.array( # dummy triangles and square
- (1, 0, 0, 1, 2, 3), dtype=numpy.uint8),
- mode='triangle_strip')
+ (1, 0, 0, 1, 2, 3), dtype=numpy.uint8
+ ),
+ mode="triangle_strip",
+ )
triangleFan = items.Mesh()
triangleFan.setData(
position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
color=(0, 0, 1, 1),
indices=numpy.array( # dummy triangle, square, dummy
- (1, 1, 0, 2, 3, 3), dtype=numpy.uint8),
- mode='fan')
+ (1, 1, 0, 2, 3, 3), dtype=numpy.uint8
+ ),
+ mode="fan",
+ )
for item in (triangles, triangleStrip, triangleFan):
with self.subTest(mode=item.getDrawMode()):
# Add item
self.widget.clearItems()
self.widget.addItem(item)
- self.widget.resetZoom('front')
+ self.widget.resetZoom("front")
self.qapp.processEvents()
# Picking on data (at widget center)
@@ -268,9 +278,11 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
nbPos = len(picking[0].getPositions())
data = picking[0].getData()
self.assertEqual(nbPos, len(data))
- self.assertTrue(numpy.array_equal(
- data,
- item.getPositionData()[picking[0].getIndices()]))
+ self.assertTrue(
+ numpy.array_equal(
+ data, item.getPositionData()[picking[0].getIndices()]
+ )
+ )
# Picking outside data
picking = list(self.widget.pickItems(1, 1))
@@ -279,7 +291,7 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
def testPickCylindricalMesh(self):
"""Test picking of Box, Cylinder and Hexagon items"""
- positions = numpy.array(((0., 0., 0.), (1., 1., 0.), (2., 2., 0.)))
+ positions = numpy.array(((0.0, 0.0, 0.0), (1.0, 1.0, 0.0), (2.0, 2.0, 0.0)))
box = items.Box()
box.setData(position=positions)
cylinder = items.Cylinder()
@@ -292,7 +304,7 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
# Add item
self.widget.clearItems()
self.widget.addItem(item)
- self.widget.resetZoom('front')
+ self.widget.resetZoom("front")
self.qapp.processEvents()
# Picking on data (at widget center)
@@ -305,9 +317,9 @@ class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
print(item.__class__.__name__, [positions[1]], data)
self.assertTrue(numpy.all(numpy.equal(positions[1], data)))
self.assertEqual(nbPos, len(data))
- self.assertTrue(numpy.array_equal(
- data,
- item.getPosition()[picking[0].getIndices()]))
+ self.assertTrue(
+ numpy.array_equal(data, item.getPosition()[picking[0].getIndices()])
+ )
# Picking outside data
picking = list(self.widget.pickItems(1, 1))
diff --git a/src/silx/gui/plot3d/test/testSceneWindow.py b/src/silx/gui/plot3d/test/testSceneWindow.py
index 6b61335..f2dc486 100644
--- a/src/silx/gui/plot3d/test/testSceneWindow.py
+++ b/src/silx/gui/plot3d/test/testSceneWindow.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2019-2021 European Synchrotron Radiation Facility
@@ -28,7 +27,7 @@ __license__ = "MIT"
__date__ = "22/03/2019"
-import unittest
+import pytest
import numpy
@@ -39,6 +38,8 @@ from silx.gui import qt
from silx.gui.plot3d.SceneWindow import SceneWindow
from silx.gui.plot3d.items import HeightMapData, HeightMapRGBA
+
+@pytest.mark.usefixtures("use_opengl")
class TestSceneWindow(TestCaseQt, ParametricTestCase):
"""Tests SceneWidget picking feature"""
@@ -61,23 +62,25 @@ class TestSceneWindow(TestCaseQt, ParametricTestCase):
items = []
# RGB image
- image = sceneWidget.addImage(numpy.random.random(
- 10*10*3).astype(numpy.float32).reshape(10, 10, 3))
- image.setLabel('RGB image')
+ image = sceneWidget.addImage(
+ numpy.random.random(10 * 10 * 3).astype(numpy.float32).reshape(10, 10, 3)
+ )
+ image.setLabel("RGB image")
items.append(image)
self.assertEqual(sceneWidget.getItems(), tuple(items))
# Data image
image = sceneWidget.addImage(
- numpy.arange(100, dtype=numpy.float32).reshape(10, 10))
- image.setTranslation(10.)
+ numpy.arange(100, dtype=numpy.float32).reshape(10, 10)
+ )
+ image.setTranslation(10.0)
items.append(image)
self.assertEqual(sceneWidget.getItems(), tuple(items))
# 2D scatter
scatter = sceneWidget.add2DScatter(
- *numpy.random.random(3000).astype(numpy.float32).reshape(3, -1),
- index=0)
+ *numpy.random.random(3000).astype(numpy.float32).reshape(3, -1), index=0
+ )
scatter.setTranslation(0, 10)
scatter.setScale(10, 10, 10)
items.insert(0, scatter)
@@ -85,7 +88,8 @@ class TestSceneWindow(TestCaseQt, ParametricTestCase):
# 3D scatter
scatter = sceneWidget.add3DScatter(
- *numpy.random.random(4000).astype(numpy.float32).reshape(4, -1))
+ *numpy.random.random(4000).astype(numpy.float32).reshape(4, -1)
+ )
scatter.setTranslation(10, 10)
scatter.setScale(10, 10, 10)
items.append(scatter)
@@ -93,44 +97,48 @@ class TestSceneWindow(TestCaseQt, ParametricTestCase):
# 3D array of float
volume = sceneWidget.addVolume(
- numpy.arange(10**3, dtype=numpy.float32).reshape(10, 10, 10))
+ numpy.arange(10**3, dtype=numpy.float32).reshape(10, 10, 10)
+ )
volume.setTranslation(0, 0, 10)
volume.setRotation(45, (0, 0, 1))
- volume.addIsosurface(500, 'red')
- volume.getCutPlanes()[0].getColormap().setName('viridis')
+ volume.addIsosurface(500, "red")
+ volume.getCutPlanes()[0].getColormap().setName("viridis")
items.append(volume)
self.assertEqual(sceneWidget.getItems(), tuple(items))
# 3D array of complex
volume = sceneWidget.addVolume(
- numpy.arange(10**3).reshape(10, 10, 10).astype(numpy.complex64))
+ numpy.arange(10**3).reshape(10, 10, 10).astype(numpy.complex64)
+ )
volume.setTranslation(10, 0, 10)
volume.setRotation(45, (0, 0, 1))
volume.setComplexMode(volume.ComplexMode.REAL)
- volume.addIsosurface(500, (1., 0., 0., .5))
+ volume.addIsosurface(500, (1.0, 0.0, 0.0, 0.5))
items.append(volume)
self.assertEqual(sceneWidget.getItems(), tuple(items))
- sceneWidget.resetZoom('front')
+ sceneWidget.resetZoom("front")
self.qapp.processEvents()
def testHeightMap(self):
"""Test height map items"""
sceneWidget = self.window.getSceneWidget()
- height = numpy.arange(10000).reshape(100, 100) /100.
+ height = numpy.arange(10000).reshape(100, 100) / 100.0
for shape in ((100, 100), (4, 5), (150, 20), (110, 110)):
with self.subTest(shape=shape):
items = []
# Colormapped data height map
- data = numpy.arange(numpy.prod(shape)).astype(numpy.float32).reshape(shape)
+ data = (
+ numpy.arange(numpy.prod(shape)).astype(numpy.float32).reshape(shape)
+ )
heightmap = HeightMapData()
heightmap.setData(height)
heightmap.setColormappedData(data)
- heightmap.getColormap().setName('viridis')
+ heightmap.getColormap().setName("viridis")
items.append(heightmap)
sceneWidget.addItem(heightmap)
@@ -141,12 +149,12 @@ class TestSceneWindow(TestCaseQt, ParametricTestCase):
heightmap = HeightMapRGBA()
heightmap.setData(height)
heightmap.setColorData(colors)
- heightmap.setTranslation(100., 0., 0.)
+ heightmap.setTranslation(100.0, 0.0, 0.0)
items.append(heightmap)
sceneWidget.addItem(heightmap)
self.assertEqual(sceneWidget.getItems(), tuple(items))
- sceneWidget.resetZoom('front')
+ sceneWidget.resetZoom("front")
self.qapp.processEvents()
sceneWidget.clearItems()
@@ -202,17 +210,18 @@ class TestSceneWindow(TestCaseQt, ParametricTestCase):
def testInteractiveMode(self):
"""Test changing interactive mode"""
sceneWidget = self.window.getSceneWidget()
- center = numpy.array((sceneWidget.width() //2, sceneWidget.height() // 2))
+ center = numpy.array((sceneWidget.width() // 2, sceneWidget.height() // 2))
self.mouseMove(sceneWidget, pos=center)
self.mouseClick(sceneWidget, qt.Qt.LeftButton, pos=center)
volume = sceneWidget.addVolume(
- numpy.arange(10**3).astype(numpy.float32).reshape(10, 10, 10))
- sceneWidget.selection().setCurrentItem( volume.getCutPlanes()[0])
- sceneWidget.resetZoom('side')
+ numpy.arange(10**3).astype(numpy.float32).reshape(10, 10, 10)
+ )
+ sceneWidget.selection().setCurrentItem(volume.getCutPlanes()[0])
+ sceneWidget.resetZoom("side")
- for mode in (None, 'rotate', 'pan', 'panSelectedPlane'):
+ for mode in (None, "rotate", "pan", "panSelectedPlane"):
with self.subTest(mode=mode):
sceneWidget.setInteractiveMode(mode)
self.qapp.processEvents()
@@ -220,14 +229,14 @@ class TestSceneWindow(TestCaseQt, ParametricTestCase):
self.mouseMove(sceneWidget, pos=center)
self.mousePress(sceneWidget, qt.Qt.LeftButton, pos=center)
- self.mouseMove(sceneWidget, pos=center-10)
- self.mouseMove(sceneWidget, pos=center-20)
- self.mouseRelease(sceneWidget, qt.Qt.LeftButton, pos=center-20)
+ self.mouseMove(sceneWidget, pos=center - 10)
+ self.mouseMove(sceneWidget, pos=center - 20)
+ self.mouseRelease(sceneWidget, qt.Qt.LeftButton, pos=center - 20)
self.keyPress(sceneWidget, qt.Qt.Key_Control)
self.mouseMove(sceneWidget, pos=center)
self.mousePress(sceneWidget, qt.Qt.LeftButton, pos=center)
- self.mouseMove(sceneWidget, pos=center-10)
- self.mouseMove(sceneWidget, pos=center-20)
- self.mouseRelease(sceneWidget, qt.Qt.LeftButton, pos=center-20)
+ self.mouseMove(sceneWidget, pos=center - 10)
+ self.mouseMove(sceneWidget, pos=center - 20)
+ self.mouseRelease(sceneWidget, qt.Qt.LeftButton, pos=center - 20)
self.keyRelease(sceneWidget, qt.Qt.Key_Control)
diff --git a/src/silx/gui/plot3d/test/testStatsWidget.py b/src/silx/gui/plot3d/test/testStatsWidget.py
index d452eb5..71dcbd9 100644
--- a/src/silx/gui/plot3d/test/testStatsWidget.py
+++ b/src/silx/gui/plot3d/test/testStatsWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2019 European Synchrotron Radiation Facility
@@ -28,7 +27,7 @@ __license__ = "MIT"
__date__ = "25/01/2019"
-import unittest
+import pytest
import numpy
@@ -43,6 +42,7 @@ from silx.gui.plot3d.ScalarFieldView import ScalarFieldView
from silx.gui.plot3d.SceneWidget import SceneWidget, items
+@pytest.mark.usefixtures("use_opengl")
class TestSceneWidget(TestCaseQt, ParametricTestCase):
"""Tests StatsWidget combined with SceneWidget"""
@@ -72,18 +72,19 @@ class TestSceneWidget(TestCaseQt, ParametricTestCase):
# Data image
image = self.sceneWidget.addImage(numpy.arange(100).reshape(10, 10))
- image.setLabel('Image')
+ image.setLabel("Image")
# RGB image
imageRGB = self.sceneWidget.addImage(
- numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3))
- imageRGB.setLabel('RGB Image')
+ numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3)
+ )
+ imageRGB.setLabel("RGB Image")
# 2D scatter
data = numpy.arange(100)
scatter2D = self.sceneWidget.add2DScatter(x=data, y=data, value=data)
- scatter2D.setLabel('2D Scatter')
+ scatter2D.setLabel("2D Scatter")
# 3D scatter
scatter3D = self.sceneWidget.add3DScatter(x=data, y=data, z=data, value=data)
- scatter3D.setLabel('3D Scatter')
+ scatter3D.setLabel("3D Scatter")
# Add a group
group = items.GroupItem()
self.sceneWidget.addItem(group)
@@ -91,7 +92,7 @@ class TestSceneWidget(TestCaseQt, ParametricTestCase):
data = numpy.arange(64**3).reshape(64, 64, 64)
scalarField = items.ScalarField3D()
scalarField.setData(data, copy=False)
- scalarField.setLabel('3D Scalar field')
+ scalarField.setLabel("3D Scalar field")
group.addItem(scalarField)
statsTable = self.statsWidget._getStatsTable()
@@ -104,7 +105,7 @@ class TestSceneWidget(TestCaseQt, ParametricTestCase):
self.assertEqual(statsTable.rowCount(), 0)
for item in (image, scatter2D, scatter3D, scalarField):
- with self.subTest('selection only', item=item.getLabel()):
+ with self.subTest("selection only", item=item.getLabel()):
self.sceneWidget.selection().setCurrentItem(item)
self.assertEqual(statsTable.rowCount(), 1)
self._checkItem(item)
@@ -114,7 +115,7 @@ class TestSceneWidget(TestCaseQt, ParametricTestCase):
self.assertEqual(statsTable.rowCount(), 4)
for item in (image, scatter2D, scatter3D, scalarField):
- with self.subTest('all items', item=item.getLabel()):
+ with self.subTest("all items", item=item.getLabel()):
self._checkItem(item)
def _checkItem(self, item):
@@ -130,9 +131,9 @@ class TestSceneWidget(TestCaseQt, ParametricTestCase):
statsTable = self.statsWidget._getStatsTable()
tableItems = statsTable._itemToTableItems(item)
self.assertTrue(len(tableItems) > 0)
- self.assertEqual(tableItems['legend'].text(), item.getLabel())
- self.assertEqual(float(tableItems['min'].text()), numpy.min(data))
- self.assertEqual(float(tableItems['max'].text()), numpy.max(data))
+ self.assertEqual(tableItems["legend"].text(), item.getLabel())
+ self.assertEqual(float(tableItems["min"].text()), numpy.min(data))
+ self.assertEqual(float(tableItems["max"].text()), numpy.max(data))
# TODO
@@ -192,10 +193,19 @@ class TestScalarFieldView(TestCaseQt):
self.assertEqual(statsTable.rowCount(), 1)
for column in range(statsTable.columnCount()):
- self.assertEqual(float(self._getTextFor(0, 'min')), numpy.min(data))
- self.assertEqual(float(self._getTextFor(0, 'max')), numpy.max(data))
+ self.assertEqual(float(self._getTextFor(0, "min")), numpy.min(data))
+ self.assertEqual(float(self._getTextFor(0, "max")), numpy.max(data))
sum_ = numpy.sum(data)
- comz = numpy.sum(numpy.arange(data.shape[0]) * numpy.sum(data, axis=(1, 2))) / sum_
- comy = numpy.sum(numpy.arange(data.shape[1]) * numpy.sum(data, axis=(0, 2))) / sum_
- comx = numpy.sum(numpy.arange(data.shape[2]) * numpy.sum(data, axis=(0, 1))) / sum_
- self.assertEqual(self._getTextFor(0, 'COM'), str((comx, comy, comz)))
+ comz = (
+ numpy.sum(numpy.arange(data.shape[0]) * numpy.sum(data, axis=(1, 2)))
+ / sum_
+ )
+ comy = (
+ numpy.sum(numpy.arange(data.shape[1]) * numpy.sum(data, axis=(0, 2)))
+ / sum_
+ )
+ comx = (
+ numpy.sum(numpy.arange(data.shape[2]) * numpy.sum(data, axis=(0, 1)))
+ / sum_
+ )
+ self.assertEqual(self._getTextFor(0, "COM"), str((comx, comy, comz)))
diff --git a/src/silx/gui/plot3d/tools/GroupPropertiesWidget.py b/src/silx/gui/plot3d/tools/GroupPropertiesWidget.py
index 146c2cd..11f45cc 100644
--- a/src/silx/gui/plot3d/tools/GroupPropertiesWidget.py
+++ b/src/silx/gui/plot3d/tools/GroupPropertiesWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
""":class:`GroupPropertiesWidget` allows to reset properties in a GroupItem."""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
@@ -59,16 +56,16 @@ class GroupPropertiesWidget(qt.QWidget):
self.setLayout(layout)
# Colormap
- colormapButton = qt.QPushButton('Set...')
+ colormapButton = qt.QPushButton("Set...")
colormapButton.setToolTip("Set colormap for all items")
colormapButton.clicked.connect(self._colormapButtonClicked)
- layout.addRow('Colormap', colormapButton)
+ layout.addRow("Colormap", colormapButton)
self._markerComboBox = qt.QComboBox(self)
self._markerComboBox.addItems(SymbolMixIn.getSupportedSymbolNames())
# Marker
- markerButton = qt.QPushButton('Set')
+ markerButton = qt.QPushButton("Set")
markerButton.setToolTip("Set marker for all items")
markerButton.clicked.connect(self._markerButtonClicked)
@@ -77,7 +74,7 @@ class GroupPropertiesWidget(qt.QWidget):
markerLayout.addWidget(self._markerComboBox, 1)
markerLayout.addWidget(markerButton, 0)
- layout.addRow('Marker', markerLayout)
+ layout.addRow("Marker", markerLayout)
# Marker size
self._markerSizeSlider = qt.QSlider()
@@ -86,18 +83,18 @@ class GroupPropertiesWidget(qt.QWidget):
self._markerSizeSlider.setRange(1, self.MAX_MARKER_SIZE)
self._markerSizeSlider.setValue(1)
- markerSizeButton = qt.QPushButton('Set')
+ markerSizeButton = qt.QPushButton("Set")
markerSizeButton.setToolTip("Set marker size for all items")
markerSizeButton.clicked.connect(self._markerSizeButtonClicked)
markerSizeLayout = qt.QHBoxLayout()
markerSizeLayout.setContentsMargins(0, 0, 0, 0)
- markerSizeLayout.addWidget(qt.QLabel('1'))
+ markerSizeLayout.addWidget(qt.QLabel("1"))
markerSizeLayout.addWidget(self._markerSizeSlider, 1)
markerSizeLayout.addWidget(qt.QLabel(str(self.MAX_MARKER_SIZE)))
markerSizeLayout.addWidget(markerSizeButton, 0)
- layout.addRow('Marker Size', markerSizeLayout)
+ layout.addRow("Marker Size", markerSizeLayout)
# Line width
self._lineWidthSlider = qt.QSlider()
@@ -106,18 +103,18 @@ class GroupPropertiesWidget(qt.QWidget):
self._lineWidthSlider.setRange(1, self.MAX_LINE_WIDTH)
self._lineWidthSlider.setValue(1)
- lineWidthButton = qt.QPushButton('Set')
+ lineWidthButton = qt.QPushButton("Set")
lineWidthButton.setToolTip("Set line width for all items")
lineWidthButton.clicked.connect(self._lineWidthButtonClicked)
lineWidthLayout = qt.QHBoxLayout()
lineWidthLayout.setContentsMargins(0, 0, 0, 0)
- lineWidthLayout.addWidget(qt.QLabel('1'))
+ lineWidthLayout.addWidget(qt.QLabel("1"))
lineWidthLayout.addWidget(self._lineWidthSlider, 1)
lineWidthLayout.addWidget(qt.QLabel(str(self.MAX_LINE_WIDTH)))
lineWidthLayout.addWidget(lineWidthButton, 0)
- layout.addRow('Line Width', lineWidthLayout)
+ layout.addRow("Line Width", lineWidthLayout)
self._colormapDialog = None # To store dialog
self._colormap = Colormap()
@@ -162,7 +159,8 @@ class GroupPropertiesWidget(qt.QWidget):
itemCmap.setColormapLUT(colormap.getColormapLUT())
itemCmap.setNormalization(colormap.getNormalization())
itemCmap.setGammaNormalizationParameter(
- colormap.getGammaNormalizationParameter())
+ colormap.getGammaNormalizationParameter()
+ )
itemCmap.setVRange(colormap.getVMin(), colormap.getVMax())
else:
# Reset colormap
@@ -198,5 +196,5 @@ class GroupPropertiesWidget(qt.QWidget):
lineWidth = self._lineWidthSlider.value()
for item in group.visit():
- if hasattr(item, 'setLineWidth'):
+ if hasattr(item, "setLineWidth"):
item.setLineWidth(lineWidth)
diff --git a/src/silx/gui/plot3d/tools/PositionInfoWidget.py b/src/silx/gui/plot3d/tools/PositionInfoWidget.py
index 99d6356..bffe952 100644
--- a/src/silx/gui/plot3d/tools/PositionInfoWidget.py
+++ b/src/silx/gui/plot3d/tools/PositionInfoWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides a widget that displays data values of a SceneWidget.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "01/10/2018"
@@ -58,18 +55,19 @@ class PositionInfoWidget(qt.QWidget):
self.setToolTip("Double-click on a data point to show its value")
layout = qt.QBoxLayout(qt.QBoxLayout.LeftToRight, self)
- self._xLabel = self._addInfoField('X')
- self._yLabel = self._addInfoField('Y')
- self._zLabel = self._addInfoField('Z')
- self._dataLabel = self._addInfoField('Data')
- self._itemLabel = self._addInfoField('Item')
+ self._xLabel = self._addInfoField("X")
+ self._yLabel = self._addInfoField("Y")
+ self._zLabel = self._addInfoField("Z")
+ self._dataLabel = self._addInfoField("Data")
+ self._itemLabel = self._addInfoField("Item")
layout.addStretch(1)
self._action = actions.mode.PickingModeAction(parent=self)
- self._action.setText('Selection')
+ self._action.setText("Selection")
self._action.setToolTip(
- 'Toggle selection information update with left button click')
+ "Toggle selection information update with left button click"
+ )
self._action.sigSceneClicked.connect(self.pick)
self._action.changed.connect(self.__actionChanged)
self._action.setChecked(False) # Disabled by default
@@ -97,14 +95,14 @@ class PositionInfoWidget(qt.QWidget):
subLayout = qt.QHBoxLayout()
subLayout.setContentsMargins(0, 0, 0, 0)
- subLayout.addWidget(qt.QLabel(label + ':'))
+ subLayout.addWidget(qt.QLabel(label + ":"))
- widget = qt.QLabel('-')
+ widget = qt.QLabel("-")
widget.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignVCenter)
widget.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
metrics = widget.fontMetrics()
- if qt.BINDING in ('PySide2', 'PyQt5'):
+ if qt.BINDING == "PyQt5":
width = metrics.width("#######")
else: # Qt6
width = metrics.horizontalAdvance("#######")
@@ -142,22 +140,29 @@ class PositionInfoWidget(qt.QWidget):
def clear(self):
"""Clean-up displayed values"""
- for widget in (self._xLabel, self._yLabel, self._zLabel,
- self._dataLabel, self._itemLabel):
- widget.setText('-')
-
- _SUPPORTED_ITEMS = (items.Scatter3D,
- items.Scatter2D,
- items.ImageData,
- items.ImageRgba,
- items.HeightMapData,
- items.HeightMapRGBA,
- items.Mesh,
- items.Box,
- items.Cylinder,
- items.Hexagon,
- volume.CutPlane,
- volume.Isosurface)
+ for widget in (
+ self._xLabel,
+ self._yLabel,
+ self._zLabel,
+ self._dataLabel,
+ self._itemLabel,
+ ):
+ widget.setText("-")
+
+ _SUPPORTED_ITEMS = (
+ items.Scatter3D,
+ items.Scatter2D,
+ items.ImageData,
+ items.ImageRgba,
+ items.HeightMapData,
+ items.HeightMapRGBA,
+ items.Mesh,
+ items.Box,
+ items.Cylinder,
+ items.Hexagon,
+ volume.CutPlane,
+ volume.Isosurface,
+ )
"""Type of items that are picked"""
def _isSupportedItem(self, item):
@@ -180,15 +185,14 @@ class PositionInfoWidget(qt.QWidget):
sceneWidget = self.getSceneWidget()
if sceneWidget is None: # No associated widget
- _logger.info('Picking without associated SceneWidget')
+ _logger.info("Picking without associated SceneWidget")
return
# Find closest (and latest in the tree) supported item
- closestNdcZ = float('inf')
+ closestNdcZ = float("inf")
picking = None
- for result in sceneWidget.pickItems(x, y,
- condition=self._isSupportedItem):
- ndcZ = result.getPositions('ndc', copy=False)[0, 2]
+ for result in sceneWidget.pickItems(x, y, condition=self._isSupportedItem):
+ ndcZ = result.getPositions("ndc", copy=False)[0, 2]
if ndcZ <= closestNdcZ:
closestNdcZ = ndcZ
picking = result
@@ -198,7 +202,7 @@ class PositionInfoWidget(qt.QWidget):
item = picking.getItem()
self._itemLabel.setText(item.getLabel())
- positions = picking.getPositions('scene', copy=False)
+ positions = picking.getPositions("scene", copy=False)
x, y, z = positions[0]
self._xLabel.setText("%g" % x)
self._yLabel.setText("%g" % y)
@@ -207,8 +211,8 @@ class PositionInfoWidget(qt.QWidget):
data = picking.getData(copy=False)
if data is not None:
data = data[0]
- if hasattr(data, '__len__'):
- text = ' '.join(["%.3g"] * len(data)) % tuple(data)
+ if hasattr(data, "__len__"):
+ text = " ".join(["%.3g"] * len(data)) % tuple(data)
else:
text = "%g" % data
self._dataLabel.setText(text)
@@ -217,7 +221,7 @@ class PositionInfoWidget(qt.QWidget):
"""Update information according to cursor position"""
widget = self.getSceneWidget()
if widget is None:
- _logger.info('Update without associated SceneWidget')
+ _logger.info("Update without associated SceneWidget")
self.clear()
return
diff --git a/src/silx/gui/plot3d/tools/ViewpointTools.py b/src/silx/gui/plot3d/tools/ViewpointTools.py
index 0607382..3554972 100644
--- a/src/silx/gui/plot3d/tools/ViewpointTools.py
+++ b/src/silx/gui/plot3d/tools/ViewpointTools.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""This module provides a toolbar to control Plot3DWidget viewpoint."""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "08/09/2017"
@@ -60,8 +57,8 @@ class ViewpointToolButton(qt.QToolButton):
self.setMenu(menu)
self.setPopupMode(qt.QToolButton.InstantPopup)
- self.setIcon(getQIcon('cube'))
- self.setToolTip('Reset the viewpoint to a defined position')
+ self.setIcon(getQIcon("cube"))
+ self.setToolTip("Reset the viewpoint to a defined position")
def setPlot3DWidget(self, widget):
"""Set the Plot3DWidget this toolbar is associated with
diff --git a/src/silx/gui/plot3d/tools/__init__.py b/src/silx/gui/plot3d/tools/__init__.py
index c8b8d21..5e2c76c 100644
--- a/src/silx/gui/plot3d/tools/__init__.py
+++ b/src/silx/gui/plot3d/tools/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot3d/tools/test/__init__.py b/src/silx/gui/plot3d/tools/test/__init__.py
index 86741ed..a6032b9 100644
--- a/src/silx/gui/plot3d/tools/test/__init__.py
+++ b/src/silx/gui/plot3d/tools/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py b/src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py
index 17fb3db..ae95fca 100644
--- a/src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py
+++ b/src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ __license__ = "MIT"
__date__ = "03/10/2018"
-import unittest
-
import numpy
from silx.gui.utils.testutils import TestCaseQt
@@ -69,12 +66,11 @@ class TestPositionInfoWidget(TestCaseQt):
def test(self):
"""Test PositionInfoWidget"""
- self.assertIs(self.positionInfoWidget.getSceneWidget(),
- self.sceneWidget)
+ self.assertIs(self.positionInfoWidget.getSceneWidget(), self.sceneWidget)
data = numpy.arange(100)
self.sceneWidget.add2DScatter(x=data, y=data, value=data)
- self.sceneWidget.resetZoom('front')
+ self.sceneWidget.resetZoom("front")
# Double click at the center
self.mouseDClick(self.sceneWidget, button=qt.Qt.LeftButton)
diff --git a/src/silx/gui/plot3d/tools/toolbars.py b/src/silx/gui/plot3d/tools/toolbars.py
index d4f32db..152e548 100644
--- a/src/silx/gui/plot3d/tools/toolbars.py
+++ b/src/silx/gui/plot3d/tools/toolbars.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
@@ -37,8 +36,6 @@ It provides the following toolbars:
- Print
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "06/09/2017"
@@ -61,7 +58,7 @@ class Plot3DWidgetToolBar(qt.QToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, title=''):
+ def __init__(self, parent=None, title=""):
super(Plot3DWidgetToolBar, self).__init__(title, parent)
self._plot3DRef = None
@@ -100,7 +97,7 @@ class InteractiveModeToolBar(Plot3DWidgetToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, title='Plot3D Interaction'):
+ def __init__(self, parent=None, title="Plot3D Interaction"):
super(InteractiveModeToolBar, self).__init__(parent, title)
self._rotateAction = actions.mode.RotateArcballAction(parent=self)
@@ -131,7 +128,7 @@ class OutputToolBar(Plot3DWidgetToolBar):
:param str title: Title of the toolbar.
"""
- def __init__(self, parent=None, title='Plot3D Output'):
+ def __init__(self, parent=None, title="Plot3D Output"):
super(OutputToolBar, self).__init__(parent, title)
self._copyAction = actions.io.CopyAction(parent=self)
@@ -182,7 +179,7 @@ class ViewpointToolBar(Plot3DWidgetToolBar):
:param str title: Title of the toolbar
"""
- def __init__(self, parent=None, title='Viewpoint control'):
+ def __init__(self, parent=None, title="Viewpoint control"):
super(ViewpointToolBar, self).__init__(parent, title)
self._viewpointToolButton = ViewpointToolButton(parent=self)
diff --git a/src/silx/gui/plot3d/utils/__init__.py b/src/silx/gui/plot3d/utils/__init__.py
index 99d3e08..3cf3825 100644
--- a/src/silx/gui/plot3d/utils/__init__.py
+++ b/src/silx/gui/plot3d/utils/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/plot3d/utils/mng.py b/src/silx/gui/plot3d/utils/mng.py
index 8049a2f..3c63266 100644
--- a/src/silx/gui/plot3d/utils/mng.py
+++ b/src/silx/gui/plot3d/utils/mng.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ It only supports RGB888 images of the same shape stored as
MNG-VLC (very low complexity) format.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/12/2016"
@@ -50,10 +47,10 @@ def _png_chunk(name, data):
:param str name: Chunk type
:param byte data: Chunk payload
"""
- length = struct.pack('>I', len(data))
- name = [char.encode('ascii') for char in name]
- chunk = struct.pack('cccc', *name) + data
- crc = struct.pack('>I', zlib.crc32(chunk) & 0xffffffff)
+ length = struct.pack(">I", len(data))
+ name = [char.encode("ascii") for char in name]
+ chunk = struct.pack("cccc", *name) + data
+ crc = struct.pack(">I", zlib.crc32(chunk) & 0xFFFFFFFF)
return length + chunk + crc
@@ -79,43 +76,46 @@ def convert(images, nb_images=0, fps=25):
height, width = image.shape[:2]
# MNG signature
- yield b'\x8aMNG\r\n\x1a\n'
+ yield b"\x8aMNG\r\n\x1a\n"
# MHDR chunk: File header
- yield _png_chunk('MHDR', struct.pack(
- ">IIIIIII",
- width,
- height,
- fps, # ticks
- nb_images + 1, # layer count
- nb_images, # frame count
- nb_images, # play time
- 1)) # profile: MNG-VLC no alpha: only least significant bit 1
+ yield _png_chunk(
+ "MHDR",
+ struct.pack(
+ ">IIIIIII",
+ width,
+ height,
+ fps, # ticks
+ nb_images + 1, # layer count
+ nb_images, # frame count
+ nb_images, # play time
+ 1,
+ ),
+ ) # profile: MNG-VLC no alpha: only least significant bit 1
assert image.shape == (height, width, 3)
- assert image.dtype == numpy.dtype('uint8')
+ assert image.dtype == numpy.dtype("uint8")
# IHDR chunk: Image header
depth = 8 # 8 bit per channel
color_type = 2 # 'truecolor' = RGB
interlace = 0 # No
- yield _png_chunk('IHDR', struct.pack(">IIBBBBB",
- width,
- height,
- depth,
- color_type,
- 0, 0, interlace))
+ yield _png_chunk(
+ "IHDR",
+ struct.pack(">IIBBBBB", width, height, depth, color_type, 0, 0, interlace),
+ )
# Add filter 'None' before each scanline
- prepared_data = b'\x00' + b'\x00'.join(
- line.tobytes() for line in image) # TODO optimize that
+ prepared_data = b"\x00" + b"\x00".join(
+ line.tobytes() for line in image
+ ) # TODO optimize that
compressed_data = zlib.compress(prepared_data, 8)
# IDAT chunk: Payload
- yield _png_chunk('IDAT', compressed_data)
+ yield _png_chunk("IDAT", compressed_data)
# IEND chunk: Image footer
- yield _png_chunk('IEND', b'')
+ yield _png_chunk("IEND", b"")
# MEND chunk: footer
- yield _png_chunk('MEND', b'')
+ yield _png_chunk("MEND", b"")
diff --git a/src/silx/gui/printer.py b/src/silx/gui/printer.py
index 761fa0f..c0af97f 100644
--- a/src/silx/gui/printer.py
+++ b/src/silx/gui/printer.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides a singleton QPrinter used by default by silx widgets.
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "01/03/2018"
diff --git a/src/silx/gui/qt/__init__.py b/src/silx/gui/qt/__init__.py
index 915c89b..675a178 100644
--- a/src/silx/gui/qt/__init__.py
+++ b/src/silx/gui/qt/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
@@ -25,11 +24,13 @@
"""Common wrapper over Python Qt bindings:
- `PyQt5 <http://pyqt.sourceforge.net/Docs/PyQt5/>`_
-- `PySide2 <https://pypi.org/project/PySide2/>`_
- `PySide6 <https://pypi.org/project/PySide6/>`_
+- `PyQt6 <https://pypi.org/project/PyQt6/>`_
-If a Qt binding is already loaded, it will use it, otherwise the different
-Qt bindings are tried in this order: PyQt5, PySide2, PySide6.
+If a Qt binding is already loaded, it will be used.
+If the `QT_API` environment variable is set to one of the supported Qt bindings
+(case insensitive), this binding is loaded if available, otherwise the
+different Qt bindings are tried in this order: PyQt5, PySide6, PyQt6.
The name of the loaded Qt binding is stored in the BINDING variable.
@@ -48,7 +49,8 @@ see `qtpy <https://pypi.org/project/QtPy/>`_.
"""
from ._qt import * # noqa
-if BINDING in ('PySide2', 'PySide6'):
+
+if BINDING == "PySide6":
# Import loadUi wrapper
- from ._pyside_dynamic import loadUi # noqa
+ from ._pyside_dynamic import loadUi # noqa
from ._utils import * # noqa
diff --git a/src/silx/gui/qt/_pyqt6.py b/src/silx/gui/qt/_pyqt6.py
new file mode 100644
index 0000000..4f28d40
--- /dev/null
+++ b/src/silx/gui/qt/_pyqt6.py
@@ -0,0 +1,79 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2021 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.
+#
+# ###########################################################################*/
+"""PyQt6 backward compatibility patching"""
+
+__authors__ = ["Thomas VINCENT"]
+__license__ = "MIT"
+__date__ = "02/09/2021"
+
+import enum
+import logging
+
+import PyQt6.sip
+
+
+_logger = logging.getLogger(__name__)
+
+
+def patch_enums(*modules):
+ """Patch PyQt6 modules to provide backward compatibility of enum values
+
+ :param modules: Modules to patch (e.g., PyQt6.QtCore).
+ """
+ for module in modules:
+ for clsName in dir(module):
+ cls = getattr(module, clsName, None)
+ if not isinstance(cls, PyQt6.sip.wrappertype) or not clsName.startswith(
+ "Q"
+ ):
+ continue
+
+ for qenumName in dir(cls):
+ if not qenumName[0].isupper():
+ continue
+ # Special cases to avoid overrides and mimic PySide6
+ if clsName == "QColorSpace" and qenumName in (
+ "Primaries",
+ "TransferFunction",
+ ):
+ continue
+ if qenumName in ("DeviceType", "PointerType"):
+ continue
+
+ qenum = getattr(cls, qenumName)
+ if not isinstance(qenum, enum.EnumMeta):
+ continue
+
+ if any(
+ map(
+ lambda ancestor: isinstance(ancestor, PyQt6.sip.wrappertype)
+ and qenum is getattr(ancestor, qenumName, None),
+ cls.__mro__[1:],
+ )
+ ):
+ continue # Only handle it once in case of inheritance
+
+ for name, value in qenum.__members__.items():
+ setattr(cls, name, value)
diff --git a/src/silx/gui/qt/_pyside_dynamic.py b/src/silx/gui/qt/_pyside_dynamic.py
index a841eae..4c1ceba 100644
--- a/src/silx/gui/qt/_pyside_dynamic.py
+++ b/src/silx/gui/qt/_pyside_dynamic.py
@@ -1,27 +1,58 @@
-# -*- coding: utf-8 -*-
-
-# Taken from: https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
-# Plus: https://github.com/spyder-ide/qtpy/commit/001a862c401d757feb63025f88dbb4601d353c84
-
+# Adapted from https://github.com/spyder-ide/qtpy/blob/296dee3da8aba381b3cf17da34a6d17626e50357/qtpy/uic.py
+# In PySide, loadUi does not exist, so we define it using QUiLoader, and
+# then make sure we expose that function. This is adapted from qt-helpers
+# which was released under a 3-clause BSD license:
+# qt-helpers - a common front-end to various Qt modules
+#
+# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of the Glue project nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# Which itself was based on the solution at
+#
+# https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
+#
+# which was released under the MIT license:
+#
# Copyright (c) 2011 Sebastian Wiesner <lunaryorn@gmail.com>
# Modifications by Charl Botha <cpbotha@vxlabs.com>
-# * customWidgets support (registerCustomWidget() causes segfault in
-# pyside 1.1.2 on Ubuntu 12.04 x86_64)
-# * workingDirectory support in loadUi
-
-# found this here:
-# https://github.com/lunaryorn/snippets/blob/master/qt4/designer/pyside_dynamic.py
-
+#
# 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
@@ -30,40 +61,68 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
-"""
- How to load a user interface dynamically with PySide.
-
- .. moduleauthor:: Sebastian Wiesner <lunaryorn@gmail.com>
-"""
+"""How to load a user interface dynamically with PySide6"""
import logging
from ._qt import BINDING
-if BINDING == 'PySide2':
- from PySide2.QtCore import QMetaObject, Property, Qt
- from PySide2.QtWidgets import QFrame
- from PySide2.QtUiTools import QUiLoader
-elif BINDING == 'PySide6':
- from PySide6.QtCore import QMetaObject, Property, Qt
- from PySide6.QtWidgets import QFrame
- from PySide6.QtUiTools import QUiLoader
-else:
+
+if BINDING != "PySide6":
raise RuntimeError("Unsupported Qt binding: %s", BINDING)
+from PySide6.QtCore import QMetaObject, Property, Qt
+from PySide6.QtWidgets import QFrame
+from PySide6.QtUiTools import QUiLoader
+
_logger = logging.getLogger(__name__)
+# Specific custom widgets
+
+
+class _Line(QFrame):
+ """Widget to use as 'Line' Qt designer"""
+
+ def __init__(self, parent=None):
+ super(_Line, self).__init__(parent)
+ self.setFrameShape(QFrame.HLine)
+ self.setFrameShadow(QFrame.Sunken)
+
+ def getOrientation(self):
+ shape = self.frameShape()
+ if shape == QFrame.HLine:
+ return Qt.Horizontal
+ elif shape == QFrame.VLine:
+ return Qt.Vertical
+ else:
+ raise RuntimeError("Wrong shape: %d", shape)
+
+ def setOrientation(self, orientation):
+ if orientation == Qt.Horizontal:
+ self.setFrameShape(QFrame.HLine)
+ elif orientation == Qt.Vertical:
+ self.setFrameShape(QFrame.VLine)
+ else:
+ raise ValueError("Unsupported orientation %s" % str(orientation))
+
+ orientation = Property("Qt::Orientation", getOrientation, setOrientation)
+
+
+CUSTOM_WIDGETS = {"Line": _Line}
+"""Default custom widgets for `loadUi`"""
+
+
class UiLoader(QUiLoader):
"""
- Subclass :class:`~PySide.QtUiTools.QUiLoader` to create the user interface
- in a base instance.
+ Subclass of :class:`~PySide.QtUiTools.QUiLoader` to create the user
+ interface in a base instance.
Unlike :class:`~PySide.QtUiTools.QUiLoader` itself this class does not
create a new instance of the top-level widget, but creates the user
- interface in an existing instance of the top-level class.
+ interface in an existing instance of the top-level class if needed.
- This mimics the behaviour of :func:`PyQt*.uic.loadUi`.
+ This mimics the behaviour of :func:`PyQt4.uic.loadUi`.
"""
def __init__(self, baseinstance, customWidgets=None):
@@ -75,129 +134,91 @@ class UiLoader(QUiLoader):
subclass thereof.
``customWidgets`` is a dictionary mapping from class name to class
- object for widgets that you've promoted in the Qt Designer
- interface. Usually, this should be done by calling
- registerCustomWidget on the QUiLoader, but
- with PySide 1.1.2 on Ubuntu 12.04 x86_64 this causes a segfault.
+ object for custom widgets. Usually, this should be done by calling
+ registerCustomWidget on the QUiLoader, but with PySide 1.1.2 on
+ Ubuntu 12.04 x86_64 this causes a segfault.
``parent`` is the parent object of this loader.
"""
QUiLoader.__init__(self, baseinstance)
+
self.baseinstance = baseinstance
- self.customWidgets = {}
- self.uifile = None
- self.customWidgets.update(customWidgets)
- def createWidget(self, class_name, parent=None, name=''):
+ if customWidgets is None:
+ self.customWidgets = {}
+ else:
+ self.customWidgets = customWidgets
+
+ def createWidget(self, class_name, parent=None, name=""):
"""
Function that is called for each widget defined in ui file,
overridden here to populate baseinstance instead.
"""
if parent is None and self.baseinstance:
- # supposed to create the top-level widget, return the base instance
- # instead
+ # supposed to create the top-level widget, return the base
+ # instance instead
return self.baseinstance
else:
- if class_name in self.availableWidgets():
+ # For some reason, Line is not in the list of available
+ # widgets, but works fine, so we have to special case it here.
+ if class_name in self.availableWidgets() or class_name == "Line":
# create a new widget for child widgets
widget = QUiLoader.createWidget(self, class_name, parent, name)
else:
- # if not in the list of availableWidgets,
- # must be a custom widget
- # this will raise KeyError if the user has not supplied the
- # relevant class_name in the dictionary, or TypeError, if
- # customWidgets is None
- if class_name not in self.customWidgets:
- raise Exception('No custom widget ' + class_name +
- ' found in customWidgets param of' +
- 'UiFile %s.' % self.uifile)
+ # If not in the list of availableWidgets, must be a custom
+ # widget. This will raise KeyError if the user has not
+ # supplied the relevant class_name in the dictionary or if
+ # customWidgets is empty.
try:
widget = self.customWidgets[class_name](parent)
- except Exception:
- _logger.error("Fail to instanciate widget %s from file %s", class_name, self.uifile)
- raise
+ except KeyError as error:
+ raise Exception(
+ f"No custom widget {class_name} " "found in customWidgets"
+ ) from error
if self.baseinstance:
# set an attribute for the new child widget on the base
- # instance, just like PyQt*.uic.loadUi does.
+ # instance, just like PyQt4.uic.loadUi does.
setattr(self.baseinstance, name, widget)
- # this outputs the various widget names, e.g.
- # sampleGraphicsView, dockWidget, samplesTableView etc.
- # print(name)
-
return widget
- def _parse_custom_widgets(self, ui_file):
- """
- This function is used to parse a ui file and look for the <customwidgets>
- section, then automatically load all the custom widget classes.
- """
- import importlib
- from xml.etree.ElementTree import ElementTree
-
- # Parse the UI file
- etree = ElementTree()
- ui = etree.parse(ui_file)
-
- # Get the customwidgets section
- custom_widgets = ui.find('customwidgets')
-
- if custom_widgets is None:
- return
-
- custom_widget_classes = {}
-
- for custom_widget in custom_widgets.getchildren():
- cw_class = custom_widget.find('class').text
- cw_header = custom_widget.find('header').text
-
- module = importlib.import_module(cw_header)
-
- custom_widget_classes[cw_class] = getattr(module, cw_class)
+def _get_custom_widgets(ui_file):
+ """
+ This function is used to parse a ui file and look for the <customwidgets>
+ section, then automatically load all the custom widget classes.
+ """
- self.customWidgets.update(custom_widget_classes)
+ import sys
+ import importlib
+ from xml.etree.ElementTree import ElementTree
- def load(self, uifile):
- self._parse_custom_widgets(uifile)
- self.uifile = uifile
- return QUiLoader.load(self, uifile)
+ # Parse the UI file
+ etree = ElementTree()
+ ui = etree.parse(ui_file)
+ # Get the customwidgets section
+ custom_widgets = ui.find("customwidgets")
-class _Line(QFrame):
- """Widget to use as 'Line' Qt designer"""
- def __init__(self, parent=None):
- super(_Line, self).__init__(parent)
- self.setFrameShape(QFrame.HLine)
- self.setFrameShadow(QFrame.Sunken)
+ if custom_widgets is None:
+ return {}
- def getOrientation(self):
- shape = self.frameShape()
- if shape == QFrame.HLine:
- return Qt.Horizontal
- elif shape == QFrame.VLine:
- return Qt.Vertical
- else:
- raise RuntimeError("Wrong shape: %d", shape)
+ custom_widget_classes = {}
- def setOrientation(self, orientation):
- if orientation == Qt.Horizontal:
- self.setFrameShape(QFrame.HLine)
- elif orientation == Qt.Vertical:
- self.setFrameShape(QFrame.VLine)
- else:
- raise ValueError("Unsupported orientation %s" % str(orientation))
+ for custom_widget in list(custom_widgets):
+ cw_class = custom_widget.find("class").text
+ cw_header = custom_widget.find("header").text
- orientation = Property("Qt::Orientation", getOrientation, setOrientation)
+ module = importlib.import_module(cw_header)
+ custom_widget_classes[cw_class] = getattr(module, cw_class)
-CUSTOM_WIDGETS = {"Line": _Line}
-"""Default custom widgets for `loadUi`"""
+ return custom_widget_classes
def loadUi(uifile, baseinstance=None, package=None, resource_suffix=None):
@@ -206,30 +227,36 @@ def loadUi(uifile, baseinstance=None, package=None, resource_suffix=None):
``uifile`` is a string containing a file name of the UI file to load.
- If ``baseinstance`` is ``None``, the a new instance of the top-level widget
- will be created. Otherwise, the user interface is created within the given
- ``baseinstance``. In this case ``baseinstance`` must be an instance of the
- top-level widget class in the UI file to load, or a subclass thereof. In
- other words, if you've created a ``QMainWindow`` interface in the designer,
- ``baseinstance`` must be a ``QMainWindow`` or a subclass thereof, too. You
- cannot load a ``QMainWindow`` UI file with a plain
- :class:`~PySide.QtGui.QWidget` as ``baseinstance``.
+ If ``baseinstance`` is ``None``, the a new instance of the top-level
+ widget will be created. Otherwise, the user interface is created within
+ the given ``baseinstance``. In this case ``baseinstance`` must be an
+ instance of the top-level widget class in the UI file to load, or a
+ subclass thereof. In other words, if you've created a ``QMainWindow``
+ interface in the designer, ``baseinstance`` must be a ``QMainWindow``
+ or a subclass thereof, too. You cannot load a ``QMainWindow`` UI file
+ with a plain :class:`~PySide.QtGui.QWidget` as ``baseinstance``.
- :method:`~PySide.QtCore.QMetaObject.connectSlotsByName()` is called on the
- created user interface, so you can implemented your slots according to its
- conventions in your widget class.
+ :method:`~PySide.QtCore.QMetaObject.connectSlotsByName()` is called on
+ the created user interface, so you can implemented your slots according
+ to its conventions in your widget class.
Return ``baseinstance``, if ``baseinstance`` is not ``None``. Otherwise
return the newly created instance of the user interface.
"""
if package is not None:
- _logger.warning(
- "loadUi package parameter not implemented with PySide")
+ _logger.warning("loadUi package parameter not implemented with PySide")
if resource_suffix is not None:
- _logger.warning(
- "loadUi resource_suffix parameter not implemented with PySide")
+ _logger.warning("loadUi resource_suffix parameter not implemented with PySide")
+
+ # We parse the UI file and import any required custom widgets
+ customWidgets = _get_custom_widgets(uifile)
+
+ # Add CUSTOM_WIDGETS
+ for name, klass in CUSTOM_WIDGETS.items():
+ customWidgets.setdefault(name, klass)
+
+ loader = UiLoader(baseinstance, customWidgets)
- loader = UiLoader(baseinstance, customWidgets=CUSTOM_WIDGETS)
widget = loader.load(uifile)
QMetaObject.connectSlotsByName(widget)
return widget
diff --git a/src/silx/gui/qt/_qt.py b/src/silx/gui/qt/_qt.py
index f62f4c8..e069f4b 100644
--- a/src/silx/gui/qt/_qt.py
+++ b/src/silx/gui/qt/_qt.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,22 +25,26 @@
__authors__ = ["V.A. Sole"]
__license__ = "MIT"
-__date__ = "23/05/2018"
+__date__ = "12/01/2022"
+import importlib
import logging
+import os
import sys
import traceback
+from packaging.version import Version
+from silx.utils import deprecation
_logger = logging.getLogger(__name__)
BINDING = None
-"""The name of the Qt binding in use: PyQt5, PySide2, PySide6."""
+"""The name of the Qt binding in use: PyQt5, PySide6, PyQt6."""
QtBinding = None # noqa
-"""The Qt binding module in use: PyQt5, PySide2, PySide6."""
+"""The Qt binding module in use: PyQt5, PySide6, PyQt6."""
HAS_SVG = False
"""True if Qt provides support for Scalable Vector Graphics (QtSVG)."""
@@ -49,39 +52,71 @@ HAS_SVG = False
HAS_OPENGL = False
"""True if Qt provides support for OpenGL (QtOpenGL)."""
-# First check for an already loaded wrapper
-for _binding in ('PySide2', 'PyQt5', 'PySide6'):
- if _binding + '.QtCore' in sys.modules:
- BINDING = _binding
- break
-else: # Then try Qt bindings
- try:
- import PyQt5.QtCore # noqa
- except ImportError:
- if 'PyQt5' in sys.modules:
- del sys.modules["PyQt5"]
- try:
- import PySide2.QtCore # noqa
- except ImportError:
- if 'PySide2' in sys.modules:
- del sys.modules["PySide2"]
+
+def _select_binding() -> str:
+ """Select and load a Qt binding
+
+ Qt binding is selected according to:
+ - Already loaded binding
+ - QT_API environment variable
+ - Bindings order of priority
+
+ :raises ImportError:
+ :returns: Loaded binding
+ """
+ bindings = "PyQt5", "PySide6", "PyQt6"
+
+ envvar = os.environ.get("QT_API", "").lower()
+
+ # First check for an already loaded binding
+ for binding in bindings:
+ if f"{binding}.QtCore" in sys.modules:
+ if envvar and envvar != binding.lower():
+ _logger.warning(
+ f"Cannot satisfy QT_API={envvar} environment variable, {binding} is already loaded"
+ )
+ return binding
+
+ # Check if QT_API can be satisfied
+ if envvar:
+ selection = [b for b in bindings if envvar == b.lower()]
+ if not selection:
+ _logger.warning(f"Environment variable QT_API={envvar} is not supported")
+ else:
+ binding = selection[0]
try:
- import PySide6.QtCore # noqa
+ importlib.import_module(f"{binding}.QtCore")
except ImportError:
- if 'PySide6' in sys.modules:
- del sys.modules["PySide6"]
- raise ImportError(
- 'No Qt wrapper found. Install PyQt5, PySide2, PySide6.')
+ _logger.warning(
+ f"Cannot import {binding} specified by QT_API environment variable"
+ )
else:
- BINDING = 'PySide6'
+ return binding
+
+ # Try to load binding
+ for binding in bindings:
+ try:
+ importlib.import_module(f"{binding}.QtCore")
+ except ImportError:
+ if binding in sys.modules:
+ del sys.modules[binding]
else:
- BINDING = 'PySide2'
- else:
- BINDING = 'PyQt5'
+ return binding
+ raise ImportError("No Qt wrapper found. Install PyQt5, PySide6, PyQt6.")
-if BINDING == 'PyQt5':
- _logger.debug('Using PyQt5 bindings')
+
+BINDING = _select_binding()
+
+
+if BINDING == "PyQt5":
+ _logger.debug("Using PyQt5 bindings")
+ from PyQt5 import QtCore
+
+ if sys.version_info >= (3, 10) and QtCore.PYQT_VERSION < 0x50E02:
+ raise RuntimeError(
+ "PyQt5 v%s is not supported, please upgrade it." % QtCore.PYQT_VERSION_STR
+ )
import PyQt5 as QtBinding # noqa
@@ -116,96 +151,109 @@ if BINDING == 'PyQt5':
# Disable PyQt5's cooperative multi-inheritance since other bindings do not provide it.
# See https://www.riverbankcomputing.com/static/Docs/PyQt5/multiinheritance.html?highlight=inheritance
- class _Foo(object): pass
- class QObject(QObject, _Foo): pass
+ class _Foo(object):
+ pass
+
+ class QObject(QObject, _Foo):
+ pass
+elif BINDING == "PySide6":
+ _logger.debug("Using PySide6 bindings")
-elif BINDING == 'PySide2':
- _logger.debug('Using PySide2 bindings')
+ import PySide6 as QtBinding # noqa
- import PySide2 as QtBinding # noqa
+ if Version(QtBinding.__version__) < Version("6.4"):
+ raise RuntimeError(
+ f"PySide6 v{QtBinding.__version__} is not supported, please upgrade it."
+ )
- from PySide2.QtCore import * # noqa
- from PySide2.QtGui import * # noqa
- from PySide2.QtWidgets import * # noqa
- from PySide2.QtPrintSupport import * # noqa
+ from PySide6.QtCore import * # noqa
+ from PySide6.QtGui import * # noqa
+ from PySide6.QtWidgets import * # noqa
+ from PySide6.QtPrintSupport import * # noqa
try:
- from PySide2.QtOpenGL import * # noqa
+ from PySide6.QtOpenGL import * # noqa
+ from PySide6.QtOpenGLWidgets import QOpenGLWidget # noqa
except ImportError:
- _logger.info("PySide2.QtOpenGL not available")
+ _logger.info("PySide6's QtOpenGL or QtOpenGLWidgets not available")
HAS_OPENGL = False
else:
HAS_OPENGL = True
try:
- from PySide2.QtSvg import * # noqa
+ from PySide6.QtSvg import * # noqa
except ImportError:
- _logger.info("PySide2.QtSvg not available")
+ _logger.info("PySide6.QtSvg not available")
HAS_SVG = False
else:
HAS_SVG = True
pyqtSignal = Signal
- # Qt6 compatibility:
- # with PySide2 `exec` method has a special behavior
- class _ExecMixIn:
- """Mix-in class providind `exec` compatibility"""
- def exec(self, *args, **kwargs):
- return super().exec_(*args, **kwargs)
-
- # QtWidgets
- class QApplication(_ExecMixIn, QApplication): pass
- class QColorDialog(_ExecMixIn, QColorDialog): pass
- class QDialog(_ExecMixIn, QDialog): pass
- class QErrorMessage(_ExecMixIn, QErrorMessage): pass
- class QFileDialog(_ExecMixIn, QFileDialog): pass
- class QFontDialog(_ExecMixIn, QFontDialog): pass
- class QInputDialog(_ExecMixIn, QInputDialog): pass
- class QMenu(_ExecMixIn, QMenu): pass
- class QMessageBox(_ExecMixIn, QMessageBox): pass
- class QProgressDialog(_ExecMixIn, QProgressDialog): pass
- #QtCore
- class QCoreApplication(_ExecMixIn, QCoreApplication): pass
- class QEventLoop(_ExecMixIn, QEventLoop): pass
- if hasattr(QTextStreamManipulator, "exec_"):
- # exec_ only wrapped in PySide2 and NOT in PyQt5
- class QTextStreamManipulator(_ExecMixIn, QTextStreamManipulator): pass
- class QThread(_ExecMixIn, QThread): pass
-
-
-elif BINDING == 'PySide6':
- _logger.debug('Using PySide6 bindings')
- import PySide6 as QtBinding # noqa
+elif BINDING == "PyQt6":
+ _logger.debug("Using PyQt6 bindings")
- from PySide6.QtCore import * # noqa
- from PySide6.QtGui import * # noqa
- from PySide6.QtWidgets import * # noqa
- from PySide6.QtPrintSupport import * # noqa
+ # Monkey-patch module to expose enum values for compatibility
+ # All Qt modules loaded here should be patched.
+ from . import _pyqt6
+ from PyQt6 import QtCore
+
+ if QtCore.PYQT_VERSION < int("0x60300", 16):
+ raise RuntimeError(
+ "PyQt6 v%s is not supported, please upgrade it." % QtCore.PYQT_VERSION_STR
+ )
+
+ from PyQt6 import QtGui, QtWidgets, QtPrintSupport, QtOpenGL, QtSvg
+ from PyQt6 import QtTest as _QtTest
+
+ _pyqt6.patch_enums(
+ QtCore, QtGui, QtWidgets, QtPrintSupport, QtOpenGL, QtSvg, _QtTest
+ )
+
+ import PyQt6 as QtBinding # noqa
+
+ from PyQt6.QtCore import * # noqa
+ from PyQt6.QtGui import * # noqa
+ from PyQt6.QtWidgets import * # noqa
+ from PyQt6.QtPrintSupport import * # noqa
try:
- from PySide6.QtOpenGL import * # noqa
- from PySide6.QtOpenGLWidgets import QOpenGLWidget # noqa
+ from PyQt6.QtOpenGL import * # noqa
+ from PyQt6.QtOpenGLWidgets import QOpenGLWidget # noqa
except ImportError:
- _logger.info("PySide6.QtOpenGL not available")
+ _logger.info("PyQt6's QtOpenGL or QtOpenGLWidgets not available")
HAS_OPENGL = False
else:
HAS_OPENGL = True
try:
- from PySide6.QtSvg import * # noqa
+ from PyQt6.QtSvg import * # noqa
except ImportError:
- _logger.info("PySide6.QtSvg not available")
+ _logger.info("PyQt6.QtSvg not available")
HAS_SVG = False
else:
HAS_SVG = True
- pyqtSignal = Signal
+ from PyQt6.uic import loadUi # noqa
+
+ Signal = pyqtSignal
+
+ Property = pyqtProperty
+
+ Slot = pyqtSlot
+
+ # Disable PyQt6 cooperative multi-inheritance since other bindings do not provide it.
+ # See https://www.riverbankcomputing.com/static/Docs/PyQt6/multiinheritance.html?highlight=inheritance
+ class _Foo(object):
+ pass
+
+ class QObject(QObject, _Foo):
+ pass
else:
- raise ImportError('No Qt wrapper found. Install PyQt5, PySide2 or PySide6')
+ raise ImportError("No Qt wrapper found. Install PyQt5, PySide6 or PyQt6")
# provide a exception handler but not implement it by default
@@ -222,11 +270,11 @@ def exceptionHandler(type_, value, trace):
sys.excepthook = qt.exceptionHandler
"""
- _logger.error("%s %s %s", type_, value, ''.join(traceback.format_tb(trace)))
+ _logger.error("%s %s %s", type_, value, "".join(traceback.format_tb(trace)))
msg = QMessageBox()
msg.setWindowTitle("Unhandled exception")
msg.setIcon(QMessageBox.Critical)
msg.setInformativeText("%s %s\nPlease report details" % (type_, value))
- msg.setDetailedText(("%s " % value) + ''.join(traceback.format_tb(trace)))
+ msg.setDetailedText(("%s " % value) + "".join(traceback.format_tb(trace)))
msg.raise_()
msg.exec()
diff --git a/src/silx/gui/qt/_utils.py b/src/silx/gui/qt/_utils.py
index 5dced95..1015c29 100644
--- a/src/silx/gui/qt/_utils.py
+++ b/src/silx/gui/qt/_utils.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,16 +32,24 @@ __date__ = "30/11/2016"
from . import _qt
+def getMouseEventPosition(event):
+ """Qt5/Qt6 compatibility wrapper to access QMouseEvent position
+
+ :param QMouseEvent event:
+ :returns: (x, y) as a tuple of float
+ """
+ if _qt.BINDING == "PyQt5":
+ return float(event.x()), float(event.y())
+ # Qt6
+ position = event.position()
+ return position.x(), position.y()
+
+
def supportedImageFormats():
"""Return a set of string of file format extensions supported by the
Qt runtime."""
- if _qt.BINDING == 'PySide2':
- def convert(data):
- return str(data.data(), 'ascii')
- else:
- convert = lambda data: str(data, 'ascii')
formats = _qt.QImageReader.supportedImageFormats()
- return set([convert(data) for data in formats])
+ return set([str(data, "ascii") for data in formats])
__globalThreadPoolInstance = None
@@ -50,7 +57,7 @@ __globalThreadPoolInstance = None
def silxGlobalThreadPool():
- """"Manage an own QThreadPool to avoid issue on Qt5 Windows with the
+ """Manage an own QThreadPool to avoid issue on Qt5 Windows with the
default Qt global thread pool.
A thread pool is create in lazy loading. With a maximum of 4 threads.
@@ -59,7 +66,7 @@ def silxGlobalThreadPool():
:rtype: qt.QThreadPool
"""
global __globalThreadPoolInstance
- if __globalThreadPoolInstance is None:
+ if __globalThreadPoolInstance is None:
tp = _qt.QThreadPool()
# Setting maxThreadCount fixes a segfault with PyQt 5.9.1 on Windows
maxThreadCount = min(4, tp.maxThreadCount())
diff --git a/src/silx/gui/qt/inspect.py b/src/silx/gui/qt/inspect.py
index b9a0d1d..990b5fa 100644
--- a/src/silx/gui/qt/inspect.py
+++ b/src/silx/gui/qt/inspect.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2021 European Synchrotron Radiation Facility
@@ -37,7 +36,7 @@ __date__ = "08/10/2018"
from . import _qt as qt
-if qt.BINDING == 'PyQt5':
+if qt.BINDING == "PyQt5":
try:
from PyQt5.sip import isdeleted as _isdeleted # noqa
from PyQt5.sip import ispycreated as createdByPython # noqa
@@ -47,7 +46,6 @@ if qt.BINDING == 'PyQt5':
from sip import ispycreated as createdByPython # noqa
from sip import ispyowned as ownedByPython # noqa
-
def isValid(obj):
"""Returns True if underlying C++ object is valid.
@@ -56,20 +54,23 @@ if qt.BINDING == 'PyQt5':
"""
return not _isdeleted(obj)
-elif qt.BINDING == 'PySide2':
- try:
- from PySide2.shiboken2 import isValid # noqa
- from PySide2.shiboken2 import createdByPython # noqa
- from PySide2.shiboken2 import ownedByPython # noqa
- except ImportError:
- from shiboken2 import isValid # noqa
- from shiboken2 import createdByPython # noqa
- from shiboken2 import ownedByPython # noqa
-
-elif qt.BINDING == 'PySide6':
+elif qt.BINDING == "PySide6":
from shiboken6 import isValid, createdByPython, ownedByPython # noqa
+elif qt.BINDING == "PyQt6":
+ from PyQt6.sip import isdeleted as _isdeleted # noqa
+ from PyQt6.sip import ispycreated as createdByPython # noqa
+ from PyQt6.sip import ispyowned as ownedByPython # noqa
+
+ def isValid(obj):
+ """Returns True if underlying C++ object is valid.
+
+ :param QObject obj:
+ :rtype: bool
+ """
+ return not _isdeleted(obj)
+
else:
raise ImportError("Unsupported Qt binding %s" % qt.BINDING)
-__all__ = ['isValid', 'createdByPython', 'ownedByPython']
+__all__ = ["isValid", "createdByPython", "ownedByPython"]
diff --git a/src/silx/gui/setup.py b/src/silx/gui/setup.py
deleted file mode 100644
index 04a2bac..0000000
--- a/src/silx/gui/setup.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/11/2017"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('gui', parent_package, top_path)
- config.add_subpackage('_glutils')
- config.add_subpackage('qt')
- config.add_subpackage('plot')
- config.add_subpackage('fit')
- config.add_subpackage('hdf5')
- config.add_subpackage('widgets')
- config.add_subpackage('test')
- config.add_subpackage('plot3d')
- config.add_subpackage('data')
- config.add_subpackage('dialog')
- config.add_subpackage('utils')
- config.add_subpackage('utils.glutils')
- config.add_subpackage('utils.test')
-
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
-
- setup(configuration=configuration)
diff --git a/src/silx/gui/test/__init__.py b/src/silx/gui/test/__init__.py
index 00d6216..d9e06fc 100644
--- a/src/silx/gui/test/__init__.py
+++ b/src/silx/gui/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/test/test_colors.py b/src/silx/gui/test/test_colors.py
index fa87d7d..8c252a7 100755
--- a/src/silx/gui/test/test_colors.py
+++ b/src/silx/gui/test/test_colors.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 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,14 +24,15 @@
"""This module provides the Colormap object
"""
-from __future__ import absolute_import
-
__authors__ = ["H.Payno"]
__license__ = "MIT"
__date__ = "09/11/2018"
import unittest
import numpy
+import pytest
+
+import silx
from silx.utils.testutils import ParametricTestCase
from silx.gui import qt
from silx.gui import colors
@@ -41,38 +41,39 @@ from silx.gui.plot import items
from silx.utils.exceptions import NotEditableError
-class TestColor(ParametricTestCase):
- """Basic tests of rgba function"""
-
- TEST_COLORS = { # name: (colors, expected values)
- 'blue': ('blue', (0., 0., 1., 1.)),
- '#010203': ('#010203', (1. / 255., 2. / 255., 3. / 255., 1.)),
- '#01020304': ('#01020304', (1. / 255., 2. / 255., 3. / 255., 4. / 255.)),
- '3 x uint8': (numpy.array((1, 255, 0), dtype=numpy.uint8),
- (1 / 255., 1., 0., 1.)),
- '4 x uint8': (numpy.array((1, 255, 0, 1), dtype=numpy.uint8),
- (1 / 255., 1., 0., 1 / 255.)),
- '3 x float overflow': ((3., 0.5, 1.), (1., 0.5, 1., 1.)),
- }
-
- def testRGBA(self):
- """"Test rgba function with accepted values"""
- for name, test in self.TEST_COLORS.items():
- color, expected = test
- with self.subTest(msg=name):
- result = colors.rgba(color)
- self.assertEqual(result, expected)
-
- def testQColor(self):
- """"Test getQColor function with accepted values"""
- for name, test in self.TEST_COLORS.items():
- color, expected = test
- with self.subTest(msg=name):
- result = colors.asQColor(color)
- self.assertAlmostEqual(result.redF(), expected[0], places=4)
- self.assertAlmostEqual(result.greenF(), expected[1], places=4)
- self.assertAlmostEqual(result.blueF(), expected[2], places=4)
- self.assertAlmostEqual(result.alphaF(), expected[3], places=4)
+RGBA_TEST_CASES = (
+ # name
+ ("blue", (0.0, 0.0, 1.0, 1.0)),
+ # code
+ ("#010203", (1.0 / 255.0, 2.0 / 255.0, 3.0 / 255.0, 1.0)),
+ ("#01020304", (1.0 / 255.0, 2.0 / 255.0, 3.0 / 255.0, 4.0 / 255.0)),
+ # index name
+ ("C0", colors.rgba(silx.config.DEFAULT_PLOT_CURVE_COLORS[0])),
+ ("C2", colors.rgba(silx.config.DEFAULT_PLOT_CURVE_COLORS[2])),
+ # 3 uint
+ (numpy.array((1, 255, 0), dtype=numpy.uint8), (1 / 255.0, 1.0, 0.0, 1.0)),
+ # 4 uint
+ (numpy.array((1, 255, 0, 1), dtype=numpy.uint8), (1 / 255.0, 1.0, 0.0, 1 / 255.0)),
+ # float with overflow
+ ((3.0, 0.5, 1.0), (1.0, 0.5, 1.0, 1.0)),
+)
+
+
+@pytest.mark.parametrize("input, expected", RGBA_TEST_CASES)
+def testRgba(input, expected):
+ """Test rgba function with accepted values"""
+ result = colors.rgba(input)
+ assert result == expected
+
+
+@pytest.mark.parametrize("input, expected", RGBA_TEST_CASES)
+def testAsQColor(input, expected):
+ """Test asQColor function with accepted values"""
+ result = colors.asQColor(input)
+ assert result.redF() == pytest.approx(expected[0], abs=1e-5)
+ assert result.greenF() == pytest.approx(expected[1], abs=1e-5)
+ assert result.blueF() == pytest.approx(expected[2], abs=1e-5)
+ assert result.alphaF() == pytest.approx(expected[3], abs=1e-5)
class TestApplyColormapToData(ParametricTestCase):
@@ -80,24 +81,23 @@ class TestApplyColormapToData(ParametricTestCase):
def testApplyColormapToData(self):
"""Simple test of applyColormapToData function"""
- colormap = Colormap(name='gray', normalization='linear',
- vmin=0, vmax=255)
+ colormap = Colormap(name="gray", normalization="linear", vmin=0, vmax=255)
size = 10
- expected = numpy.empty((size, 4), dtype='uint8')
- expected[:, 0] = numpy.arange(size, dtype='uint8')
+ expected = numpy.empty((size, 4), dtype="uint8")
+ expected[:, 0] = numpy.arange(size, dtype="uint8")
expected[:, 1] = expected[:, 0]
expected[:, 2] = expected[:, 0]
expected[:, 3] = 255
- for dtype in ('uint8', 'int32', 'float32', 'float64'):
+ for dtype in ("uint8", "int32", "float32", "float64"):
with self.subTest(dtype=dtype):
array = numpy.arange(size, dtype=dtype)
result = colormap.applyToData(data=array)
self.assertTrue(numpy.all(numpy.equal(result, expected)))
def testAutoscaleFromDataReference(self):
- colormap = Colormap(name='gray', normalization='linear')
+ colormap = Colormap(name="gray", normalization="linear")
data = numpy.array([50])
reference = numpy.array([0, 100])
value = colormap.applyToData(data, reference)
@@ -105,7 +105,7 @@ class TestApplyColormapToData(ParametricTestCase):
self.assertEqual(value[0, 0], 128)
def testAutoscaleFromItemReference(self):
- colormap = Colormap(name='gray', normalization='linear')
+ colormap = Colormap(name="gray", normalization="linear")
data = numpy.array([50])
image = items.ImageData()
image.setData(numpy.array([[0, 100]]))
@@ -115,11 +115,11 @@ class TestApplyColormapToData(ParametricTestCase):
def testNaNColor(self):
"""Test Colormap.applyToData with NaN values"""
- colormap = Colormap(name='gray', normalization='linear')
- colormap.setNaNColor('red')
+ colormap = Colormap(name="gray", normalization="linear")
+ colormap.setNaNColor("red")
self.assertEqual(colormap.getNaNColor(), qt.QColor(255, 0, 0))
- data = numpy.array([50., numpy.nan])
+ data = numpy.array([50.0, numpy.nan])
image = items.ImageData()
image.setData(numpy.array([[0, 100]]))
value = colormap.applyToData(data, reference=image)
@@ -129,8 +129,7 @@ class TestApplyColormapToData(ParametricTestCase):
class TestDictAPI(unittest.TestCase):
- """Make sure the old dictionary API is working
- """
+ """Make sure the old dictionary API is working"""
def setUp(self):
self.vmin = -1.0
@@ -138,75 +137,79 @@ class TestDictAPI(unittest.TestCase):
def testGetItem(self):
"""test the item getter API ([xxx])"""
- colormap = Colormap(name='viridis',
- normalization=Colormap.LINEAR,
- vmin=self.vmin,
- vmax=self.vmax)
- self.assertTrue(colormap['name'] == 'viridis')
- self.assertTrue(colormap['normalization'] == Colormap.LINEAR)
- self.assertTrue(colormap['vmin'] == self.vmin)
- self.assertTrue(colormap['vmax'] == self.vmax)
+ colormap = Colormap(
+ name="viridis",
+ normalization=Colormap.LINEAR,
+ vmin=self.vmin,
+ vmax=self.vmax,
+ )
+ self.assertTrue(colormap["name"] == "viridis")
+ self.assertTrue(colormap["normalization"] == Colormap.LINEAR)
+ self.assertTrue(colormap["vmin"] == self.vmin)
+ self.assertTrue(colormap["vmax"] == self.vmax)
with self.assertRaises(KeyError):
- colormap['toto']
+ colormap["toto"]
def testGetDict(self):
"""Test the getDict function API"""
- clmObject = Colormap(name='viridis',
- normalization=Colormap.LINEAR,
- vmin=self.vmin,
- vmax=self.vmax)
+ clmObject = Colormap(
+ name="viridis",
+ normalization=Colormap.LINEAR,
+ vmin=self.vmin,
+ vmax=self.vmax,
+ )
clmDict = clmObject._toDict()
- self.assertTrue(clmDict['name'] == 'viridis')
- self.assertTrue(clmDict['autoscale'] is False)
- self.assertTrue(clmDict['vmin'] == self.vmin)
- self.assertTrue(clmDict['vmax'] == self.vmax)
- self.assertTrue(clmDict['normalization'] == Colormap.LINEAR)
+ self.assertTrue(clmDict["name"] == "viridis")
+ self.assertTrue(clmDict["autoscale"] is False)
+ self.assertTrue(clmDict["vmin"] == self.vmin)
+ self.assertTrue(clmDict["vmax"] == self.vmax)
+ self.assertTrue(clmDict["normalization"] == Colormap.LINEAR)
clmObject.setVRange(None, None)
- self.assertTrue(clmObject._toDict()['autoscale'] is True)
+ self.assertTrue(clmObject._toDict()["autoscale"] is True)
def testSetValidDict(self):
"""Test that if a colormap is created from a dict then it is correctly
created and the values are copied (so if some values from the dict
is changing, this won't affect the Colormap object"""
clm_dict = {
- 'name': 'temperature',
- 'vmin': 1.0,
- 'vmax': 2.0,
- 'normalization': 'linear',
- 'colors': None,
- 'autoscale': False
+ "name": "temperature",
+ "vmin": 1.0,
+ "vmax": 2.0,
+ "normalization": "linear",
+ "colors": None,
+ "autoscale": False,
}
# Test that the colormap is correctly created
colormapObject = Colormap._fromDict(clm_dict)
- self.assertTrue(colormapObject.getName() == clm_dict['name'])
- self.assertTrue(colormapObject.getColormapLUT() == clm_dict['colors'])
- self.assertTrue(colormapObject.getVMin() == clm_dict['vmin'])
- self.assertTrue(colormapObject.getVMax() == clm_dict['vmax'])
- self.assertTrue(colormapObject.isAutoscale() == clm_dict['autoscale'])
+ self.assertTrue(colormapObject.getName() == clm_dict["name"])
+ self.assertTrue(colormapObject.getColormapLUT() == clm_dict["colors"])
+ self.assertTrue(colormapObject.getVMin() == clm_dict["vmin"])
+ self.assertTrue(colormapObject.getVMax() == clm_dict["vmax"])
+ self.assertTrue(colormapObject.isAutoscale() == clm_dict["autoscale"])
# Check that the colormap has copied the values
- clm_dict['vmin'] = None
- clm_dict['vmax'] = None
- clm_dict['colors'] = [1.0, 2.0]
- clm_dict['autoscale'] = True
- clm_dict['normalization'] = Colormap.LOGARITHM
- clm_dict['name'] = 'viridis'
-
- self.assertFalse(colormapObject.getName() == clm_dict['name'])
- self.assertFalse(colormapObject.getColormapLUT() == clm_dict['colors'])
- self.assertFalse(colormapObject.getVMin() == clm_dict['vmin'])
- self.assertFalse(colormapObject.getVMax() == clm_dict['vmax'])
- self.assertFalse(colormapObject.isAutoscale() == clm_dict['autoscale'])
+ clm_dict["vmin"] = None
+ clm_dict["vmax"] = None
+ clm_dict["colors"] = [1.0, 2.0]
+ clm_dict["autoscale"] = True
+ clm_dict["normalization"] = Colormap.LOGARITHM
+ clm_dict["name"] = "viridis"
+
+ self.assertFalse(colormapObject.getName() == clm_dict["name"])
+ self.assertFalse(colormapObject.getColormapLUT() == clm_dict["colors"])
+ self.assertFalse(colormapObject.getVMin() == clm_dict["vmin"])
+ self.assertFalse(colormapObject.getVMax() == clm_dict["vmax"])
+ self.assertFalse(colormapObject.isAutoscale() == clm_dict["autoscale"])
def testMissingKeysFromDict(self):
"""Make sure we can create a Colormap object from a dictionary even if
there is missing keys except if those keys are 'colors' or 'name'
"""
- colormap = Colormap._fromDict({'name': 'blue'})
+ colormap = Colormap._fromDict({"name": "blue"})
self.assertTrue(colormap.getVMin() is None)
- colormap = Colormap._fromDict({'colors': numpy.zeros((5, 3))})
+ colormap = Colormap._fromDict({"colors": numpy.zeros((5, 3))})
self.assertTrue(colormap.getName() is None)
with self.assertRaises(ValueError):
@@ -217,12 +220,12 @@ class TestDictAPI(unittest.TestCase):
knowed
"""
clm_dict = {
- 'name': 'temperature',
- 'vmin': 1.0,
- 'vmax': 2.0,
- 'normalization': 'toto',
- 'colors': None,
- 'autoscale': False
+ "name": "temperature",
+ "vmin": 1.0,
+ "vmax": 2.0,
+ "normalization": "toto",
+ "colors": None,
+ "autoscale": False,
}
with self.assertRaises(ValueError):
Colormap._fromDict(clm_dict)
@@ -230,26 +233,26 @@ class TestDictAPI(unittest.TestCase):
def testNumericalColors(self):
"""Make sure the old API using colors=int was supported"""
clm_dict = {
- 'name': 'temperature',
- 'vmin': 1.0,
- 'vmax': 2.0,
- 'colors': 256,
- 'autoscale': False
+ "name": "temperature",
+ "vmin": 1.0,
+ "vmax": 2.0,
+ "colors": 256,
+ "autoscale": False,
}
Colormap._fromDict(clm_dict)
class TestObjectAPI(ParametricTestCase):
"""Test the new Object API of the colormap"""
+
def testVMinVMax(self):
"""Test getter and setter associated to vmin and vmax values"""
vmin = 1.0
vmax = 2.0
- colormapObject = Colormap(name='viridis',
- vmin=vmin,
- vmax=vmax,
- normalization=Colormap.LINEAR)
+ colormapObject = Colormap(
+ name="viridis", vmin=vmin, vmax=vmax, normalization=Colormap.LINEAR
+ )
with self.assertRaises(ValueError):
colormapObject.setVMin(3)
@@ -268,15 +271,14 @@ class TestObjectAPI(ParametricTestCase):
self.assertTrue(colormapObject.isAutoscale() is True)
def testCopy(self):
- """Make sure the copy function is correctly processing
- """
- colormapObject = Colormap(name=None,
- colors=numpy.array([[1., 0., 0.],
- [0., 1., 0.],
- [0., 0., 1.]]),
- vmin=None,
- vmax=None,
- normalization=Colormap.LOGARITHM)
+ """Make sure the copy function is correctly processing"""
+ colormapObject = Colormap(
+ name=None,
+ colors=numpy.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]),
+ vmin=None,
+ vmax=None,
+ normalization=Colormap.LOGARITHM,
+ )
colormapObject2 = colormapObject.copy()
self.assertTrue(colormapObject == colormapObject2)
@@ -293,23 +295,11 @@ class TestObjectAPI(ParametricTestCase):
applying
"""
# test linear scale
- data = numpy.array([-1, 1, 2, 3, float('nan')])
- cl1 = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=0,
- vmax=2)
- cl2 = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=2)
- cl3 = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=0,
- vmax=None)
- cl4 = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None)
+ data = numpy.array([-1, 1, 2, 3, float("nan")])
+ cl1 = Colormap(name="gray", normalization=Colormap.LINEAR, vmin=0, vmax=2)
+ cl2 = Colormap(name="gray", normalization=Colormap.LINEAR, vmin=None, vmax=2)
+ cl3 = Colormap(name="gray", normalization=Colormap.LINEAR, vmin=0, vmax=None)
+ cl4 = Colormap(name="gray", normalization=Colormap.LINEAR, vmin=None, vmax=None)
self.assertTrue(cl1.getColormapRange(data) == (0, 2))
self.assertTrue(cl2.getColormapRange(data) == (-1, 2))
@@ -318,30 +308,23 @@ class TestObjectAPI(ParametricTestCase):
# test linear with annoying cases
self.assertEqual(cl3.getColormapRange((-1, -2)), (0, 0))
- self.assertEqual(cl4.getColormapRange(()), (0., 1.))
- self.assertEqual(cl4.getColormapRange(
- (float('nan'), float('inf'), 1., -float('inf'), 2)), (1., 2.))
- self.assertEqual(cl4.getColormapRange(
- (float('nan'), float('inf'))), (0., 1.))
+ self.assertEqual(cl4.getColormapRange(()), (0.0, 1.0))
+ self.assertEqual(
+ cl4.getColormapRange((float("nan"), float("inf"), 1.0, -float("inf"), 2)),
+ (1.0, 2.0),
+ )
+ self.assertEqual(cl4.getColormapRange((float("nan"), float("inf"))), (0.0, 1.0))
# test log scale
- data = numpy.array([float('nan'), -1, 1, 10, 100, 1000])
- cl1 = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=1,
- vmax=100)
- cl2 = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=100)
- cl3 = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=1,
- vmax=None)
- cl4 = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
+ data = numpy.array([float("nan"), -1, 1, 10, 100, 1000])
+ cl1 = Colormap(name="gray", normalization=Colormap.LOGARITHM, vmin=1, vmax=100)
+ cl2 = Colormap(
+ name="gray", normalization=Colormap.LOGARITHM, vmin=None, vmax=100
+ )
+ cl3 = Colormap(name="gray", normalization=Colormap.LOGARITHM, vmin=1, vmax=None)
+ cl4 = Colormap(
+ name="gray", normalization=Colormap.LOGARITHM, vmin=None, vmax=None
+ )
self.assertTrue(cl1.getColormapRange(data) == (1, 100))
self.assertTrue(cl2.getColormapRange(data) == (1, 100))
@@ -350,12 +333,15 @@ class TestObjectAPI(ParametricTestCase):
# test log with annoying cases
self.assertEqual(cl3.getColormapRange((0.1, 0.2)), (1, 1))
- self.assertEqual(cl4.getColormapRange((-2., -1.)), (1., 1.))
- self.assertEqual(cl4.getColormapRange(()), (1., 10.))
- self.assertEqual(cl4.getColormapRange(
- (float('nan'), float('inf'), 1., -float('inf'), 2)), (1., 2.))
- self.assertEqual(cl4.getColormapRange(
- (float('nan'), float('inf'))), (1., 10.))
+ self.assertEqual(cl4.getColormapRange((-2.0, -1.0)), (1.0, 1.0))
+ self.assertEqual(cl4.getColormapRange(()), (1.0, 10.0))
+ self.assertEqual(
+ cl4.getColormapRange((float("nan"), float("inf"), 1.0, -float("inf"), 2)),
+ (1.0, 2.0),
+ )
+ self.assertEqual(
+ cl4.getColormapRange((float("nan"), float("inf"))), (1.0, 10.0)
+ )
def testApplyToData(self):
"""Test applyToData on different datasets"""
@@ -365,11 +351,10 @@ class TestObjectAPI(ParametricTestCase):
numpy.array((-numpy.inf, numpy.inf, 1.0, 2.0)), # Some infinite
]
- for normalization in ('linear', 'log'):
- colormap = Colormap(name='gray',
- normalization=normalization,
- vmin=None,
- vmax=None)
+ for normalization in ("linear", "log"):
+ colormap = Colormap(
+ name="gray", normalization=normalization, vmin=None, vmax=None
+ )
for data in datasets:
with self.subTest(data=data):
@@ -381,14 +366,13 @@ class TestObjectAPI(ParametricTestCase):
def testGetNColors(self):
"""Test getNColors method"""
# specific LUT
- colormap = Colormap(name=None,
- colors=((0., 0., 0.), (1., 1., 1.)),
- vmin=1000,
- vmax=2000)
+ colormap = Colormap(
+ name=None, colors=((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), vmin=1000, vmax=2000
+ )
colors = colormap.getNColors()
- self.assertTrue(numpy.all(numpy.equal(
- colors,
- ((0, 0, 0, 255), (255, 255, 255, 255)))))
+ self.assertTrue(
+ numpy.all(numpy.equal(colors, ((0, 0, 0, 255), (255, 255, 255, 255))))
+ )
def testEditableMode(self):
"""Make sure the colormap will raise NotEditableError when try to
@@ -396,17 +380,17 @@ class TestObjectAPI(ParametricTestCase):
colormap = Colormap()
colormap.setEditable(False)
with self.assertRaises(NotEditableError):
- colormap.setVRange(0., 1.)
+ colormap.setVRange(0.0, 1.0)
with self.assertRaises(NotEditableError):
- colormap.setVMin(1.)
+ colormap.setVMin(1.0)
with self.assertRaises(NotEditableError):
- colormap.setVMax(1.)
+ colormap.setVMax(1.0)
with self.assertRaises(NotEditableError):
colormap.setNormalization(Colormap.LOGARITHM)
with self.assertRaises(NotEditableError):
- colormap.setName('magma')
+ colormap.setName("magma")
with self.assertRaises(NotEditableError):
- colormap.setColormapLUT([[0., 0., 0.], [1., 1., 1.]])
+ colormap.setColormapLUT([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])
with self.assertRaises(NotEditableError):
colormap._setFromDict(colormap._toDict())
state = colormap.saveState()
@@ -433,7 +417,9 @@ class TestObjectAPI(ParametricTestCase):
def testSet(self):
colormap = Colormap()
- other = Colormap(name="viridis", vmin=1, vmax=2, normalization=Colormap.LOGARITHM)
+ other = Colormap(
+ name="viridis", vmin=1, vmax=2, normalization=Colormap.LOGARITHM
+ )
self.assertNotEqual(colormap, other)
colormap.setFromColormap(other)
self.assertIsNot(colormap, other)
@@ -446,13 +432,10 @@ class TestObjectAPI(ParametricTestCase):
self.assertEqual(colormap.getAutoscaleMode(), Colormap.MINMAX)
def testStoreRestore(self):
- colormaps = [
- Colormap(name="viridis"),
- Colormap(normalization=Colormap.SQRT)
- ]
+ colormaps = [Colormap(name="viridis"), Colormap(normalization=Colormap.SQRT)]
cmap = Colormap(normalization=Colormap.GAMMA)
cmap.setGammaNormalizationParameter(1.2)
- cmap.setNaNColor('red')
+ cmap.setNaNColor("red")
colormaps.append(cmap)
for expected in colormaps:
with self.subTest(colormap=expected):
@@ -462,29 +445,37 @@ class TestObjectAPI(ParametricTestCase):
self.assertEqual(expected, result)
def testStorageV1(self):
- state = b'\x00\x00\x00\x10\x00C\x00o\x00l\x00o\x00r\x00m\x00a\x00p\x00\x00'\
- b'\x00\x01\x00\x00\x00\x0E\x00v\x00i\x00r\x00i\x00d\x00i\x00s\x00'\
- b'\x00\x00\x00\x06\x00?\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
- b'\x00\x06\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00'\
- b'l\x00o\x00g'
+ state = (
+ b"\x00\x00\x00\x10\x00C\x00o\x00l\x00o\x00r\x00m\x00a\x00p\x00\x00"
+ b"\x00\x01\x00\x00\x00\x0E\x00v\x00i\x00r\x00i\x00d\x00i\x00s\x00"
+ b"\x00\x00\x00\x06\x00?\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x06\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00"
+ b"l\x00o\x00g"
+ )
state = qt.QByteArray(state)
colormap = Colormap()
colormap.restoreState(state)
- expected = Colormap(name="viridis", vmin=1, vmax=2, normalization=Colormap.LOGARITHM)
+ expected = Colormap(
+ name="viridis", vmin=1, vmax=2, normalization=Colormap.LOGARITHM
+ )
self.assertEqual(colormap, expected)
def testStorageV2(self):
- state = b'\x00\x00\x00\x10\x00C\x00o\x00l\x00o\x00r\x00m\x00a\x00p\x00'\
- b'\x00\x00\x02\x00\x00\x00\x0e\x00v\x00i\x00r\x00i\x00d\x00i\x00'\
- b's\x00\x00\x00\x00\x06\x00?\xf0\x00\x00\x00\x00\x00\x00\x00\x00'\
- b'\x00\x00\x06\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06'\
- b'\x00l\x00o\x00g\x00\x00\x00\x0c\x00m\x00i\x00n\x00m\x00a\x00x'
+ state = (
+ b"\x00\x00\x00\x10\x00C\x00o\x00l\x00o\x00r\x00m\x00a\x00p\x00"
+ b"\x00\x00\x02\x00\x00\x00\x0e\x00v\x00i\x00r\x00i\x00d\x00i\x00"
+ b"s\x00\x00\x00\x00\x06\x00?\xf0\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x06\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06"
+ b"\x00l\x00o\x00g\x00\x00\x00\x0c\x00m\x00i\x00n\x00m\x00a\x00x"
+ )
state = qt.QByteArray(state)
colormap = Colormap()
colormap.restoreState(state)
- expected = Colormap(name="viridis", vmin=1, vmax=2, normalization=Colormap.LOGARITHM)
+ expected = Colormap(
+ name="viridis", vmin=1, vmax=2, normalization=Colormap.LOGARITHM
+ )
expected.setGammaNormalizationParameter(1.5)
self.assertEqual(colormap, expected)
@@ -501,7 +492,7 @@ class TestPreferredColormaps(unittest.TestCase):
colors.setPreferredColormaps(self._colormaps)
def test(self):
- colormaps = 'viridis', 'magma'
+ colormaps = "viridis", "magma"
colors.setPreferredColormaps(colormaps)
self.assertEqual(colors.preferredColormaps(), colormaps)
@@ -510,10 +501,10 @@ class TestPreferredColormaps(unittest.TestCase):
colors.setPreferredColormaps(())
with self.assertRaises(ValueError):
- colors.setPreferredColormaps(('This is not a colormap',))
+ colors.setPreferredColormaps(("This is not a colormap",))
- colormaps = 'red', 'green'
- colors.setPreferredColormaps(('This is not a colormap',) + colormaps)
+ colormaps = "red", "green"
+ colors.setPreferredColormaps(("This is not a colormap",) + colormaps)
self.assertEqual(colors.preferredColormaps(), colormaps)
@@ -525,7 +516,7 @@ class TestRegisteredLut(unittest.TestCase):
lut = numpy.arange(8 * 3)
lut.shape = -1, 3
lut = lut / (8.0 * 3)
- colors.registerLUT("test_8", colors=lut, cursor_color='blue')
+ colors.registerLUT("test_8", colors=lut, cursor_color="blue")
def testColormap(self):
colormap = Colormap("test_8")
@@ -533,7 +524,7 @@ class TestRegisteredLut(unittest.TestCase):
def testCursor(self):
color = colors.cursorColorForColormap("test_8")
- self.assertEqual(color, 'blue')
+ self.assertEqual(color, "blue")
def testLut(self):
colormap = Colormap("test_8")
@@ -557,7 +548,10 @@ class TestRegisteredLut(unittest.TestCase):
self.assertEqual(lut[0, 0], 255)
def testFloatRGBA(self):
- lut = numpy.array([[1.0, 0, 0, 128 / 256.0], [0.5, 0, 0, 1.0], [0.0, 0, 0, 1.0]], dtype="float")
+ lut = numpy.array(
+ [[1.0, 0, 0, 128 / 256.0], [0.5, 0, 0, 1.0], [0.0, 0, 0, 1.0]],
+ dtype="float",
+ )
colors.registerLUT("test_type", lut)
colormap = colors.Colormap(name="test_type")
lut = colormap.getNColors(3)
@@ -567,28 +561,75 @@ class TestRegisteredLut(unittest.TestCase):
class TestAutoscaleRange(ParametricTestCase):
-
def testAutoscaleRange(self):
nan = numpy.nan
- data_std_inside = numpy.array([0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2])
- data_std_inside_nan = numpy.array([0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, numpy.nan])
+ data_std_inside = numpy.array(
+ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]
+ )
+ data_std_inside_nan = numpy.array(
+ [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, numpy.nan]
+ )
data = [
# Positive values
(Colormap.LINEAR, Colormap.MINMAX, numpy.array([10, 20, 50]), (10, 50)),
- (Colormap.LOGARITHM, Colormap.MINMAX, numpy.array([10, 50, 100]), (10, 100)),
- (Colormap.LINEAR, Colormap.STDDEV3, data_std_inside, (0.026671473215424735, 1.9733285267845753)),
- (Colormap.LOGARITHM, Colormap.STDDEV3, data_std_inside, (1, 1.6733506885453602)),
+ (
+ Colormap.LOGARITHM,
+ Colormap.MINMAX,
+ numpy.array([10, 50, 100]),
+ (10, 100),
+ ),
+ (
+ Colormap.LINEAR,
+ Colormap.STDDEV3,
+ data_std_inside,
+ (0.026671473215424735, 1.9733285267845753),
+ ),
+ (
+ Colormap.LOGARITHM,
+ Colormap.STDDEV3,
+ data_std_inside,
+ (1, 1.6733506885453602),
+ ),
(Colormap.LINEAR, Colormap.STDDEV3, numpy.array([10, 100]), (10, 100)),
(Colormap.LOGARITHM, Colormap.STDDEV3, numpy.array([10, 100]), (10, 100)),
-
# With nan
- (Colormap.LINEAR, Colormap.MINMAX, numpy.array([10, 20, 50, nan]), (10, 50)),
- (Colormap.LOGARITHM, Colormap.MINMAX, numpy.array([10, 50, 100, nan]), (10, 100)),
- (Colormap.LINEAR, Colormap.STDDEV3, data_std_inside_nan, (0.026671473215424735, 1.9733285267845753)),
- (Colormap.LOGARITHM, Colormap.STDDEV3, data_std_inside_nan, (1, 1.6733506885453602)),
+ (
+ Colormap.LINEAR,
+ Colormap.MINMAX,
+ numpy.array([10, 20, 50, nan]),
+ (10, 50),
+ ),
+ (
+ Colormap.LOGARITHM,
+ Colormap.MINMAX,
+ numpy.array([10, 50, 100, nan]),
+ (10, 100),
+ ),
+ (
+ Colormap.LINEAR,
+ Colormap.STDDEV3,
+ data_std_inside_nan,
+ (0.026671473215424735, 1.9733285267845753),
+ ),
+ (
+ Colormap.LOGARITHM,
+ Colormap.STDDEV3,
+ data_std_inside_nan,
+ (1, 1.6733506885453602),
+ ),
# With negative
- (Colormap.LOGARITHM, Colormap.MINMAX, numpy.array([10, 50, 100, -50]), (10, 100)),
- (Colormap.LOGARITHM, Colormap.STDDEV3, numpy.array([10, 100, -10]), (10, 100)),
+ (
+ Colormap.LOGARITHM,
+ Colormap.MINMAX,
+ numpy.array([10, 50, 100, -50]),
+ (10, 100),
+ ),
+ (
+ Colormap.LOGARITHM,
+ Colormap.STDDEV3,
+ numpy.array([10, 100, -10]),
+ (10, 100),
+ ),
]
for norm, mode, array, expectedRange in data:
with self.subTest(norm=norm, mode=mode, array=array):
diff --git a/src/silx/gui/test/test_console.py b/src/silx/gui/test/test_console.py
index 21f3564..4a25fe3 100644
--- a/src/silx/gui/test/test_console.py
+++ b/src/silx/gui/test/test_console.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -24,8 +23,6 @@
# ###########################################################################*/
"""Basic tests for IPython console widget"""
-from __future__ import print_function
-
__authors__ = ["P. Knobel"]
__license__ = "MIT"
__date__ = "05/12/2016"
@@ -54,8 +51,8 @@ def console(qapp_utils):
pytest.skip("IPythonDockWidget is not available")
console = IPythonDockWidget(
- available_vars={"a": _a, "f": _f},
- custom_banner="Welcome!\n")
+ available_vars={"a": _a, "f": _f}, custom_banner="Welcome!\n"
+ )
console.show()
qapp_utils.qWaitForWindowExposed(console)
yield console
@@ -70,6 +67,6 @@ def testShow(console):
def testInteract(console, qapp_utils):
qapp_utils.mouseClick(console, qt.Qt.LeftButton)
- qapp_utils.keyClicks(console, 'import silx')
+ qapp_utils.keyClicks(console, "import silx")
qapp_utils.keyClick(console, qt.Qt.Key_Enter)
qapp_utils.qapp.processEvents()
diff --git a/src/silx/gui/test/test_icons.py b/src/silx/gui/test/test_icons.py
index 154adf6..6797398 100644
--- a/src/silx/gui/test/test_icons.py
+++ b/src/silx/gui/test/test_icons.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -52,8 +51,12 @@ class TestIcons(TestCaseQt):
os.mkdir(os.path.join(cls.tmpDirectory, "gui"))
destination = os.path.join(cls.tmpDirectory, "gui", "icons")
os.mkdir(destination)
- shutil.copy(silx.resources.resource_filename("gui/icons/zoom-in.png"), destination)
- shutil.copy(silx.resources.resource_filename("gui/icons/zoom-out.svg"), destination)
+ shutil.copy(
+ silx.resources.resource_filename("gui/icons/zoom-in.png"), destination
+ )
+ shutil.copy(
+ silx.resources.resource_filename("gui/icons/zoom-out.svg"), destination
+ )
@classmethod
def tearDownClass(cls):
@@ -63,7 +66,9 @@ class TestIcons(TestCaseQt):
def setUp(self):
# Store the original configuration
self._oldResources = dict(silx.resources._RESOURCE_DIRECTORIES)
- silx.resources.register_resource_directory("test", "foo.bar", forced_path=self.tmpDirectory)
+ silx.resources.register_resource_directory(
+ "test", "foo.bar", forced_path=self.tmpDirectory
+ )
unittest.TestCase.setUp(self)
def tearDown(self):
diff --git a/src/silx/gui/test/test_qt.py b/src/silx/gui/test/test_qt.py
index 8554744..17bdc72 100644
--- a/src/silx/gui/test/test_qt.py
+++ b/src/silx/gui/test/test_qt.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -37,6 +36,7 @@ from silx.test.utils import temp_dir
from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
+
try:
from silx.gui.qt import inspect as qt_inspect
except ImportError:
@@ -147,7 +147,7 @@ class TestLoadUi(TestCaseQt):
uifile = os.path.join(tmp, "test.ui")
# write file
- with open(uifile, mode='w') as f:
+ with open(uifile, mode="w") as f:
f.write(self.TEST_UI)
class TestMainWindow(qt.QMainWindow):
@@ -186,12 +186,11 @@ class TestQtInspect(unittest.TestCase):
self.assertFalse(qt_inspect.isValid(obj))
-@pytest.mark.skipif(qt.BINDING not in ("PyQt5", "PySide2"),
- reason="PyQt5/PySide2 only test")
+@pytest.mark.skipif(qt.BINDING != "PyQt5", reason="PyQt5 only test")
def test_exec_():
"""Test the exec_ is still useable with Qt5 bindings"""
klasses = [
- #QtWidgets
+ # QtWidgets
qt.QApplication,
qt.QColorDialog,
qt.QDialog,
@@ -202,11 +201,15 @@ def test_exec_():
qt.QMenu,
qt.QMessageBox,
qt.QProgressDialog,
- #QtCore
+ # QtCore
qt.QCoreApplication,
qt.QEventLoop,
qt.QThread,
]
for klass in klasses:
- assert hasattr(klass, "exec") and callable(klass.exec), "%s.exec missing" % klass.__name__
- assert hasattr(klass, "exec_") and callable(klass.exec_), "%s.exec_ missing" % klass.__name__
+ assert hasattr(klass, "exec") and callable(klass.exec), (
+ "%s.exec missing" % klass.__name__
+ )
+ assert hasattr(klass, "exec_") and callable(klass.exec_), (
+ "%s.exec_ missing" % klass.__name__
+ )
diff --git a/src/silx/gui/test/utils.py b/src/silx/gui/test/utils.py
deleted file mode 100644
index db4c0ee..0000000
--- a/src/silx/gui/test/utils.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""Color conversion function, color dictionary and colormap tools."""
-
-from __future__ import absolute_import
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "05/10/2018"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.test.utils",
- reason="moved",
- replacement="silx.gui.utils.testutils",
- since_version="0.9.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..utils.testutils import * # noqa
diff --git a/src/silx/gui/utils/__init__.py b/src/silx/gui/utils/__init__.py
index 726ad74..248aa16 100755
--- a/src/silx/gui/utils/__init__.py
+++ b/src/silx/gui/utils/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
@@ -48,9 +47,9 @@ def blockSignals(*objs):
obj.blockSignals(previous)
-class LockReentrant():
- """Context manager to lock a code block and check the state.
- """
+class LockReentrant:
+ """Context manager to lock a code block and check the state."""
+
def __init__(self):
self.__locked = False
@@ -73,4 +72,5 @@ def getQEventName(eventType):
:returns: str
"""
from . import qtutils
+
return qtutils.getQEventName(eventType)
diff --git a/src/silx/gui/utils/concurrent.py b/src/silx/gui/utils/concurrent.py
index c27374f..242e804 100644
--- a/src/silx/gui/utils/concurrent.py
+++ b/src/silx/gui/utils/concurrent.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module allows to run a function in Qt main thread from another thread
"""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "09/03/2018"
diff --git a/src/silx/gui/utils/glutils/__init__.py b/src/silx/gui/utils/glutils/__init__.py
index 20e611e..8e34605 100644
--- a/src/silx/gui/utils/glutils/__init__.py
+++ b/src/silx/gui/utils/glutils/__init__.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2020-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2020-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,6 +23,8 @@
# ###########################################################################*/
"""This module provides the :func:`isOpenGLAvailable` utility function.
"""
+from __future__ import annotations
+
import os
import sys
@@ -38,9 +39,9 @@ class _isOpenGLAvailableResult:
an `error` string attribute storting the possible error message.
"""
- def __init__(self, status=True, error=''):
- self.__status = bool(status)
+ def __init__(self, error: str = "", status: bool = False):
self.__error = str(error)
+ self.__status = bool(status)
status = property(lambda self: self.__status, doc="True if OpenGL is working")
error = property(lambda self: self.__error, doc="Error message")
@@ -49,95 +50,119 @@ class _isOpenGLAvailableResult:
return self.status
def __repr__(self):
- return '<_isOpenGLAvailableResult: %s, "%s">' % (self.status, self.error)
+ return f'<_isOpenGLAvailableResult: {self.status}, "{self.error}">'
-def _runtimeOpenGLCheck(version):
+def _runtimeOpenGLCheck(
+ version: tuple[int, int],
+ shareOpenGLContexts: bool,
+) -> _isOpenGLAvailableResult:
"""Run OpenGL check in a subprocess.
This is done by starting a subprocess that displays a Qt OpenGL widget.
- :param List[int] version:
+ :param version:
The minimal required OpenGL version as a 2-tuple (major, minor).
- Default: (2, 1)
- :return: An error string that is empty if no error occured
- :rtype: str
+ :param shareOpenGLContexts:
+ True to test the `QApplication` with `AA_ShareOpenGLContexts`.
+ :return: Result status and error message
"""
major, minor = str(version[0]), str(version[1])
env = os.environ.copy()
- env['PYTHONPATH'] = os.pathsep.join(
- [os.path.abspath(p) for p in sys.path])
+ env["PYTHONPATH"] = os.pathsep.join([os.path.abspath(p) for p in sys.path])
+
+ cmd = [sys.executable, "-s", "-S", __file__, major, minor]
+ if shareOpenGLContexts:
+ cmd.append("--shareOpenGLContexts")
try:
- error = subprocess.check_output(
- [sys.executable, '-s', '-S', __file__, major, minor],
- env=env,
- timeout=2)
+ output = subprocess.check_output(cmd, env=env, timeout=2)
except subprocess.TimeoutExpired:
- status = False
error = "Qt OpenGL widget hang"
- if sys.platform.startswith('linux'):
- error += ':\nIf connected remotely, GLX forwarding might be disabled.'
+ if sys.platform.startswith("linux"):
+ error += ":\nIf connected remotely, GLX forwarding might be disabled."
+ return _isOpenGLAvailableResult(error)
except subprocess.CalledProcessError as e:
- status = False
- error = "Qt OpenGL widget error: retcode=%d, error=%s" % (e.returncode, e.output)
- else:
- status = True
- error = error.decode()
- return _isOpenGLAvailableResult(status, error)
+ return _isOpenGLAvailableResult(
+ f"Qt OpenGL widget error: retcode={e.returncode}, error={e.output}"
+ )
+
+ return _isOpenGLAvailableResult(output.decode(), status=True)
_runtimeCheckCache = {} # Cache runtime check results: {version: result}
-def isOpenGLAvailable(version=(2, 1), runtimeCheck=True):
+def isOpenGLAvailable(
+ version: tuple[int, int] = (2, 1),
+ runtimeCheck: bool = True,
+ shareOpenGLContexts: bool = False,
+) -> _isOpenGLAvailableResult:
"""Check if OpenGL is available through Qt and actually working.
After some basic tests, this is done by starting a subprocess that
displays a Qt OpenGL widget.
- :param List[int] version:
+ :param version:
The minimal required OpenGL version as a 2-tuple (major, minor).
Default: (2, 1)
- :param bool runtimeCheck:
- True (default) to run the test creating a Qt OpenGL widgt in a subprocess,
+ :param runtimeCheck:
+ True (default) to run the test creating a Qt OpenGL widget in a subprocess,
False to avoid this check.
+ :param shareOpenGLContexts:
+ True to test the `QApplication` with `AA_ShareOpenGLContexts`.
+ This only can be checked with `runtimeCheck` enabled.
+ Default is false.
:return: A result object that evaluates to True if successful and
which has a `status` boolean attribute (True if successful) and
an `error` string attribute that is not empty if `status` is False.
"""
- error = ''
-
- if sys.platform.startswith('linux') and not os.environ.get('DISPLAY', ''):
+ if sys.platform.startswith("linux") and not os.environ.get("DISPLAY", ""):
# On Linux and no DISPLAY available (e.g., ssh without -X)
- error = 'DISPLAY environment variable not set'
-
- else:
- # Check pyopengl availability
- try:
- import silx.gui._glutils.gl # noqa
- except ImportError:
- error = "Cannot import OpenGL wrapper: pyopengl is not installed"
- else:
- # Pre checks for Qt < 5.4
- if not hasattr(qt, 'QOpenGLWidget'):
- if not qt.HAS_OPENGL:
- error = '%s.QtOpenGL not available' % qt.BINDING
-
- elif qt.BINDING in ('PySide2', 'PyQt5') and 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'
-
- result = _isOpenGLAvailableResult(error == '', error)
-
- if result: # No error so far, runtime check
- if version in _runtimeCheckCache: # Use cache
- result = _runtimeCheckCache[version]
- elif runtimeCheck: # Run test in subprocess
- result = _runtimeOpenGLCheck(version)
- _runtimeCheckCache[version] = result
+ return _isOpenGLAvailableResult("DISPLAY environment variable not set")
+ # Check pyopengl availability
+ try:
+ from silx.gui._glutils import gl
+ except ImportError:
+ return _isOpenGLAvailableResult(
+ "Cannot import OpenGL wrapper: pyopengl is not installed"
+ )
+
+ # Pre checks for Qt < 5.4
+ if not hasattr(qt, "QOpenGLWidget"):
+ if not qt.HAS_OPENGL:
+ return _isOpenGLAvailableResult(f"{qt.BINDING}.QtOpenGL not available")
+
+ if (
+ qt.BINDING == "PyQt5"
+ and 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
+ return _isOpenGLAvailableResult("Qt reports OpenGL not available")
+
+ # Check compatibility between Qt platform and pyopengl selected platform
+ qt_qpa_platform = qt.QGuiApplication.platformName()
+ pyopengl_platform = gl.getPlatform()
+ if (qt_qpa_platform == "wayland" and pyopengl_platform != "EGLPlatform") or (
+ qt_qpa_platform == "xcb" and pyopengl_platform != "GLXPlatform"
+ ):
+ return _isOpenGLAvailableResult(
+ f"Qt platform '{qt_qpa_platform}' is not compatible with PyOpenGL platform '{pyopengl_platform}'"
+ )
+
+ keyCache = version, shareOpenGLContexts
+ if keyCache in _runtimeCheckCache: # Use cache
+ return _runtimeCheckCache[keyCache]
+
+ if not runtimeCheck:
+ return _isOpenGLAvailableResult(status=True)
+
+ # Run test in subprocess
+ result = _runtimeOpenGLCheck(version, shareOpenGLContexts)
+ _runtimeCheckCache[keyCache] = result
return result
@@ -149,15 +174,16 @@ if __name__ == "__main__":
class _TestOpenGLWidget(OpenGLWidget):
"""Widget checking that OpenGL is indeed available
- :param List[int] version: (major, minor) minimum OpenGL version
+ :param version: (major, minor) minimum OpenGL version
"""
- def __init__(self, version):
+ def __init__(self, version: tuple[int, int]):
super(_TestOpenGLWidget, self).__init__(
alphaBufferSize=0,
depthBufferSize=0,
stencilBufferSize=0,
- version=version)
+ version=version,
+ )
def paintEvent(self, event):
super(_TestOpenGLWidget, self).paintEvent(event)
@@ -171,22 +197,25 @@ if __name__ == "__main__":
qt.QTimer.singleShot(100, app.quit)
def paintGL(self):
- gl.glClearColor(1., 0., 0., 0.)
+ gl.glClearColor(1.0, 0.0, 0.0, 0.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
-
parser = argparse.ArgumentParser()
- parser.add_argument('major')
- parser.add_argument('minor')
+ parser.add_argument("major")
+ parser.add_argument("minor")
+ parser.add_argument("--shareOpenGLContexts", action="store_true")
args = parser.parse_args(args=sys.argv[1:])
+ if args.shareOpenGLContexts:
+ qt.QCoreApplication.setAttribute(qt.Qt.AA_ShareOpenGLContexts)
app = qt.QApplication([])
- window = qt.QMainWindow(flags=
- qt.Qt.Popup |
- qt.Qt.FramelessWindowHint |
- qt.Qt.NoDropShadowWindowHint |
- qt.Qt.WindowStaysOnTopHint)
+ window = qt.QMainWindow(
+ flags=qt.Qt.Popup
+ | qt.Qt.FramelessWindowHint
+ | qt.Qt.NoDropShadowWindowHint
+ | qt.Qt.WindowStaysOnTopHint
+ )
window.setAttribute(qt.Qt.WA_ShowWithoutActivating)
window.move(0, 0)
window.resize(3, 3)
diff --git a/src/silx/gui/utils/image.py b/src/silx/gui/utils/image.py
index 96f50ab..b9ab7c3 100644
--- a/src/silx/gui/utils/image.py
+++ b/src/silx/gui/utils/image.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,9 +27,6 @@
- :func:`convertQImageToArray`
"""
-from __future__ import division
-
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "04/09/2018"
@@ -43,7 +39,7 @@ from numpy.lib.stride_tricks import as_strided as _as_strided
from .. import qt
-def convertArrayToQImage(array):
+def convertArrayToQImage(array: numpy.ndarray) -> qt.QImage:
"""Convert an array-like image to a QImage.
The created QImage is using a copy of the array data.
@@ -54,71 +50,81 @@ def convertArrayToQImage(array):
Channels are expected to be either RGB or RGBA.
:type array: numpy.ndarray of uint8
:return: Corresponding Qt image with RGB888 or ARGB32 format.
- :rtype: QImage
"""
- array = numpy.array(array, copy=False, order='C', dtype=numpy.uint8)
+ array = numpy.array(array, copy=False, order="C", dtype=numpy.uint8)
if array.ndim != 3 or array.shape[2] not in (3, 4):
- raise ValueError(
- 'Image must be a 3D array with 3 or 4 channels per pixel')
+ raise ValueError("Image must be a 3D array with 3 or 4 channels per pixel")
if array.shape[2] == 4:
format_ = qt.QImage.Format_ARGB32
# RGBA -> ARGB + take care of endianness
- if sys.byteorder == 'little': # RGBA -> BGRA
+ if sys.byteorder == "little": # RGBA -> BGRA
array = array[:, :, (2, 1, 0, 3)]
else: # big endian: RGBA -> ARGB
array = array[:, :, (3, 0, 1, 2)]
- array = numpy.array(array, order='C') # Make a contiguous array
+ array = numpy.array(array, order="C") # Make a contiguous array
else: # array.shape[2] == 3
format_ = qt.QImage.Format_RGB888
height, width, depth = array.shape
qimage = qt.QImage(
- array.data,
- width,
- height,
- array.strides[0], # bytesPerLine
- format_)
+ array.data, width, height, array.strides[0], format_ # bytesPerLine
+ )
return qimage.copy() # Making a copy of the image and its data
-def convertQImageToArray(image):
+def convertQImageToArray(image: qt.QImage) -> numpy.ndarray:
"""Convert a QImage to a numpy array.
- If QImage format is not Format_RGB888, Format_RGBA8888 or Format_ARGB32,
- it is first converted to one of this format depending on
- the presence of an alpha channel.
+ If QImage format is not one of:
+
+ - Format_Grayscale8
+ - Format_RGB888
+ - Format_RGBA8888
+ - Format_ARGB32,
+
+ it is first converted to one of this format.
The created numpy array is using a copy of the QImage data.
:param QImage image: The QImage to convert.
- :return: The image array of RGB or RGBA channels of shape
- (height, width, channels (3 or 4))
- :rtype: numpy.ndarray of uint8
+ :return: Image array of uint8 of shape:
+
+ - (height, width) for grayscale images
+ - (height, width, channels (3 or 4)) for RGB and RGBA images
"""
- rgba8888 = getattr(qt.QImage, 'Format_RGBA8888', None) # Only in Qt5
+ supportedFormats = (
+ qt.QImage.Format_Grayscale8,
+ qt.QImage.Format_ARGB32,
+ qt.QImage.Format_RGB888,
+ qt.QImage.Format_RGBA8888,
+ )
# Convert to supported format if needed
- if image.format() not in (qt.QImage.Format_ARGB32,
- qt.QImage.Format_RGB888,
- rgba8888):
+ if image.format() not in supportedFormats:
if image.hasAlphaChannel():
- image = image.convertToFormat(
- rgba8888 if rgba8888 is not None else qt.QImage.Format_ARGB32)
+ image = image.convertToFormat(qt.QImage.Format_RGBA8888)
else:
image = image.convertToFormat(qt.QImage.Format_RGB888)
format_ = image.format()
- channels = 3 if format_ == qt.QImage.Format_RGB888 else 4
+ if format_ == qt.QImage.Format_Grayscale8:
+ channels = 1
+ elif format_ == qt.QImage.Format_RGB888:
+ channels = 3
+ else:
+ channels = 4
ptr = image.bits()
- if qt.BINDING == 'PyQt5':
+ if qt.BINDING == "PyQt5":
ptr.setsize(image.byteCount())
- elif qt.BINDING in ('PySide2', 'PySide6'):
+ elif qt.BINDING == "PyQt6":
+ ptr.setsize(image.sizeInBytes())
+ elif qt.BINDING == "PySide6":
ptr = ptr.tobytes()
else:
raise RuntimeError("Unsupported Qt binding: %s" % qt.BINDING)
@@ -127,17 +133,21 @@ def convertQImageToArray(image):
view = _as_strided(
numpy.frombuffer(ptr, dtype=numpy.uint8),
shape=(image.height(), image.width(), channels),
- strides=(image.bytesPerLine(), channels, 1))
+ strides=(image.bytesPerLine(), channels, 1),
+ )
if format_ == qt.QImage.Format_ARGB32:
# Convert from ARGB to RGBA
# Not a byte-ordered format: do care about endianness
- if sys.byteorder == 'little': # BGRA -> RGBA
+ if sys.byteorder == "little": # BGRA -> RGBA
view = view[:, :, (2, 1, 0, 3)]
else: # big endian: ARGB -> RGBA
view = view[:, :, (1, 2, 3, 0)]
+ if channels == 1: # Remove channel dimension
+ view = view[:, :, 0]
+
# Format_RGB888 and Format_RGBA8888 do not need reshuffling channels:
# They are byte-ordered and already in the right order
- return numpy.array(view, copy=True, order='C')
+ return numpy.array(view, copy=True, order="C")
diff --git a/src/silx/gui/utils/matplotlib.py b/src/silx/gui/utils/matplotlib.py
index 90257f8..c51ccd2 100644
--- a/src/silx/gui/utils/matplotlib.py
+++ b/src/silx/gui/utils/matplotlib.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2024 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
@@ -23,8 +22,6 @@
#
# ###########################################################################*/
-from __future__ import absolute_import
-
"""This module initializes matplotlib and sets-up the backend to use.
It MUST be imported prior to any other import of matplotlib.
@@ -32,34 +29,157 @@ It MUST be imported prior to any other import of matplotlib.
It provides the matplotlib :class:`FigureCanvasQTAgg` class corresponding
to the used backend.
"""
+from __future__ import annotations
+
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "02/05/2018"
-from pkg_resources import parse_version
+import io
import matplotlib
+import numpy
from .. import qt
+# This must be performed before any import from matplotlib
+if qt.BINDING in ("PySide6", "PyQt6", "PyQt5"):
+ matplotlib.use("Qt5Agg", force=False)
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg # noqa
+
+else:
+ raise ImportError("Unsupported Qt binding: %s" % qt.BINDING)
+
+
+from matplotlib.font_manager import FontProperties
+from matplotlib.mathtext import MathTextParser
+from matplotlib.ticker import ScalarFormatter as _ScalarFormatter
+from matplotlib import figure, font_manager
+from packaging.version import Version
+
+_MATPLOTLIB_VERSION = Version(matplotlib.__version__)
+
+
+class DefaultTickFormatter(_ScalarFormatter):
+ """Tick label formatter"""
+
+ def __init__(self):
+ super().__init__(useOffset=True, useMathText=True)
+ self.set_scientific(True)
+ self.create_dummy_axis()
+
+ if _MATPLOTLIB_VERSION < Version("3.1.0"):
+
+ def format_ticks(self, values):
+ self.set_locs(values)
+ return [self(value, i) for i, value in enumerate(values)]
-def _matplotlib_use(backend, force):
- """Wrapper of `matplotlib.use` to set-up backend.
- It adds extra initialization for PySide2 with matplotlib < 2.2.
+_FONT_STYLES = {
+ qt.QFont.StyleNormal: "normal",
+ qt.QFont.StyleItalic: "italic",
+ qt.QFont.StyleOblique: "oblique",
+}
+
+
+def qFontToFontProperties(font: qt.QFont):
+ """Convert a QFont to a matplotlib FontProperties"""
+ weightFactor = 10 if qt.BINDING == "PyQt5" else 1
+ families = [font.family(), font.defaultFamily()]
+ if _MATPLOTLIB_VERSION >= Version("3.6.0"):
+ # Prevent 'Font family not found' warnings
+ availableNames = font_manager.get_font_names()
+ families = [f for f in families if f in availableNames]
+ families.append(font_manager.fontManager.defaultFamily["ttf"])
+
+ if "Sans" in font.family():
+ families.insert(0, "sans-serif")
+
+ return FontProperties(
+ family=families,
+ style=_FONT_STYLES[font.style()],
+ weight=weightFactor * font.weight(),
+ size=font.pointSizeF(),
+ )
+
+
+def rasterMathText(
+ text: str,
+ font: qt.QFont,
+ dotsPerInch: float = 96.0,
+) -> tuple[numpy.ndarray, float]:
+ """Raster text using matplotlib supporting latex-like math syntax.
+
+ It supports multiple lines.
+
+ :param text: The text to raster
+ :param font: Font to use
+ :param dotsPerInch: The DPI resolution of the created image
+ :return: Corresponding image in gray scale and baseline offset from top
"""
- # This is kept for compatibility with matplotlib < 2.2
- if (parse_version(matplotlib.__version__) < parse_version('2.2') and
- qt.BINDING == 'PySide2'):
- matplotlib.rcParams['backend.qt5'] = 'PySide2'
+ # Implementation adapted from:
+ # https://github.com/matplotlib/matplotlib/blob/d624571a19aec7c7d4a24123643288fc27db17e7/lib/matplotlib/mathtext.py#L264
- matplotlib.use(backend, force=force)
+ stripped_text = text.strip("\n")
+ font_prop = qFontToFontProperties(font)
+ parser = MathTextParser("path")
+ lines_info = [
+ parser.parse(line, prop=font_prop, dpi=dotsPerInch)
+ for line in stripped_text.split("\n")
+ ]
+ max_line_width = max(info[0] for info in lines_info)
+ # Use lp string as minimum height/ascent
+ ref_info = parser.parse("lp", prop=font_prop, dpi=dotsPerInch)
+ line_height = max(
+ ref_info[1],
+ *(info[1] for info in lines_info),
+ )
+ first_line_ascent = max(
+ ref_info[1] - ref_info[2], lines_info[0][1] - lines_info[0][2]
+ )
-if qt.BINDING in ('PySide6', 'PyQt5', 'PySide2'):
- _matplotlib_use('Qt5Agg', force=False)
- from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg # noqa
+ linespacing = 1.2
-else:
- raise ImportError("Unsupported Qt binding: %s" % qt.BINDING)
+ figure_height = numpy.ceil(line_height * len(lines_info) * linespacing) + 2
+ fig = figure.Figure(
+ figsize=(
+ (max_line_width + 1) / dotsPerInch,
+ figure_height / dotsPerInch,
+ )
+ )
+ fig.set_dpi(dotsPerInch)
+ text = fig.text(
+ 0,
+ 1,
+ stripped_text,
+ fontproperties=font_prop,
+ verticalalignment="top",
+ )
+ text.set_linespacing(linespacing)
+ with io.BytesIO() as buffer:
+ fig.savefig(buffer, dpi=dotsPerInch, format="raw")
+ canvas_width, canvas_height = fig.get_window_extent().max
+ buffer.seek(0)
+ image = numpy.frombuffer(buffer.read(), dtype=numpy.uint8).reshape(
+ int(canvas_height), int(canvas_width), 4
+ )
+
+ # RGB to inverted R channel
+ array = 255 - image[:, :, 0]
+
+ # Remove leading/trailing empty columns and trailing rows but one on each side
+ filled_rows = numpy.nonzero(numpy.sum(array, axis=1))[0]
+ filled_columns = numpy.nonzero(numpy.sum(array, axis=0))[0]
+ if len(filled_rows) == 0 or len(filled_columns) == 0:
+ return array, first_line_ascent
+ return (
+ numpy.ascontiguousarray(
+ array[
+ 0 : filled_rows[-1] + 2,
+ max(0, filled_columns[0] - 1) : filled_columns[-1] + 2,
+ ]
+ ),
+ first_line_ascent,
+ )
diff --git a/src/silx/gui/utils/projecturl.py b/src/silx/gui/utils/projecturl.py
index 0832c2e..125e8e7 100644
--- a/src/silx/gui/utils/projecturl.py
+++ b/src/silx/gui/utils/projecturl.py
@@ -1,4 +1,3 @@
-# coding: utf-8
#
# Project: Azimuthal integration
# https://github.com/silx-kit/silx
@@ -23,8 +22,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-from __future__ import absolute_import, print_function, division
-
"""Provide convenient URL for silx-kit projects."""
__author__ = "Valentin Valls"
@@ -70,7 +67,8 @@ def getDocumentationUrl(subpath):
"minor": version.MINOR,
"micro": version.MICRO,
"relev": version.RELEV,
- "subpath": subpath}
+ "subpath": subpath,
+ }
template = BASE_DOC_URL
if template is None:
template = _DEFAULT_BASE_DOC_URL
diff --git a/src/silx/gui/utils/qtutils.py b/src/silx/gui/utils/qtutils.py
index 9682913..d686a48 100755
--- a/src/silx/gui/utils/qtutils.py
+++ b/src/silx/gui/utils/qtutils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2020 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/utils/signal.py b/src/silx/gui/utils/signal.py
index 359f5cc..00a4d9b 100644
--- a/src/silx/gui/utils/signal.py
+++ b/src/silx/gui/utils/signal.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2012 University of North Carolina at Chapel Hill, Luke Campagnola
@@ -31,8 +30,8 @@ import weakref
from time import time
from silx.gui.utils import concurrent
-__all__ = ['SignalProxy']
-__authors__ = ['L. Campagnola', 'M. Liberty']
+__all__ = ["SignalProxy"]
+__authors__ = ["L. Campagnola", "M. Liberty"]
__license__ = "MIT"
@@ -92,7 +91,9 @@ class SignalProxy(qt.QObject):
leakTime = max(0, (lastFlush + (1.0 / self.rateLimit)) - now)
concurrent.submitToQtMainThread(self.timer.stop)
- concurrent.submitToQtMainThread(self.timer.start, (min(leakTime, self.delay) * 1000) + 1)
+ concurrent.submitToQtMainThread(
+ self.timer.start, (min(leakTime, self.delay) * 1000) + 1
+ )
# self.timer.stop()
# self.timer.start((min(leakTime, self.delay) * 1000) + 1)
@@ -120,22 +121,19 @@ class SignalProxy(qt.QObject):
pass
-if __name__ == '__main__':
+if __name__ == "__main__":
app = qt.QApplication([])
win = qt.QMainWindow()
spin = qt.QSpinBox()
win.setCentralWidget(spin)
win.show()
-
def fn(*args):
print("Raw signal:", args)
-
def fn2(*args):
print("Delayed signal:", args)
-
spin.valueChanged.connect(fn)
# proxy = proxyConnect(spin, QtCore.SIGNAL('valueChanged(int)'), fn)
proxy = SignalProxy(spin.valueChanged, delay=0.5, slot=fn2)
diff --git a/src/silx/gui/utils/test/__init__.py b/src/silx/gui/utils/test/__init__.py
index 15cd186..7a8edb9 100755
--- a/src/silx/gui/utils/test/__init__.py
+++ b/src/silx/gui/utils/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/utils/test/test.py b/src/silx/gui/utils/test/test.py
index 0208d64..59c031e 100644
--- a/src/silx/gui/utils/test/test.py
+++ b/src/silx/gui/utils/test/test.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2019-2021 European Synchrotron Radiation Facility
@@ -24,14 +23,11 @@
# ###########################################################################*/
"""Test of functions available in silx.gui.utils module."""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "01/08/2019"
-import unittest
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt, SignalListener
diff --git a/src/silx/gui/utils/test/test_async.py b/src/silx/gui/utils/test/test_async.py
index 7304ca9..ef61df2 100644
--- a/src/silx/gui/utils/test/test_async.py
+++ b/src/silx/gui/utils/test/test_async.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -24,16 +23,12 @@
# ###########################################################################*/
"""Test of async module."""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "09/03/2018"
import threading
-import unittest
-
from concurrent.futures import wait
from silx.gui import qt
@@ -54,7 +49,7 @@ class TestSubmitToQtThread(TestCaseQt):
return value1, value2
def _taskWithException(self, *args, **kwargs):
- raise RuntimeError('task exception')
+ raise RuntimeError("task exception")
def testFromMainThread(self):
"""Call submitToQtMainThread from the main thread"""
@@ -100,10 +95,11 @@ class TestSubmitToQtThread(TestCaseQt):
if not thread.is_alive():
break
else:
- self.fail(('Thread task still running'))
+ self.fail(("Thread task still running"))
def testFromQtThread(self):
"""Call submitToQtMainThread from a Qt thread pool"""
+
class Runner(qt.QRunnable):
def __init__(self, fn):
super(Runner, self).__init__()
@@ -124,4 +120,4 @@ class TestSubmitToQtThread(TestCaseQt):
if done:
break
else:
- self.fail('Thread pool task still running')
+ self.fail("Thread pool task still running")
diff --git a/src/silx/gui/utils/test/test_glutils.py b/src/silx/gui/utils/test/test_glutils.py
index 7c9831b..fb19e36 100644
--- a/src/silx/gui/utils/test/test_glutils.py
+++ b/src/silx/gui/utils/test/test_glutils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2020 European Synchrotron Radiation Facility
@@ -30,26 +29,26 @@ __date__ = "15/01/2020"
import logging
-import unittest
+import pytest
+
from silx.gui.utils.glutils import isOpenGLAvailable
_logger = logging.getLogger(__name__)
-class TestIsOpenGLAvailable(unittest.TestCase):
- """Test isOpenGLAvailable"""
-
- def test(self):
- for version in ((2, 1), (2, 1), (1000, 1)):
- with self.subTest(version=version):
- result = isOpenGLAvailable(version=version)
- _logger.info("isOpenGLAvailable returned: %s", str(result))
- if version[0] == 1000:
- self.assertFalse(result)
- if not result:
- self.assertFalse(result.status)
- self.assertTrue(len(result.error) > 0)
- else:
- self.assertTrue(result.status)
- self.assertTrue(len(result.error) == 0)
+@pytest.mark.parametrize(
+ "params", (((2, 1), False), ((2, 1), False), ((1000, 1), False), ((2, 1), True))
+)
+def testOpenGLAvailable(params):
+ version, shareOpenGLContexts = params
+ result = isOpenGLAvailable(version=version, shareOpenGLContexts=shareOpenGLContexts)
+ _logger.info("isOpenGLAvailable returned: %s", str(result))
+ if version[0] == 1000:
+ assert not result
+ if not result:
+ assert not result.status
+ assert len(result.error) > 0
+ else:
+ assert result.status
+ assert len(result.error) == 0
diff --git a/src/silx/gui/utils/test/test_image.py b/src/silx/gui/utils/test/test_image.py
index 62316b0..9ae1b80 100644
--- a/src/silx/gui/utils/test/test_image.py
+++ b/src/silx/gui/utils/test/test_image.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,51 +28,58 @@ __license__ = "MIT"
__date__ = "16/01/2017"
import numpy
-import unittest
+import pytest
from silx.gui import qt
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.utils.testutils import TestCaseQt
from silx.gui.utils.image import convertArrayToQImage, convertQImageToArray
-class TestQImageConversion(TestCaseQt, ParametricTestCase):
- """Tests conversion of QImage to/from numpy array."""
+@pytest.mark.parametrize(
+ "format_, channels",
+ [
+ (qt.QImage.Format_RGB888, 3), # Native support
+ (qt.QImage.Format_ARGB32, 4), # Native support
+ ],
+)
+def testConvertArrayToQImage(format_, channels):
+ """Test conversion of numpy array to QImage"""
+ image = numpy.arange(3 * 3 * channels, dtype=numpy.uint8).reshape(3, 3, channels)
+ qimage = convertArrayToQImage(image)
- def testConvertArrayToQImage(self):
- """Test conversion of numpy array to QImage"""
- for format_, channels in [('Format_RGB888', 3),
- ('Format_ARGB32', 4)]:
- with self.subTest(format_):
- image = numpy.arange(
- 3*3*channels, dtype=numpy.uint8).reshape(3, 3, channels)
- qimage = convertArrayToQImage(image)
+ assert (qimage.height(), qimage.width()) == image.shape[:2]
+ assert qimage.format() == format_
- self.assertEqual(qimage.height(), image.shape[0])
- self.assertEqual(qimage.width(), image.shape[1])
- self.assertEqual(qimage.format(), getattr(qt.QImage, format_))
+ for row in range(3):
+ for col in range(3):
+ # Qrgb has no alpha channel, not compared
+ # Qt uses x,y while array is row,col...
+ assert qt.QColor(qimage.pixel(col, row)) == qt.QColor(*image[row, col, :3])
- for row in range(3):
- for col in range(3):
- # Qrgb has no alpha channel, not compared
- # Qt uses x,y while array is row,col...
- self.assertEqual(qt.QColor(qimage.pixel(col, row)),
- qt.QColor(*image[row, col, :3]))
+@pytest.mark.parametrize(
+ "format_, channels",
+ [
+ (qt.QImage.Format_RGB888, 3), # Native support
+ (qt.QImage.Format_ARGB32, 4), # Native support
+ (qt.QImage.Format_RGB32, 3), # Conversion to RGB
+ ],
+)
+def testConvertQImageToArray(format_, channels):
+ """Test conversion of QImage to numpy array"""
+ color = numpy.arange(channels) # RGB(A) values
+ qimage = qt.QImage(3, 3, format_)
+ qimage.fill(qt.QColor(*color))
+ image = convertQImageToArray(qimage)
- def testConvertQImageToArray(self):
- """Test conversion of QImage to numpy array"""
- for format_, channels in [
- ('Format_RGB888', 3), # Native support
- ('Format_ARGB32', 4), # Native support
- ('Format_RGB32', 3)]: # Conversion to RGB
- with self.subTest(format_):
- color = numpy.arange(channels) # RGB(A) values
- qimage = qt.QImage(3, 3, getattr(qt.QImage, format_))
- qimage.fill(qt.QColor(*color))
- image = convertQImageToArray(qimage)
+ assert (qimage.height(), qimage.width(), len(color)) == image.shape
+ assert numpy.all(numpy.equal(image, color))
- self.assertEqual(qimage.height(), image.shape[0])
- self.assertEqual(qimage.width(), image.shape[1])
- self.assertEqual(image.shape[2], len(color))
- self.assertTrue(numpy.all(numpy.equal(image, color)))
+
+def testConvertQImageToArrayGrayscale():
+ """Test conversion of grayscale QImage to numpy array"""
+ qimage = qt.QImage(3, 3, qt.QImage.Format_Grayscale8)
+ qimage.fill(1)
+ image = convertQImageToArray(qimage)
+
+ assert (qimage.height(), qimage.width()) == image.shape
+ assert numpy.all(numpy.equal(image, 1))
diff --git a/src/silx/gui/utils/test/test_qtutils.py b/src/silx/gui/utils/test/test_qtutils.py
index c00280b..23e6cdf 100755
--- a/src/silx/gui/utils/test/test_qtutils.py
+++ b/src/silx/gui/utils/test/test_qtutils.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2019 European Synchrotron Radiation Facility
@@ -24,14 +23,11 @@
# ###########################################################################*/
"""Test of functions available in silx.gui.utils module."""
-from __future__ import absolute_import
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "01/08/2019"
-import unittest
from silx.gui import qt
from silx.gui import utils
from silx.gui.utils.testutils import TestCaseQt
diff --git a/src/silx/gui/utils/test/test_testutils.py b/src/silx/gui/utils/test/test_testutils.py
index 07294a7..2277cb3 100644
--- a/src/silx/gui/utils/test/test_testutils.py
+++ b/src/silx/gui/utils/test/test_testutils.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,16 +28,13 @@ __license__ = "MIT"
__date__ = "16/01/2017"
import unittest
-import sys
-from silx.gui import qt
from ..testutils import TestCaseQt
class TestOutcome(unittest.TestCase):
"""Tests conversion of QImage to/from numpy array."""
- @unittest.skipIf(sys.version_info.major <= 2, 'Python3 only')
def testNoneOutcome(self):
test = TestCaseQt()
test._currentTestSucceeded()
diff --git a/src/silx/gui/utils/testutils.py b/src/silx/gui/utils/testutils.py
index 40c8237..76d0b9b 100644
--- a/src/silx/gui/utils/testutils.py
+++ b/src/silx/gui/utils/testutils.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +25,7 @@
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "22/11/2023"
import gc
@@ -43,14 +42,14 @@ from silx.gui import qt
from silx.gui.qt import inspect as _inspect
-if qt.BINDING == 'PySide2':
- from PySide2.QtTest import QTest
-elif qt.BINDING == 'PyQt5':
+if qt.BINDING == "PyQt5":
from PyQt5.QtTest import QTest
-elif qt.BINDING == 'PySide6':
+elif qt.BINDING == "PySide6":
from PySide6.QtTest import QTest
+elif qt.BINDING == "PyQt6":
+ from PyQt6.QtTest import QTest
else:
- raise ImportError('Unsupported Qt bindings')
+ raise ImportError("Unsupported Qt bindings")
def qWaitForWindowExposedAndActivate(window, timeout=None):
@@ -84,7 +83,7 @@ class TestCaseQt(unittest.TestCase):
To allow some widgets to remain alive at the end of a test, set the
allowedLeakingWidgets attribute to the number of widgets that can remain
alive at the end of the test.
- With PySide2, this test is not run for now as it seems PySide2
+ With PySide, this test is not run for now as it seems PySide
is leaking widgets internally.
All keyboard and mouse event simulation methods call qWait(20) after
@@ -110,8 +109,9 @@ class TestCaseQt(unittest.TestCase):
@classmethod
def exceptionHandler(cls, exceptionClass, exception, stack):
import traceback
- message = (''.join(traceback.format_tb(stack)))
- template = 'Traceback (most recent call last):\n{2}{0}: {1}'
+
+ message = "".join(traceback.format_tb(stack))
+ template = "Traceback (most recent call last):\n{2}{0}: {1}"
message = template.format(exceptionClass.__name__, exception, message)
cls._exceptions.append(message)
@@ -132,37 +132,46 @@ class TestCaseQt(unittest.TestCase):
def setUp(self):
"""Get the list of existing widgets."""
self.allowedLeakingWidgets = 0
- if qt.BINDING in ('PySide2', 'PySide6'):
+ if qt.BINDING == "PySide6":
self.__previousWidgets = None
else:
self.__previousWidgets = self.qapp.allWidgets()
self.__class__._exceptions = []
def _currentTestSucceeded(self):
- if hasattr(self, '_outcome'):
- # For Python >= 3.4
- result = self.defaultTestResult() # these 2 methods have no side effects
- if hasattr(self._outcome, 'errors'):
+ if hasattr(self, "_feedErrorsToResult"):
+ # Python 3.4 - 3.10 (These two methods have no side effects)
+ result = self.defaultTestResult()
+ if hasattr(self._outcome, "errors"):
self._feedErrorsToResult(result, self._outcome.errors)
- else:
- # For Python < 3.4
- result = getattr(self, '_outcomeForDoCleanups', self._resultForDoCleanups)
+ elif hasattr(self._outcome, "result"):
+ # Python 3.11+
+ result = self._outcome.result
- skipped = self.id() in [case.id() for case, _ in result.skipped]
- error = self.id() in [case.id() for case, _ in result.errors]
- failure = self.id() in [case.id() for case, _ in result.failures]
- return not error and not failure and not skipped
+ if self._outcome is None:
+ return True
+ elif hasattr(self._outcome, "success"):
+ # using pytest
+ return self._outcome.success
+ else:
+ # using unittest
+ return all(test != self for test, text in result.errors + result.failures)
def _checkForUnreleasedWidgets(self):
"""Test fixture checking that no more widgets exists."""
- gc.collect()
-
if self.__previousWidgets is None:
- return # Do not test for leaking widgets with PySide2
+ return # Do not test for leaking widgets with PySide
+
+ gc.collect()
- widgets = [widget for widget in self.qapp.allWidgets()
- if (widget not in self.__previousWidgets and
- _inspect.createdByPython(widget))]
+ widgets = [
+ widget
+ for widget in self.qapp.allWidgets()
+ if (
+ widget not in self.__previousWidgets
+ and _inspect.createdByPython(widget)
+ )
+ ]
self.__previousWidgets = None
allowedLeakingWidgets = self.allowedLeakingWidgets
@@ -170,12 +179,11 @@ class TestCaseQt(unittest.TestCase):
if widgets and len(widgets) <= allowedLeakingWidgets:
_logger.info(
- '%s: %d remaining widgets after test' % (self.id(),
- len(widgets)))
+ "%s: %d remaining widgets after test" % (self.id(), len(widgets))
+ )
if len(widgets) > allowedLeakingWidgets:
- raise RuntimeError(
- "Test ended with widgets alive: %s" % str(widgets))
+ raise RuntimeError("Test ended with widgets alive: %s" % str(widgets))
def tearDown(self):
self.qapp.processEvents()
@@ -203,8 +211,9 @@ class TestCaseQt(unittest.TestCase):
Click = QTest.Click
"""Key click action code"""
- QTest = property(lambda self: QTest,
- doc="""The Qt QTest class from the used Qt binding.""")
+ QTest = property(
+ lambda self: QTest, doc="""The Qt QTest class from the used Qt binding."""
+ )
def keyClick(self, widget, key, modifier=qt.Qt.NoModifier, delay=-1):
"""Simulate clicking a key.
@@ -222,8 +231,7 @@ class TestCaseQt(unittest.TestCase):
QTest.keyClicks(widget, sequence, modifier, delay)
self.qWait(20)
- def keyEvent(self, action, widget, key,
- modifier=qt.Qt.NoModifier, delay=-1):
+ def keyEvent(self, action, widget, key, modifier=qt.Qt.NoModifier, delay=-1):
"""Sends a Qt key event.
See QTest.keyEvent for details.
@@ -253,7 +261,7 @@ class TestCaseQt(unittest.TestCase):
See QTest.mouseClick for details.
"""
if modifier is None:
- modifier = qt.Qt.KeyboardModifiers()
+ modifier = self.qapp.keyboardModifiers()
pos = qt.QPoint(int(pos[0]), int(pos[1])) if pos is not None else qt.QPoint()
QTest.mouseClick(widget, button, modifier, pos, delay)
self.qWait(20)
@@ -264,7 +272,7 @@ class TestCaseQt(unittest.TestCase):
See QTest.mouseDClick for details.
"""
if modifier is None:
- modifier = qt.Qt.KeyboardModifiers()
+ modifier = self.qapp.keyboardModifiers()
pos = qt.QPoint(int(pos[0]), int(pos[1])) if pos is not None else qt.QPoint()
QTest.mouseDClick(widget, button, modifier, pos, delay)
self.qWait(20)
@@ -284,7 +292,7 @@ class TestCaseQt(unittest.TestCase):
See QTest.mousePress for details.
"""
if modifier is None:
- modifier = qt.Qt.KeyboardModifiers()
+ modifier = self.qapp.keyboardModifiers()
pos = qt.QPoint(int(pos[0]), int(pos[1])) if pos is not None else qt.QPoint()
QTest.mousePress(widget, button, modifier, pos, delay)
self.qWait(20)
@@ -295,7 +303,7 @@ class TestCaseQt(unittest.TestCase):
See QTest.mouseRelease for details.
"""
if modifier is None:
- modifier = qt.Qt.KeyboardModifiers()
+ modifier = self.qapp.keyboardModifiers()
pos = qt.QPoint(int(pos[0]), int(pos[1])) if pos is not None else qt.QPoint()
QTest.mouseRelease(widget, button, modifier, pos, delay)
self.qWait(20)
@@ -316,14 +324,13 @@ class TestCaseQt(unittest.TestCase):
if ms is None:
ms = cls.DEFAULT_TIMEOUT_WAIT
- if qt.BINDING in ('PySide2', 'PySide6'):
- # PySide2 has no qWait, provide a replacement
+ if qt.BINDING == "PySide6":
+ # PySide has no qWait, provide a replacement
timeout = int(ms)
endTimeMS = int(time.time() * 1000) + timeout
qapp = qt.QApplication.instance()
while timeout > 0:
- qapp.processEvents(qt.QEventLoop.AllEvents,
- timeout)
+ qapp.processEvents(qt.QEventLoop.AllEvents, timeout)
timeout = endTimeMS - int(time.time() * 1000)
else:
QTest.qWait(int(ms) + cls.TIMEOUT_WAIT)
@@ -415,8 +422,7 @@ class TestCaseQt(unittest.TestCase):
class SignalListener(object):
- """Util to listen a Qt event and store parameters
- """
+ """Util to listen a Qt event and store parameters"""
def __init__(self):
self.__calls = []
@@ -486,7 +492,7 @@ def getQToolButtonFromAction(action):
:param QAction action: The QAction from which to get QToolButton.
:return: A QToolButton associated to action or None.
"""
- if qt.BINDING == "PySide6":
+ if qt.BINDING in ("PySide6", "PyQt6"):
widgets = action.associatedObjects()
else:
widgets = action.associatedWidgets()
@@ -498,7 +504,7 @@ def getQToolButtonFromAction(action):
def findChildren(parent, kind, name=None):
- if qt.BINDING in ("PySide2", "PySide6") and name is not None:
+ if qt.BINDING == "PySide6" and name is not None:
result = []
for obj in parent.findChildren(kind):
if obj.objectName() == name:
diff --git a/src/silx/gui/widgets/BoxLayoutDockWidget.py b/src/silx/gui/widgets/BoxLayoutDockWidget.py
index 3d2b853..aa45153 100644
--- a/src/silx/gui/widgets/BoxLayoutDockWidget.py
+++ b/src/silx/gui/widgets/BoxLayoutDockWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/widgets/ColormapNameComboBox.py b/src/silx/gui/widgets/ColormapNameComboBox.py
index fa8faf1..388b032 100644
--- a/src/silx/gui/widgets/ColormapNameComboBox.py
+++ b/src/silx/gui/widgets/ColormapNameComboBox.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""A QComboBox to display prefered colormaps
"""
-from __future__ import division
-
__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
__date__ = "27/11/2018"
diff --git a/src/silx/gui/widgets/ElidedLabel.py b/src/silx/gui/widgets/ElidedLabel.py
index 7c6dfb5..ae45931 100644
--- a/src/silx/gui/widgets/ElidedLabel.py
+++ b/src/silx/gui/widgets/ElidedLabel.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
@@ -28,17 +27,18 @@
__license__ = "MIT"
__date__ = "07/12/2018"
+from ...utils.deprecation import deprecated
from silx.gui import qt
class ElidedLabel(qt.QLabel):
- """QLabel with an edile property.
+ """QLabel with an elide property.
- By default if the text is too big, it is elided on the right.
+ By default if the text is too long, it is elided on the right.
This mode can be changed with :func:`setElideMode`.
- In case the text is elided, the full content is displayed as part of the
+ In this case the text is elided, the full content is displayed as part of the
tool tip. This behavior can be disabled with :func:`setTextAsToolTip`.
"""
@@ -62,7 +62,7 @@ class ElidedLabel(qt.QLabel):
def __updateMinimumSize(self):
metrics = self.fontMetrics()
- if qt.BINDING in ('PySide2', 'PyQt5'):
+ if qt.BINDING == "PyQt5":
width = metrics.width("...")
else: # Qt6
width = metrics.horizontalAdvance("...")
@@ -83,25 +83,41 @@ class ElidedLabel(qt.QLabel):
else:
qt.QLabel.setToolTip(self, self.__toolTip)
- # Properties
+ # Inherited properties
+
+ def text(self):
+ """Returns the text defined by the user.
+
+ It can be different from the one really displayed, depending on the
+ `elideMode` defined for this widget.
+ """
+ return self.__text
+
+ @deprecated(replacement="text", since_version="1.1.0")
+ def getText(self):
+ return self.text()
def setText(self, text):
self.__text = text
self.__updateText()
- def getText(self):
- return self.__text
+ def toolTip(self):
+ """Returns the tooltip defined by the user.
- text = qt.Property(str, getText, setText)
+ It can be different from the one really displayed, if `textAsToolTip` was
+ set to true.
+ """
+ return self.__toolTip
+
+ @deprecated(replacement="toolTip", since_version="1.1.0")
+ def getToolTip(self):
+ return self.toolTip()
def setToolTip(self, toolTip):
self.__toolTip = toolTip
self.__updateToolTip()
- def getToolTip(self):
- return self.__toolTip
-
- toolTip = qt.Property(str, getToolTip, setToolTip)
+ # New properties
def setElideMode(self, elideMode):
"""Set the elide mode.
@@ -118,7 +134,7 @@ class ElidedLabel(qt.QLabel):
"""
return self.__elideMode
- elideMode = qt.Property(qt.Qt.TextElideMode, getToolTip, setToolTip)
+ elideMode = qt.Property(qt.Qt.TextElideMode, getElideMode, setElideMode)
def setTextAsToolTip(self, enabled):
"""Enable displaying text as part of the tooltip if it is elided.
diff --git a/src/silx/gui/widgets/FloatEdit.py b/src/silx/gui/widgets/FloatEdit.py
index 08ed67d..f9d7331 100644
--- a/src/silx/gui/widgets/FloatEdit.py
+++ b/src/silx/gui/widgets/FloatEdit.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,8 +23,8 @@
# ###########################################################################*/
"""Module contains a float editor
"""
+from __future__ import annotations
-from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
@@ -37,18 +36,33 @@ from .. import qt
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.
+ The value can be modified with :meth:`value` and :meth:`setValue`.
+
+ The property :meth:`widgetResizable` allow to change the default
+ behaviour in order to automatically resize the widget to the displayed value.
+ Use :meth:`setMinimumWidth` to enforce the minimum width.
+
+ :param parent: Parent of the widget
+ :param value: The value to set the QLineEdit to.
"""
- def __init__(self, parent=None, value=None):
+
+ _QLineEditPrivateHorizontalMargin = 2
+ """Constant from Qt source code"""
+
+ def __init__(self, parent: qt.QWidget | None = None, value: float | None = None):
qt.QLineEdit.__init__(self, parent)
validator = qt.QDoubleValidator(self)
+ self.__widgetResizable: bool = False
+ self.__minimumWidth = 30
+ """Store the minimum width requested by the user, the real one is
+ dynamic"""
self.setValidator(validator)
self.setAlignment(qt.Qt.AlignRight)
+ self.textChanged.connect(self.__textChanged)
if value is not None:
self.setValue(value)
- def value(self):
+ def value(self) -> float:
"""Return the QLineEdit current value as a float."""
text = self.text()
value, validated = self.validator().locale().toDouble(text)
@@ -56,16 +70,85 @@ class FloatEdit(qt.QLineEdit):
self.setValue(value)
return value
- def setValue(self, value):
+ def setValue(self, value: float):
"""Set the current value of the LineEdit
- :param float value: The value to set the QLineEdit to.
+ :param value: The value to set the QLineEdit to.
"""
locale = self.validator().locale()
if qt.BINDING == "PySide6":
# Fix for PySide6 not selecting the right method
- text = locale.toString(float(value), 'g')
+ text = locale.toString(float(value), "g")
else:
text = locale.toString(float(value))
self.setText(text)
+ if self.__widgetResizable:
+ self.__forceMinimumWidthFromContent()
+
+ def __textChanged(self, text: str):
+ if self.__widgetResizable:
+ self.__forceMinimumWidthFromContent()
+
+ def widgetResizable(self) -> bool:
+ """
+ Returns whether or not the widget auto resizes itself based on it's content
+ """
+ return self.__widgetResizable
+
+ def setWidgetResizable(self, resizable: bool):
+ """
+ If true, the widget will automatically resize itself to its displayed content.
+
+ This avoids to have to scroll to see the widget's content, and allow to take
+ advantage of extra space.
+ """
+ if self.__widgetResizable == resizable:
+ return
+ self.__widgetResizable = resizable
+ self.updateGeometry()
+ if resizable:
+ self.__forceMinimumWidthFromContent()
+ else:
+ qt.QLineEdit.setMinimumWidth(self, self.__minimumWidth)
+
+ def __minimumWidthFromContent(self) -> int:
+ """Minimum size for the widget to properly read the actual number"""
+ text = self.text()
+ font = self.font()
+ metrics = qt.QFontMetrics(font)
+ margins = self.textMargins()
+ width = (
+ metrics.horizontalAdvance(text)
+ + self._QLineEditPrivateHorizontalMargin * 2
+ + margins.left()
+ + margins.right()
+ )
+ width = max(self.__minimumWidth, width)
+ opt = qt.QStyleOptionFrame()
+ self.initStyleOption(opt)
+ s = self.style().sizeFromContents(
+ qt.QStyle.CT_LineEdit, opt, qt.QSize(width, self.height())
+ )
+ return s.width()
+
+ def sizeHint(self) -> qt.QSize:
+ sizeHint = qt.QLineEdit.sizeHint(self)
+ if not self.__widgetResizable:
+ return sizeHint
+ width = self.__minimumWidthFromContent()
+ return qt.QSize(width, sizeHint.height())
+
+ def __forceMinimumWidthFromContent(self):
+ width = self.__minimumWidthFromContent()
+ qt.QLineEdit.setMinimumWidth(self, width)
+ self.updateGeometry()
+
+ def setMinimumWidth(self, width: int):
+ self.__minimumWidth = width
+ qt.QLineEdit.setMinimumWidth(self, width)
+ self.updateGeometry()
+
+ def minimumWidth(self) -> int:
+ """Returns the user defined minimum width."""
+ return self.__minimumWidth
diff --git a/src/silx/gui/widgets/FlowLayout.py b/src/silx/gui/widgets/FlowLayout.py
index 3c4c9dd..691cb06 100644
--- a/src/silx/gui/widgets/FlowLayout.py
+++ b/src/silx/gui/widgets/FlowLayout.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -25,8 +24,6 @@
"""This module provides a flow layout for QWidget: :class:`FlowLayout`.
"""
-from __future__ import division
-
__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "20/07/2018"
@@ -108,13 +105,13 @@ class FlowLayout(qt.QLayout):
spaceX = widget.style().layoutSpacing(
qt.QSizePolicy.PushButton,
qt.QSizePolicy.PushButton,
- qt.Qt.Horizontal)
+ qt.Qt.Horizontal,
+ )
spaceY = self.verticalSpacing()
if spaceY == -1:
spaceY = widget.style().layoutSpacing(
- qt.QSizePolicy.PushButton,
- qt.QSizePolicy.PushButton,
- qt.Qt.Vertical)
+ qt.QSizePolicy.PushButton, qt.QSizePolicy.PushButton, qt.Qt.Vertical
+ )
nextX = x + item.sizeHint().width() + spaceX
if (nextX - spaceX) > effectiveRect.right() and lineHeight > 0:
diff --git a/src/silx/gui/widgets/FormGridLayout.py b/src/silx/gui/widgets/FormGridLayout.py
new file mode 100644
index 0000000..a1a26b2
--- /dev/null
+++ b/src/silx/gui/widgets/FormGridLayout.py
@@ -0,0 +1,79 @@
+# /*##########################################################################
+#
+# Copyright (c) 2022 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 form layout for QWidget: :class:`FormGridLayout`.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "29/09/2022"
+
+
+import typing
+from .. import qt
+
+
+class FormGridLayout(qt.QGridLayout):
+ """A layout with the API of :class:`qt.QFormLayout` based on a :class:`qt.QGridLayout`.
+
+ This allow a bit more flexibility, like allow vertical expanding
+ of the rows.
+ """
+
+ def __init__(self, parent):
+ super(FormGridLayout, self).__init__(parent)
+ self.__cursor = 0
+
+ def _addCell(self, something, row, column, rowSpan=1, columnSpan=1):
+ if isinstance(something, qt.QLayout):
+ self.addLayout(something, row, column, rowSpan, columnSpan)
+ else:
+ if isinstance(something, str):
+ something = qt.QLabel(something)
+ self.addWidget(something, row, column, rowSpan, columnSpan)
+
+ def addRow(
+ self,
+ label: typing.Union[str, qt.QWidget, qt.QLayout],
+ field: typing.Union[None, qt.QWidget, qt.QLayout] = None,
+ ):
+ """
+ Adds a new row to the bottom of this form layout.
+
+ If field is defined, the given label and field are added.
+
+ Else, the label is a widget and spans both columns.
+ """
+ if field is None:
+ self._addCell(label, self.__cursor, 0, 1, 2)
+ else:
+ self._addCell(label, self.__cursor, 0)
+ self._addCell(field, self.__cursor, 1)
+ self.__cursor += 1
+
+ def addItem(self, item: qt.QLayoutItem):
+ """
+ Adds a new layout item to the bottom of this form layout.
+ """
+ super(FormGridLayout, self).addItem(item)
+ self.__cursor += 1
diff --git a/src/silx/gui/widgets/FrameBrowser.py b/src/silx/gui/widgets/FrameBrowser.py
index 671991f..c03b2a8 100644
--- a/src/silx/gui/widgets/FrameBrowser.py
+++ b/src/silx/gui/widgets/FrameBrowser.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,7 +32,6 @@
"""
from silx.gui import qt
from silx.gui import icons
-from silx.utils import deprecation
__authors__ = ["V.A. Sole", "P. Knobel"]
__license__ = "MIT"
@@ -95,7 +93,9 @@ class FrameBrowser(qt.QWidget):
else:
first, last = 0, n
- self._lineEdit.setFixedWidth(self._lineEdit.fontMetrics().boundingRect('%05d' % last).width())
+ self._lineEdit.setFixedWidth(
+ self._lineEdit.fontMetrics().boundingRect("%05d" % last).width()
+ )
validator = qt.QIntValidator(first, last, self._lineEdit)
self._lineEdit.setValidator(validator)
self._lineEdit.setText("%d" % first)
@@ -153,7 +153,7 @@ class FrameBrowser(qt.QWidget):
"event": "indexChanged",
"old": self._index,
"new": new_value,
- "id": id(self)
+ "id": id(self),
}
self._index = new_value
self.sigIndexChanged.emit(ddict)
@@ -183,11 +183,6 @@ class FrameBrowser(qt.QWidget):
# Update limits
self._label.setText(" limits: %d, %d " % (bottom, top))
- @deprecation.deprecated(replacement="FrameBrowser.setRange",
- since_version="0.8")
- def setLimits(self, first, last):
- return self.setRange(first, last)
-
def setNFrames(self, nframes):
"""Set minimum=0 and maximum=nframes-1 frame numbers.
@@ -200,11 +195,6 @@ class FrameBrowser(qt.QWidget):
# display 1-based index in label
self._label.setText(" of %d " % top)
- @deprecation.deprecated(replacement="FrameBrowser.getValue",
- since_version="0.8")
- def getCurrentIndex(self):
- return self._index
-
def getValue(self):
"""Return current frame index"""
return self._index
@@ -244,6 +234,7 @@ class HorizontalSliderWithBrowser(qt.QAbstractSlider):
:param QWidget parent: Optional parent widget
"""
+
def __init__(self, parent=None):
qt.QAbstractSlider.__init__(self, parent)
self.setOrientation(qt.Qt.Horizontal)
@@ -303,14 +294,13 @@ class HorizontalSliderWithBrowser(qt.QAbstractSlider):
self._browser.setRange(first, last)
def _sliderSlot(self, value):
- """Emit selected value when slider is activated
- """
+ """Emit selected value when slider is activated"""
self._browser.setValue(value)
self.valueChanged.emit(value)
def _browserSlot(self, ddict):
"""Emit selected value when browser state is changed"""
- self._slider.setValue(ddict['new'])
+ self._slider.setValue(ddict["new"])
def setValue(self, value):
"""Set value
diff --git a/src/silx/gui/widgets/HierarchicalTableView.py b/src/silx/gui/widgets/HierarchicalTableView.py
index 3ccf4c7..6e6329b 100644
--- a/src/silx/gui/widgets/HierarchicalTableView.py
+++ b/src/silx/gui/widgets/HierarchicalTableView.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/widgets/LegendIconWidget.py b/src/silx/gui/widgets/LegendIconWidget.py
index 1c95e41..ae86c35 100755
--- a/src/silx/gui/widgets/LegendIconWidget.py
+++ b/src/silx/gui/widgets/LegendIconWidget.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -45,27 +44,27 @@ _logger = logging.getLogger(__name__)
# Courtesy of the pyqtgraph project
_Symbols = None
-""""Cache supported symbols as Qt paths"""
+"""Cache supported symbols as Qt paths"""
-_NoSymbols = (None, 'None', 'none', '', ' ')
+_NoSymbols = (None, "None", "none", "", " ")
"""List of values resulting in no symbol being displayed for a curve"""
_LineStyles = {
None: qt.Qt.NoPen,
- 'None': qt.Qt.NoPen,
- 'none': qt.Qt.NoPen,
- '': qt.Qt.NoPen,
- ' ': qt.Qt.NoPen,
- '-': qt.Qt.SolidLine,
- '--': qt.Qt.DashLine,
- ':': qt.Qt.DotLine,
- '-.': qt.Qt.DashDotLine
+ "None": qt.Qt.NoPen,
+ "none": qt.Qt.NoPen,
+ "": qt.Qt.NoPen,
+ " ": qt.Qt.NoPen,
+ "-": qt.Qt.SolidLine,
+ "--": qt.Qt.DashLine,
+ ":": qt.Qt.DotLine,
+ "-.": qt.Qt.DashDotLine,
}
"""Conversion from matplotlib-like linestyle to Qt"""
-_NoLineStyle = (None, 'None', 'none', '', ' ')
+_NoLineStyle = (None, "None", "none", "", " ")
"""List of style values resulting in no line being displayed for a curve"""
@@ -83,22 +82,45 @@ def _initSymbols():
if _Symbols is not None:
return
- symbols = dict([(name, qt.QPainterPath())
- for name in ['o', 's', 't', 'd', '+', 'x', '.', ',']])
- symbols['o'].addEllipse(qt.QRectF(.1, .1, .8, .8))
- symbols['.'].addEllipse(qt.QRectF(.3, .3, .4, .4))
- symbols[','].addEllipse(qt.QRectF(.4, .4, .2, .2))
- symbols['s'].addRect(qt.QRectF(.1, .1, .8, .8))
+ symbols = dict(
+ [(name, qt.QPainterPath()) for name in ["o", "s", "t", "d", "+", "x", ".", ","]]
+ )
+ symbols["o"].addEllipse(qt.QRectF(0.1, 0.1, 0.8, 0.8))
+ symbols["."].addEllipse(qt.QRectF(0.3, 0.3, 0.4, 0.4))
+ symbols[","].addEllipse(qt.QRectF(0.4, 0.4, 0.2, 0.2))
+ symbols["s"].addRect(qt.QRectF(0.1, 0.1, 0.8, 0.8))
coords = {
- 't': [(0.5, 0.), (.1, .8), (.9, .8)],
- 'd': [(0.1, 0.5), (0.5, 0.), (0.9, 0.5), (0.5, 1.)],
- '+': [(0.0, 0.40), (0.40, 0.40), (0.40, 0.), (0.60, 0.),
- (0.60, 0.40), (1., 0.40), (1., 0.60), (0.60, 0.60),
- (0.60, 1.), (0.40, 1.), (0.40, 0.60), (0., 0.60)],
- 'x': [(0.0, 0.40), (0.40, 0.40), (0.40, 0.), (0.60, 0.),
- (0.60, 0.40), (1., 0.40), (1., 0.60), (0.60, 0.60),
- (0.60, 1.), (0.40, 1.), (0.40, 0.60), (0., 0.60)]
+ "t": [(0.5, 0.0), (0.1, 0.8), (0.9, 0.8)],
+ "d": [(0.1, 0.5), (0.5, 0.0), (0.9, 0.5), (0.5, 1.0)],
+ "+": [
+ (0.0, 0.40),
+ (0.40, 0.40),
+ (0.40, 0.0),
+ (0.60, 0.0),
+ (0.60, 0.40),
+ (1.0, 0.40),
+ (1.0, 0.60),
+ (0.60, 0.60),
+ (0.60, 1.0),
+ (0.40, 1.0),
+ (0.40, 0.60),
+ (0.0, 0.60),
+ ],
+ "x": [
+ (0.0, 0.40),
+ (0.40, 0.40),
+ (0.40, 0.0),
+ (0.60, 0.0),
+ (0.60, 0.40),
+ (1.0, 0.40),
+ (1.0, 0.60),
+ (0.60, 0.60),
+ (0.60, 1.0),
+ (0.40, 1.0),
+ (0.40, 0.60),
+ (0.0, 0.60),
+ ],
}
for s, c in coords.items():
symbols[s].moveTo(*c[0])
@@ -107,9 +129,9 @@ def _initSymbols():
symbols[s].closeSubpath()
tr = qt.QTransform()
tr.rotate(45)
- symbols['x'].translate(qt.QPointF(-0.5, -0.5))
- symbols['x'] = tr.map(symbols['x'])
- symbols['x'].translate(qt.QPointF(0.5, 0.5))
+ symbols["x"].translate(qt.QPointF(-0.5, -0.5))
+ symbols["x"] = tr.map(symbols["x"])
+ symbols["x"].translate(qt.QPointF(0.5, 0.5))
_Symbols = symbols
@@ -131,10 +153,11 @@ class LegendIconWidget(qt.QWidget):
# Line attributes
self.lineStyle = qt.Qt.NoPen
- self.lineWidth = 1.
+ self.__dashPattern = []
+ self.lineWidth = 1.0
self.lineColor = qt.Qt.green
- self.symbol = ''
+ self.symbol = ""
# Symbol attributes
self.symbolStyle = qt.Qt.SolidPattern
self.symbolColor = qt.Qt.green
@@ -148,8 +171,7 @@ class LegendIconWidget(qt.QWidget):
# Control widget size: sizeHint "is the only acceptable
# alternative, so the widget can never grow or shrink"
# (c.f. Qt Doc, enum QSizePolicy::Policy)
- self.setSizePolicy(qt.QSizePolicy.Fixed,
- qt.QSizePolicy.Fixed)
+ self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
def sizeHint(self):
return qt.QSize(50, 15)
@@ -191,12 +213,21 @@ class LegendIconWidget(qt.QWidget):
- '--': dashed
- ':': dotted
- '-.': dash and dot
+ - (offset, (dash pattern))
- :param str style: The linestyle to use
+ :param style: The linestyle to use
"""
+ print("setLineStyle", style)
if style not in _LineStyles:
- raise ValueError('Unknown style: %s', style)
- self.lineStyle = _LineStyles[style]
+ self.lineStyle = qt.Qt.SolidLine
+ dashPattern = style[1]
+ if dashPattern is None or dashPattern == ():
+ self.__dashPattern = None
+ else:
+ self.__dashPattern = style[1]
+ else:
+ self.lineStyle = _LineStyles[style]
+ self.__dashPattern = None
self.update()
def _toLut(self, colormap):
@@ -309,7 +340,7 @@ class LegendIconWidget(qt.QWidget):
# current -> width = 2.5, height = 1.0
scale = float(self.height())
ratio = float(self.width()) / scale
- symbolOffset = qt.QPointF(.5 * (ratio - 1.), 0.)
+ symbolOffset = qt.QPointF(0.5 * (ratio - 1.0), 0.0)
# Determine and scale offset
offset = qt.QPointF(float(rect.left()) / scale, float(rect.top()) / scale)
@@ -317,8 +348,7 @@ class LegendIconWidget(qt.QWidget):
if self.isEnabled():
overrideColor = None
else:
- overrideColor = palette.color(qt.QPalette.Disabled,
- qt.QPalette.WindowText)
+ overrideColor = palette.color(qt.QPalette.Disabled, qt.QPalette.WindowText)
# Draw BG rectangle (for debugging)
# bottomRight = qt.QPointF(
@@ -350,21 +380,23 @@ class LegendIconWidget(qt.QWidget):
llist = []
if self.showLine:
linePath = qt.QPainterPath()
- linePath.moveTo(0., 0.5)
+ linePath.moveTo(0.0, 0.5)
linePath.lineTo(ratio, 0.5)
# linePath.lineTo(2.5, 0.5)
lineBrush = qt.QBrush(
- self.lineColor if overrideColor is None else overrideColor)
+ self.lineColor if overrideColor is None else overrideColor
+ )
linePen = qt.QPen(
lineBrush,
(self.lineWidth / self.height()),
self.lineStyle,
- qt.Qt.FlatCap
+ qt.Qt.FlatCap,
)
+ if self.__dashPattern is not None:
+ linePen.setDashPattern(self.__dashPattern)
llist.append((linePath, linePen, lineBrush))
- isValidSymbol = (len(self.symbol) and
- self.symbol not in _NoSymbols)
+ isValidSymbol = len(self.symbol) and self.symbol not in _NoSymbols
if self.showSymbol and isValidSymbol:
if self.symbolColormap is None:
# PITFALL ahead: Let this be a warning to others
@@ -374,15 +406,14 @@ class LegendIconWidget(qt.QWidget):
symbolPath.translate(symbolOffset)
symbolBrush = qt.QBrush(
self.symbolColor if overrideColor is None else overrideColor,
- self.symbolStyle)
+ self.symbolStyle,
+ )
symbolPen = qt.QPen(
self.symbolOutlineBrush, # Brush
- 1. / self.height(), # Width
- qt.Qt.SolidLine # Style
+ 1.0 / self.height(), # Width
+ qt.Qt.SolidLine, # Style
)
- llist.append((symbolPath,
- symbolPen,
- symbolBrush))
+ llist.append((symbolPath, symbolPen, symbolBrush))
else:
nbSymbols = int(ratio + 2)
for i in range(nbSymbols):
@@ -391,21 +422,21 @@ class LegendIconWidget(qt.QWidget):
else:
image = self.getGrayedColormapImage(self.symbolColormap)
pos = int((_COLORMAP_PIXMAP_SIZE / nbSymbols) * i)
- pos = numpy.clip(pos, 0, _COLORMAP_PIXMAP_SIZE-1)
+ pos = numpy.clip(pos, 0, _COLORMAP_PIXMAP_SIZE - 1)
color = image.pixelColor(pos, 0)
- delta = qt.QPointF(ratio * ((i - (nbSymbols-1)/2) / nbSymbols), 0)
+ delta = qt.QPointF(
+ ratio * ((i - (nbSymbols - 1) / 2) / nbSymbols), 0
+ )
symbolPath = qt.QPainterPath(_Symbols[self.symbol])
symbolPath.translate(symbolOffset + delta)
symbolBrush = qt.QBrush(color, self.symbolStyle)
symbolPen = qt.QPen(
self.symbolOutlineBrush, # Brush
- 1. / self.height(), # Width
- qt.Qt.SolidLine # Style
+ 1.0 / self.height(), # Width
+ qt.Qt.SolidLine, # Style
)
- llist.append((symbolPath,
- symbolPen,
- symbolBrush))
+ llist.append((symbolPath, symbolPen, symbolBrush))
# Draw
for path, pen, brush in llist:
diff --git a/src/silx/gui/widgets/MedianFilterDialog.py b/src/silx/gui/widgets/MedianFilterDialog.py
index dd4a00d..5fe134f 100644
--- a/src/silx/gui/widgets/MedianFilterDialog.py
+++ b/src/silx/gui/widgets/MedianFilterDialog.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
@@ -43,8 +42,10 @@ from silx.gui import qt
_logger = logging.getLogger(__name__)
+
class MedianFilterDialog(qt.QDialog):
"""QDialog window featuring a :class:`BackgroundWidget`"""
+
sigFilterOptChanged = qt.Signal(int, bool)
def __init__(self, parent=None):
@@ -55,11 +56,11 @@ class MedianFilterDialog(qt.QDialog):
self.setLayout(self.mainLayout)
# filter width GUI
- self.mainLayout.addWidget(qt.QLabel('filter width:', parent = self))
+ self.mainLayout.addWidget(qt.QLabel("filter width:", parent=self))
self._filterWidth = qt.QSpinBox(parent=self)
self._filterWidth.setMinimum(1)
self._filterWidth.setValue(1)
- self._filterWidth.setSingleStep(2);
+ self._filterWidth.setSingleStep(2)
widthTooltip = """radius width of the pixel including in the filter
for each pixel"""
self._filterWidth.setToolTip(widthTooltip)
@@ -67,14 +68,16 @@ class MedianFilterDialog(qt.QDialog):
self.mainLayout.addWidget(self._filterWidth)
# filter option GUI
- self._filterOption = qt.QCheckBox('conditional', parent=self)
+ self._filterOption = qt.QCheckBox("conditional", parent=self)
conditionalTooltip = """if check, implement a conditional filter"""
self._filterOption.stateChanged.connect(self._filterOptionChanged)
self.mainLayout.addWidget(self._filterOption)
def _filterOptionChanged(self):
"""Call back used when the filter values are changed"""
- if self._filterWidth.value()%2 == 0:
- _logger.warning('median filter only accept odd values')
+ if self._filterWidth.value() % 2 == 0:
+ _logger.warning("median filter only accept odd values")
else:
- self.sigFilterOptChanged.emit(self._filterWidth.value(), self._filterOption.isChecked()) \ No newline at end of file
+ self.sigFilterOptChanged.emit(
+ self._filterWidth.value(), self._filterOption.isChecked()
+ )
diff --git a/src/silx/gui/widgets/MultiModeAction.py b/src/silx/gui/widgets/MultiModeAction.py
index 502275d..b40d285 100644
--- a/src/silx/gui/widgets/MultiModeAction.py
+++ b/src/silx/gui/widgets/MultiModeAction.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/widgets/PeriodicTable.py b/src/silx/gui/widgets/PeriodicTable.py
index 6fed109..2923cc6 100644
--- a/src/silx/gui/widgets/PeriodicTable.py
+++ b/src/silx/gui/widgets/PeriodicTable.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2023 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
@@ -137,122 +136,123 @@ __authors__ = ["E. Papillon", "V.A. Sole", "P. Knobel"]
__license__ = "MIT"
__date__ = "26/01/2017"
-from collections import OrderedDict
import logging
from silx.gui import qt
_logger = logging.getLogger(__name__)
# Symbol Atomic Number col row name mass subcategory
-_elements = [("H", 1, 1, 1, "hydrogen", 1.00800, "diatomic nonmetal"),
- ("He", 2, 18, 1, "helium", 4.0030, "noble gas"),
- ("Li", 3, 1, 2, "lithium", 6.94000, "alkali metal"),
- ("Be", 4, 2, 2, "beryllium", 9.01200, "alkaline earth metal"),
- ("B", 5, 13, 2, "boron", 10.8110, "metalloid"),
- ("C", 6, 14, 2, "carbon", 12.0100, "polyatomic nonmetal"),
- ("N", 7, 15, 2, "nitrogen", 14.0080, "diatomic nonmetal"),
- ("O", 8, 16, 2, "oxygen", 16.0000, "diatomic nonmetal"),
- ("F", 9, 17, 2, "fluorine", 19.0000, "diatomic nonmetal"),
- ("Ne", 10, 18, 2, "neon", 20.1830, "noble gas"),
- ("Na", 11, 1, 3, "sodium", 22.9970, "alkali metal"),
- ("Mg", 12, 2, 3, "magnesium", 24.3200, "alkaline earth metal"),
- ("Al", 13, 13, 3, "aluminium", 26.9700, "post transition metal"),
- ("Si", 14, 14, 3, "silicon", 28.0860, "metalloid"),
- ("P", 15, 15, 3, "phosphorus", 30.9750, "polyatomic nonmetal"),
- ("S", 16, 16, 3, "sulphur", 32.0660, "polyatomic nonmetal"),
- ("Cl", 17, 17, 3, "chlorine", 35.4570, "diatomic nonmetal"),
- ("Ar", 18, 18, 3, "argon", 39.9440, "noble gas"),
- ("K", 19, 1, 4, "potassium", 39.1020, "alkali metal"),
- ("Ca", 20, 2, 4, "calcium", 40.0800, "alkaline earth metal"),
- ("Sc", 21, 3, 4, "scandium", 44.9600, "transition metal"),
- ("Ti", 22, 4, 4, "titanium", 47.9000, "transition metal"),
- ("V", 23, 5, 4, "vanadium", 50.9420, "transition metal"),
- ("Cr", 24, 6, 4, "chromium", 51.9960, "transition metal"),
- ("Mn", 25, 7, 4, "manganese", 54.9400, "transition metal"),
- ("Fe", 26, 8, 4, "iron", 55.8500, "transition metal"),
- ("Co", 27, 9, 4, "cobalt", 58.9330, "transition metal"),
- ("Ni", 28, 10, 4, "nickel", 58.6900, "transition metal"),
- ("Cu", 29, 11, 4, "copper", 63.5400, "transition metal"),
- ("Zn", 30, 12, 4, "zinc", 65.3800, "transition metal"),
- ("Ga", 31, 13, 4, "gallium", 69.7200, "post transition metal"),
- ("Ge", 32, 14, 4, "germanium", 72.5900, "metalloid"),
- ("As", 33, 15, 4, "arsenic", 74.9200, "metalloid"),
- ("Se", 34, 16, 4, "selenium", 78.9600, "polyatomic nonmetal"),
- ("Br", 35, 17, 4, "bromine", 79.9200, "diatomic nonmetal"),
- ("Kr", 36, 18, 4, "krypton", 83.8000, "noble gas"),
- ("Rb", 37, 1, 5, "rubidium", 85.4800, "alkali metal"),
- ("Sr", 38, 2, 5, "strontium", 87.6200, "alkaline earth metal"),
- ("Y", 39, 3, 5, "yttrium", 88.9050, "transition metal"),
- ("Zr", 40, 4, 5, "zirconium", 91.2200, "transition metal"),
- ("Nb", 41, 5, 5, "niobium", 92.9060, "transition metal"),
- ("Mo", 42, 6, 5, "molybdenum", 95.9500, "transition metal"),
- ("Tc", 43, 7, 5, "technetium", 99.0000, "transition metal"),
- ("Ru", 44, 8, 5, "ruthenium", 101.0700, "transition metal"),
- ("Rh", 45, 9, 5, "rhodium", 102.9100, "transition metal"),
- ("Pd", 46, 10, 5, "palladium", 106.400, "transition metal"),
- ("Ag", 47, 11, 5, "silver", 107.880, "transition metal"),
- ("Cd", 48, 12, 5, "cadmium", 112.410, "transition metal"),
- ("In", 49, 13, 5, "indium", 114.820, "post transition metal"),
- ("Sn", 50, 14, 5, "tin", 118.690, "post transition metal"),
- ("Sb", 51, 15, 5, "antimony", 121.760, "metalloid"),
- ("Te", 52, 16, 5, "tellurium", 127.600, "metalloid"),
- ("I", 53, 17, 5, "iodine", 126.910, "diatomic nonmetal"),
- ("Xe", 54, 18, 5, "xenon", 131.300, "noble gas"),
- ("Cs", 55, 1, 6, "caesium", 132.910, "alkali metal"),
- ("Ba", 56, 2, 6, "barium", 137.360, "alkaline earth metal"),
- ("La", 57, 3, 6, "lanthanum", 138.920, "lanthanide"),
- ("Ce", 58, 4, 9, "cerium", 140.130, "lanthanide"),
- ("Pr", 59, 5, 9, "praseodymium", 140.920, "lanthanide"),
- ("Nd", 60, 6, 9, "neodymium", 144.270, "lanthanide"),
- ("Pm", 61, 7, 9, "promethium", 147.000, "lanthanide"),
- ("Sm", 62, 8, 9, "samarium", 150.350, "lanthanide"),
- ("Eu", 63, 9, 9, "europium", 152.000, "lanthanide"),
- ("Gd", 64, 10, 9, "gadolinium", 157.260, "lanthanide"),
- ("Tb", 65, 11, 9, "terbium", 158.930, "lanthanide"),
- ("Dy", 66, 12, 9, "dysprosium", 162.510, "lanthanide"),
- ("Ho", 67, 13, 9, "holmium", 164.940, "lanthanide"),
- ("Er", 68, 14, 9, "erbium", 167.270, "lanthanide"),
- ("Tm", 69, 15, 9, "thulium", 168.940, "lanthanide"),
- ("Yb", 70, 16, 9, "ytterbium", 173.040, "lanthanide"),
- ("Lu", 71, 17, 9, "lutetium", 174.990, "lanthanide"),
- ("Hf", 72, 4, 6, "hafnium", 178.500, "transition metal"),
- ("Ta", 73, 5, 6, "tantalum", 180.950, "transition metal"),
- ("W", 74, 6, 6, "tungsten", 183.920, "transition metal"),
- ("Re", 75, 7, 6, "rhenium", 186.200, "transition metal"),
- ("Os", 76, 8, 6, "osmium", 190.200, "transition metal"),
- ("Ir", 77, 9, 6, "iridium", 192.200, "transition metal"),
- ("Pt", 78, 10, 6, "platinum", 195.090, "transition metal"),
- ("Au", 79, 11, 6, "gold", 197.200, "transition metal"),
- ("Hg", 80, 12, 6, "mercury", 200.610, "transition metal"),
- ("Tl", 81, 13, 6, "thallium", 204.390, "post transition metal"),
- ("Pb", 82, 14, 6, "lead", 207.210, "post transition metal"),
- ("Bi", 83, 15, 6, "bismuth", 209.000, "post transition metal"),
- ("Po", 84, 16, 6, "polonium", 209.000, "post transition metal"),
- ("At", 85, 17, 6, "astatine", 210.000, "metalloid"),
- ("Rn", 86, 18, 6, "radon", 222.000, "noble gas"),
- ("Fr", 87, 1, 7, "francium", 223.000, "alkali metal"),
- ("Ra", 88, 2, 7, "radium", 226.000, "alkaline earth metal"),
- ("Ac", 89, 3, 7, "actinium", 227.000, "actinide"),
- ("Th", 90, 4, 10, "thorium", 232.000, "actinide"),
- ("Pa", 91, 5, 10, "proactinium", 231.03588, "actinide"),
- ("U", 92, 6, 10, "uranium", 238.070, "actinide"),
- ("Np", 93, 7, 10, "neptunium", 237.000, "actinide"),
- ("Pu", 94, 8, 10, "plutonium", 239.100, "actinide"),
- ("Am", 95, 9, 10, "americium", 243, "actinide"),
- ("Cm", 96, 10, 10, "curium", 247, "actinide"),
- ("Bk", 97, 11, 10, "berkelium", 247, "actinide"),
- ("Cf", 98, 12, 10, "californium", 251, "actinide"),
- ("Es", 99, 13, 10, "einsteinium", 252, "actinide"),
- ("Fm", 100, 14, 10, "fermium", 257, "actinide"),
- ("Md", 101, 15, 10, "mendelevium", 258, "actinide"),
- ("No", 102, 16, 10, "nobelium", 259, "actinide"),
- ("Lr", 103, 17, 10, "lawrencium", 262, "actinide"),
- ("Rf", 104, 4, 7, "rutherfordium", 261, "transition metal"),
- ("Db", 105, 5, 7, "dubnium", 262, "transition metal"),
- ("Sg", 106, 6, 7, "seaborgium", 266, "transition metal"),
- ("Bh", 107, 7, 7, "bohrium", 264, "transition metal"),
- ("Hs", 108, 8, 7, "hassium", 269, "transition metal"),
- ("Mt", 109, 9, 7, "meitnerium", 268)]
+_elements = [
+ ("H", 1, 1, 1, "hydrogen", 1.00800, "diatomic nonmetal"),
+ ("He", 2, 18, 1, "helium", 4.0030, "noble gas"),
+ ("Li", 3, 1, 2, "lithium", 6.94000, "alkali metal"),
+ ("Be", 4, 2, 2, "beryllium", 9.01200, "alkaline earth metal"),
+ ("B", 5, 13, 2, "boron", 10.8110, "metalloid"),
+ ("C", 6, 14, 2, "carbon", 12.0100, "polyatomic nonmetal"),
+ ("N", 7, 15, 2, "nitrogen", 14.0080, "diatomic nonmetal"),
+ ("O", 8, 16, 2, "oxygen", 16.0000, "diatomic nonmetal"),
+ ("F", 9, 17, 2, "fluorine", 19.0000, "diatomic nonmetal"),
+ ("Ne", 10, 18, 2, "neon", 20.1830, "noble gas"),
+ ("Na", 11, 1, 3, "sodium", 22.9970, "alkali metal"),
+ ("Mg", 12, 2, 3, "magnesium", 24.3200, "alkaline earth metal"),
+ ("Al", 13, 13, 3, "aluminium", 26.9700, "post transition metal"),
+ ("Si", 14, 14, 3, "silicon", 28.0860, "metalloid"),
+ ("P", 15, 15, 3, "phosphorus", 30.9750, "polyatomic nonmetal"),
+ ("S", 16, 16, 3, "sulphur", 32.0660, "polyatomic nonmetal"),
+ ("Cl", 17, 17, 3, "chlorine", 35.4570, "diatomic nonmetal"),
+ ("Ar", 18, 18, 3, "argon", 39.9440, "noble gas"),
+ ("K", 19, 1, 4, "potassium", 39.1020, "alkali metal"),
+ ("Ca", 20, 2, 4, "calcium", 40.0800, "alkaline earth metal"),
+ ("Sc", 21, 3, 4, "scandium", 44.9600, "transition metal"),
+ ("Ti", 22, 4, 4, "titanium", 47.9000, "transition metal"),
+ ("V", 23, 5, 4, "vanadium", 50.9420, "transition metal"),
+ ("Cr", 24, 6, 4, "chromium", 51.9960, "transition metal"),
+ ("Mn", 25, 7, 4, "manganese", 54.9400, "transition metal"),
+ ("Fe", 26, 8, 4, "iron", 55.8500, "transition metal"),
+ ("Co", 27, 9, 4, "cobalt", 58.9330, "transition metal"),
+ ("Ni", 28, 10, 4, "nickel", 58.6900, "transition metal"),
+ ("Cu", 29, 11, 4, "copper", 63.5400, "transition metal"),
+ ("Zn", 30, 12, 4, "zinc", 65.3800, "transition metal"),
+ ("Ga", 31, 13, 4, "gallium", 69.7200, "post transition metal"),
+ ("Ge", 32, 14, 4, "germanium", 72.5900, "metalloid"),
+ ("As", 33, 15, 4, "arsenic", 74.9200, "metalloid"),
+ ("Se", 34, 16, 4, "selenium", 78.9600, "polyatomic nonmetal"),
+ ("Br", 35, 17, 4, "bromine", 79.9200, "diatomic nonmetal"),
+ ("Kr", 36, 18, 4, "krypton", 83.8000, "noble gas"),
+ ("Rb", 37, 1, 5, "rubidium", 85.4800, "alkali metal"),
+ ("Sr", 38, 2, 5, "strontium", 87.6200, "alkaline earth metal"),
+ ("Y", 39, 3, 5, "yttrium", 88.9050, "transition metal"),
+ ("Zr", 40, 4, 5, "zirconium", 91.2200, "transition metal"),
+ ("Nb", 41, 5, 5, "niobium", 92.9060, "transition metal"),
+ ("Mo", 42, 6, 5, "molybdenum", 95.9500, "transition metal"),
+ ("Tc", 43, 7, 5, "technetium", 99.0000, "transition metal"),
+ ("Ru", 44, 8, 5, "ruthenium", 101.0700, "transition metal"),
+ ("Rh", 45, 9, 5, "rhodium", 102.9100, "transition metal"),
+ ("Pd", 46, 10, 5, "palladium", 106.400, "transition metal"),
+ ("Ag", 47, 11, 5, "silver", 107.880, "transition metal"),
+ ("Cd", 48, 12, 5, "cadmium", 112.410, "transition metal"),
+ ("In", 49, 13, 5, "indium", 114.820, "post transition metal"),
+ ("Sn", 50, 14, 5, "tin", 118.690, "post transition metal"),
+ ("Sb", 51, 15, 5, "antimony", 121.760, "metalloid"),
+ ("Te", 52, 16, 5, "tellurium", 127.600, "metalloid"),
+ ("I", 53, 17, 5, "iodine", 126.910, "diatomic nonmetal"),
+ ("Xe", 54, 18, 5, "xenon", 131.300, "noble gas"),
+ ("Cs", 55, 1, 6, "caesium", 132.910, "alkali metal"),
+ ("Ba", 56, 2, 6, "barium", 137.360, "alkaline earth metal"),
+ ("La", 57, 3, 6, "lanthanum", 138.920, "lanthanide"),
+ ("Ce", 58, 4, 9, "cerium", 140.130, "lanthanide"),
+ ("Pr", 59, 5, 9, "praseodymium", 140.920, "lanthanide"),
+ ("Nd", 60, 6, 9, "neodymium", 144.270, "lanthanide"),
+ ("Pm", 61, 7, 9, "promethium", 147.000, "lanthanide"),
+ ("Sm", 62, 8, 9, "samarium", 150.350, "lanthanide"),
+ ("Eu", 63, 9, 9, "europium", 152.000, "lanthanide"),
+ ("Gd", 64, 10, 9, "gadolinium", 157.260, "lanthanide"),
+ ("Tb", 65, 11, 9, "terbium", 158.930, "lanthanide"),
+ ("Dy", 66, 12, 9, "dysprosium", 162.510, "lanthanide"),
+ ("Ho", 67, 13, 9, "holmium", 164.940, "lanthanide"),
+ ("Er", 68, 14, 9, "erbium", 167.270, "lanthanide"),
+ ("Tm", 69, 15, 9, "thulium", 168.940, "lanthanide"),
+ ("Yb", 70, 16, 9, "ytterbium", 173.040, "lanthanide"),
+ ("Lu", 71, 17, 9, "lutetium", 174.990, "lanthanide"),
+ ("Hf", 72, 4, 6, "hafnium", 178.500, "transition metal"),
+ ("Ta", 73, 5, 6, "tantalum", 180.950, "transition metal"),
+ ("W", 74, 6, 6, "tungsten", 183.920, "transition metal"),
+ ("Re", 75, 7, 6, "rhenium", 186.200, "transition metal"),
+ ("Os", 76, 8, 6, "osmium", 190.200, "transition metal"),
+ ("Ir", 77, 9, 6, "iridium", 192.200, "transition metal"),
+ ("Pt", 78, 10, 6, "platinum", 195.090, "transition metal"),
+ ("Au", 79, 11, 6, "gold", 197.200, "transition metal"),
+ ("Hg", 80, 12, 6, "mercury", 200.610, "transition metal"),
+ ("Tl", 81, 13, 6, "thallium", 204.390, "post transition metal"),
+ ("Pb", 82, 14, 6, "lead", 207.210, "post transition metal"),
+ ("Bi", 83, 15, 6, "bismuth", 209.000, "post transition metal"),
+ ("Po", 84, 16, 6, "polonium", 209.000, "post transition metal"),
+ ("At", 85, 17, 6, "astatine", 210.000, "metalloid"),
+ ("Rn", 86, 18, 6, "radon", 222.000, "noble gas"),
+ ("Fr", 87, 1, 7, "francium", 223.000, "alkali metal"),
+ ("Ra", 88, 2, 7, "radium", 226.000, "alkaline earth metal"),
+ ("Ac", 89, 3, 7, "actinium", 227.000, "actinide"),
+ ("Th", 90, 4, 10, "thorium", 232.000, "actinide"),
+ ("Pa", 91, 5, 10, "proactinium", 231.03588, "actinide"),
+ ("U", 92, 6, 10, "uranium", 238.070, "actinide"),
+ ("Np", 93, 7, 10, "neptunium", 237.000, "actinide"),
+ ("Pu", 94, 8, 10, "plutonium", 239.100, "actinide"),
+ ("Am", 95, 9, 10, "americium", 243, "actinide"),
+ ("Cm", 96, 10, 10, "curium", 247, "actinide"),
+ ("Bk", 97, 11, 10, "berkelium", 247, "actinide"),
+ ("Cf", 98, 12, 10, "californium", 251, "actinide"),
+ ("Es", 99, 13, 10, "einsteinium", 252, "actinide"),
+ ("Fm", 100, 14, 10, "fermium", 257, "actinide"),
+ ("Md", 101, 15, 10, "mendelevium", 258, "actinide"),
+ ("No", 102, 16, 10, "nobelium", 259, "actinide"),
+ ("Lr", 103, 17, 10, "lawrencium", 262, "actinide"),
+ ("Rf", 104, 4, 7, "rutherfordium", 261, "transition metal"),
+ ("Db", 105, 5, 7, "dubnium", 262, "transition metal"),
+ ("Sg", 106, 6, 7, "seaborgium", 266, "transition metal"),
+ ("Bh", 107, 7, 7, "bohrium", 264, "transition metal"),
+ ("Hs", 108, 8, 7, "hassium", 269, "transition metal"),
+ ("Mt", 109, 9, 7, "meitnerium", 268),
+]
class PeriodicTableItem(object):
@@ -280,8 +280,8 @@ class PeriodicTableItem(object):
:param str subcategory: Subcategory, based on physical properties
(e.g. "alkali metal", "noble gas"...)
"""
- def __init__(self, symbol, Z, col, row, name, mass,
- subcategory=""):
+
+ def __init__(self, symbol, Z, col, row, name, mass, subcategory=""):
self.symbol = symbol
"""Atomic symbol (e.g. H, He, Li...)"""
self.Z = Z
@@ -303,10 +303,7 @@ class PeriodicTableItem(object):
if idx == 6:
_logger.warning("density not implemented in silx, returning 0.")
- ret = [self.symbol, self.Z,
- self.col, self.row,
- self.name, self.mass,
- 0.]
+ ret = [self.symbol, self.Z, self.col, self.row, self.name, self.mass, 0.0]
return ret[idx]
def __len__(self):
@@ -321,6 +318,7 @@ class ColoredPeriodicTableItem(PeriodicTableItem):
:param str bgcolor: Custom background color for element in
periodic table, as a RGB string *#RRGGBB*"""
+
COLORS = {
"diatomic nonmetal": "#7FFF00", # chartreuse
"noble gas": "#00FFFF", # cyan
@@ -332,14 +330,12 @@ class ColoredPeriodicTableItem(PeriodicTableItem):
"post transition metal": "#D3D3D3", # light gray
"lanthanide": "#FFB6C1", # light pink
"actinide": "#F08080", # Light Coral
- "": "#FFFFFF" # white
+ "": "#FFFFFF", # white
}
"""Dictionary defining RGB colors for each subcategory."""
- def __init__(self, symbol, Z, col, row, name, mass,
- subcategory="", bgcolor=None):
- PeriodicTableItem.__init__(self, symbol, Z, col, row, name, mass,
- subcategory)
+ def __init__(self, symbol, Z, col, row, name, mass, subcategory="", bgcolor=None):
+ PeriodicTableItem.__init__(self, symbol, Z, col, row, name, mass, subcategory)
self.bgcolor = self.COLORS.get(subcategory, "#FFFFFF")
"""Background color of element in the periodic table,
@@ -357,8 +353,8 @@ _defaultTableItems = [ColoredPeriodicTableItem(*info) for info in _elements]
class _ElementButton(qt.QPushButton):
- """Atomic element button, used as a cell in the periodic table
- """
+ """Atomic element button, used as a cell in the periodic table"""
+
sigElementEnter = qt.pyqtSignal(object)
"""Signal emitted as the cursor enters the widget"""
sigElementLeave = qt.pyqtSignal(object)
@@ -381,8 +377,9 @@ class _ElementButton(qt.QPushButton):
self.setFlat(1)
self.setCheckable(0)
- self.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Expanding))
+ self.setSizePolicy(
+ qt.QSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ )
self.selected = False
self.current = False
@@ -455,18 +452,19 @@ class _ElementButton(qt.QPushButton):
self.brush = qt.QBrush(self.bgcolor)
else:
self.brush = qt.QBrush()
- palette.setBrush(self.backgroundRole(),
- self.brush)
+ palette.setBrush(self.backgroundRole(), self.brush)
self.setPalette(palette)
self.update()
def paintEvent(self, pEvent):
# get button geometry
widgGeom = self.rect()
- paintGeom = qt.QRect(widgGeom.left() + 1,
- widgGeom.top() + 1,
- widgGeom.width() - 2,
- widgGeom.height() - 2)
+ paintGeom = qt.QRect(
+ widgGeom.left() + 1,
+ widgGeom.top() + 1,
+ widgGeom.width() - 2,
+ widgGeom.height() - 2,
+ )
# paint background color
painter = qt.QPainter(self)
@@ -522,6 +520,7 @@ class PeriodicTable(qt.QWidget):
pt.sigElementClicked.connect(my_slot)
"""
+
sigElementClicked = qt.pyqtSignal(object)
"""When any element is clicked in the table, the widget emits
this signal and sends a :class:`PeriodicTableItem` object.
@@ -552,8 +551,9 @@ class PeriodicTable(qt.QWidget):
selection is only possible with method :meth:`setSelection`.
"""
- def __init__(self, parent=None, name="PeriodicTable", elements=None,
- selectable=False):
+ def __init__(
+ self, parent=None, name="PeriodicTable", elements=None, selectable=False
+ ):
self.selectable = selectable
qt.QWidget.__init__(self, parent)
self.setWindowTitle(name)
@@ -577,7 +577,7 @@ class PeriodicTable(qt.QWidget):
self._eltCurrent = None
"""Current :class:`_ElementButton` (last clicked)"""
- self._eltButtons = OrderedDict()
+ self._eltButtons = {}
"""Dictionary of all :class:`_ElementButton`. Keys are the symbols
("H", "He", "Li"...)"""
@@ -618,7 +618,7 @@ class PeriodicTable(qt.QWidget):
def _elementClicked(self, item):
"""Emit :attr:`sigElementClicked`,
toggle selected state of element
-
+
:param PeriodicTableItem item: Element clicked
"""
if self._eltCurrent is not None:
@@ -653,7 +653,7 @@ class PeriodicTable(qt.QWidget):
if isinstance(symbols[0], PeriodicTableItem):
symbols = [elmt.symbol for elmt in symbols]
- for (e, b) in self._eltButtons.items():
+ for e, b in self._eltButtons.items():
b.setSelected(e in symbols)
self.sigSelectionChanged.emit(self.getSelection())
@@ -697,6 +697,7 @@ class PeriodicCombo(qt.QComboBox):
a predefined list with minimal information (symbol, atomic number,
name, mass).
"""
+
sigSelectionChanged = qt.pyqtSignal(object)
"""Signal emitted when the selection changes. Send
:class:`PeriodicTableItem` object representing selected
@@ -753,6 +754,7 @@ class PeriodicList(qt.QTreeWidget):
:param single: *True* for single element selection with mouse click,
*False* for multiple element selection mode.
"""
+
sigSelectionChanged = qt.pyqtSignal(object)
"""When any element is selected/unselected in the widget, it emits
this signal and sends a list of currently selected
@@ -775,8 +777,11 @@ class PeriodicList(qt.QTreeWidget):
self.setRootIsDecorated(0)
self.itemClicked.connect(self.__selectionChanged)
- self.setSelectionMode(qt.QAbstractItemView.SingleSelection if single
- else qt.QAbstractItemView.ExtendedSelection)
+ self.setSelectionMode(
+ qt.QAbstractItemView.SingleSelection
+ if single
+ else qt.QAbstractItemView.ExtendedSelection
+ )
self.__fill_widget(elements)
self.resizeColumnToContents(0)
self.resizeColumnToContents(1)
@@ -784,7 +789,7 @@ class PeriodicList(qt.QTreeWidget):
self.resizeColumnToContents(2)
def __fill_widget(self, elements):
- """Fill tree widget with elements """
+ """Fill tree widget with elements"""
if elements is None:
elements = _defaultTableItems
@@ -814,8 +819,11 @@ class PeriodicList(qt.QTreeWidget):
:return: Selected elements
:rtype: List[PeriodicTableItem]"""
- return [_defaultTableItems[idx] for idx in range(len(self.tree_items))
- if self.tree_items[idx].isSelected()]
+ return [
+ _defaultTableItems[idx]
+ for idx in range(len(self.tree_items))
+ if self.tree_items[idx].isSelected()
+ ]
# setSelection is a bad name (name of a QTreeWidget method)
def setSelectedElements(self, symbolList):
@@ -828,4 +836,6 @@ class PeriodicList(qt.QTreeWidget):
if isinstance(symbolList[0], PeriodicTableItem):
symbolList = [elmt.symbol for elmt in symbolList]
for idx in range(len(self.tree_items)):
- self.tree_items[idx].setSelected(_defaultTableItems[idx].symbol in symbolList)
+ self.tree_items[idx].setSelected(
+ _defaultTableItems[idx].symbol in symbolList
+ )
diff --git a/src/silx/gui/widgets/PrintGeometryDialog.py b/src/silx/gui/widgets/PrintGeometryDialog.py
index 98ff8d1..652e1bc 100644
--- a/src/silx/gui/widgets/PrintGeometryDialog.py
+++ b/src/silx/gui/widgets/PrintGeometryDialog.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
@@ -35,6 +34,7 @@ class PrintGeometryWidget(qt.QWidget):
Use methods :meth:`setPrintGeometry` and :meth:`getPrintGeometry`
to interact with the widget.
"""
+
def __init__(self, parent=None):
super(PrintGeometryWidget, self).__init__(parent)
self.mainLayout = qt.QGridLayout(self)
@@ -108,21 +108,21 @@ class PrintGeometryWidget(qt.QWidget):
print geometry dictionary."""
ddict = {}
if self._inchButton.isChecked():
- ddict['units'] = "inches"
+ ddict["units"] = "inches"
elif self._cmButton.isChecked():
- ddict['units'] = "centimeters"
+ ddict["units"] = "centimeters"
else:
- ddict['units'] = "page"
+ ddict["units"] = "page"
- ddict['xOffset'] = self._xOffset.value()
- ddict['yOffset'] = self._yOffset.value()
- ddict['width'] = self._width.value()
- ddict['height'] = self._height.value()
+ ddict["xOffset"] = self._xOffset.value()
+ ddict["yOffset"] = self._yOffset.value()
+ ddict["width"] = self._width.value()
+ ddict["height"] = self._height.value()
if self._aspect.isChecked():
- ddict['keepAspectRatio'] = True
+ ddict["keepAspectRatio"] = True
else:
- ddict['keepAspectRatio'] = False
+ ddict["keepAspectRatio"] = False
return ddict
def setPrintGeometry(self, geometry=None):
@@ -145,22 +145,28 @@ class PrintGeometryWidget(qt.QWidget):
if geometry is None:
geometry = {}
oldDict = self.getPrintGeometry()
- for key in ["units", "xOffset", "yOffset",
- "width", "height", "keepAspectRatio"]:
+ for key in [
+ "units",
+ "xOffset",
+ "yOffset",
+ "width",
+ "height",
+ "keepAspectRatio",
+ ]:
geometry[key] = geometry.get(key, oldDict[key])
- if geometry['units'].lower().startswith("inc"):
+ if geometry["units"].lower().startswith("inc"):
self._inchButton.setChecked(True)
- elif geometry['units'].lower().startswith("c"):
+ elif geometry["units"].lower().startswith("c"):
self._cmButton.setChecked(True)
else:
self._pageButton.setChecked(True)
- self._xOffset.setText("%s" % float(geometry['xOffset']))
- self._yOffset.setText("%s" % float(geometry['yOffset']))
- self._width.setText("%s" % float(geometry['width']))
- self._height.setText("%s" % float(geometry['height']))
- if geometry['keepAspectRatio']:
+ self._xOffset.setText("%s" % float(geometry["xOffset"]))
+ self._yOffset.setText("%s" % float(geometry["yOffset"]))
+ self._width.setText("%s" % float(geometry["width"]))
+ self._height.setText("%s" % float(geometry["height"]))
+ if geometry["keepAspectRatio"]:
self._aspect.setChecked(True)
else:
self._aspect.setChecked(False)
diff --git a/src/silx/gui/widgets/PrintPreview.py b/src/silx/gui/widgets/PrintPreview.py
index 53e0a1f..285f12c 100644
--- a/src/silx/gui/widgets/PrintPreview.py
+++ b/src/silx/gui/widgets/PrintPreview.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
@@ -43,10 +42,9 @@ _logger = logging.getLogger(__name__)
class PrintPreviewDialog(qt.QDialog):
- """Print preview dialog widget.
- """
- def __init__(self, parent=None, printer=None):
+ """Print preview dialog widget."""
+ def __init__(self, parent=None, printer=None):
qt.QDialog.__init__(self, parent)
self.setWindowTitle("Print Preview")
self.setModal(False)
@@ -109,8 +107,7 @@ class PrintPreviewDialog(qt.QDialog):
cancelBut.setToolTip("Remove all items")
cancelBut.clicked.connect(self._clearAll)
- removeBut = qt.QPushButton("Remove",
- toolBar)
+ removeBut = qt.QPushButton("Remove", toolBar)
removeBut.setToolTip("Remove selected item (use left click to select)")
removeBut.clicked.connect(self._remove)
@@ -161,18 +158,17 @@ class PrintPreviewDialog(qt.QDialog):
self.targetLabel.setText("Undefined printer")
return
if self.printer.outputFileName():
- self.targetLabel.setText("File:" +
- self.printer.outputFileName())
+ self.targetLabel.setText("File:" + self.printer.outputFileName())
else:
- self.targetLabel.setText("Printer:" +
- self.printer.printerName())
+ self.targetLabel.setText("Printer:" + self.printer.printerName())
def _updatePrinter(self):
"""Resize :attr:`page`, :attr:`scene` and :attr:`view` to :attr:`printer`
width and height."""
printer = self.printer
- assert printer is not None, \
- "_updatePrinter should not be called unless a printer is defined"
+ assert (
+ printer is not None
+ ), "_updatePrinter should not be called unless a printer is defined"
if self.scene is None:
self.scene = qt.QGraphicsScene()
self.scene.setBackgroundBrush(qt.QColor(qt.Qt.lightGray))
@@ -205,9 +201,12 @@ class PrintPreviewDialog(qt.QDialog):
:param str comment: Comment displayed below the image
:param commentPosition: "CENTER" or "LEFT"
"""
- self.addPixmap(qt.QPixmap.fromImage(image),
- title=title, comment=comment,
- commentPosition=commentPosition)
+ self.addPixmap(
+ qt.QPixmap.fromImage(image),
+ title=title,
+ comment=comment,
+ commentPosition=commentPosition,
+ )
def addPixmap(self, pixmap, title=None, comment=None, commentPosition=None):
"""Add a pixmap to the print preview scene
@@ -224,14 +223,13 @@ class PrintPreviewDialog(qt.QDialog):
_logger.error("printer is not set, cannot add pixmap to page")
return
if title is None:
- title = ' ' * 88
+ title = " " * 88
if comment is None:
- comment = ' ' * 88
+ comment = " " * 88
if commentPosition is None:
commentPosition = "CENTER"
rectItem = qt.QGraphicsRectItem(self.page)
- rectItem.setRect(qt.QRectF(1, 1,
- pixmap.width(), pixmap.height()))
+ rectItem.setRect(qt.QRectF(1, 1, pixmap.width(), pixmap.height()))
pen = rectItem.pen()
color = qt.QColor(qt.Qt.red)
@@ -270,9 +268,15 @@ class PrintPreviewDialog(qt.QDialog):
rectItem.moveBy(20, 40)
- def addSvgItem(self, item, title=None,
- comment=None, commentPosition=None,
- viewBox=None, keepRatio=True):
+ def addSvgItem(
+ self,
+ item,
+ title=None,
+ comment=None,
+ commentPosition=None,
+ viewBox=None,
+ keepRatio=True,
+ ):
"""Add a SVG item to the scene.
:param QSvgRenderer item: SVG item to be added to the scene.
@@ -297,9 +301,9 @@ class PrintPreviewDialog(qt.QDialog):
return
if title is None:
- title = 50 * ' '
+ title = 50 * " "
if comment is None:
- comment = 80 * ' '
+ comment = 80 * " "
if commentPosition is None:
commentPosition = "CENTER"
@@ -320,8 +324,9 @@ class PrintPreviewDialog(qt.QDialog):
svgItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True)
svgItem.setFlag(qt.QGraphicsItem.ItemIsFocusable, False)
- rectItemResizeRect = _GraphicsResizeRectItem(svgItem, self.scene,
- keepratio=keepRatio)
+ rectItemResizeRect = _GraphicsResizeRectItem(
+ svgItem, self.scene, keepratio=keepRatio
+ )
rectItemResizeRect.setZValue(2)
self._svgItems.append(item)
@@ -358,9 +363,13 @@ class PrintPreviewDialog(qt.QDialog):
if alignment == qt.Qt.AlignLeft:
deltax = 0
else:
- deltax = (svgItem.boundingRect().width() - commentItem.boundingRect().width()) / 2.
- commentItem.moveBy(svgItem.boundingRect().x() + deltax,
- svgItem.boundingRect().y() + svgItem.boundingRect().height())
+ deltax = (
+ svgItem.boundingRect().width() - commentItem.boundingRect().width()
+ ) / 2.0
+ commentItem.moveBy(
+ svgItem.boundingRect().x() + deltax,
+ svgItem.boundingRect().y() + svgItem.boundingRect().height(),
+ )
# Title
textItem = qt.QGraphicsTextItem(title, svgItem)
@@ -369,9 +378,12 @@ class PrintPreviewDialog(qt.QDialog):
textItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True)
title_offset = 0.5 * textItem.boundingRect().width()
- textItem.moveBy(svgItem.boundingRect().x() +
- 0.5 * svgItem.boundingRect().width() - title_offset * scale,
- svgItem.boundingRect().y())
+ textItem.moveBy(
+ svgItem.boundingRect().x()
+ + 0.5 * svgItem.boundingRect().width()
+ - title_offset * scale,
+ svgItem.boundingRect().y(),
+ )
textItem.setScale(scale)
def setup(self):
@@ -388,7 +400,9 @@ class PrintPreviewDialog(qt.QDialog):
if self.printer.width() <= 0 or self.printer.height() <= 0:
self.message = qt.QMessageBox(self)
self.message.setIcon(qt.QMessageBox.Critical)
- self.message.setText("Unknown library error \non printer initialization")
+ self.message.setText(
+ "Unknown library error \non printer initialization"
+ )
self.message.setWindowTitle("Library Error")
self.message.setModal(0)
self.printer = None
@@ -413,8 +427,9 @@ class PrintPreviewDialog(qt.QDialog):
self.setup()
if self.printer is None:
self.hide()
- _logger.warning("Printer setup failed or was cancelled, " +
- "but printer is required.")
+ _logger.warning(
+ "Printer setup failed or was cancelled, " + "but printer is required."
+ )
return self.printer is not None
def setOutputFileName(self, name):
@@ -462,19 +477,27 @@ class PrintPreviewDialog(qt.QDialog):
_logger.error("Cannot initialize printer")
return
try:
- self.scene.render(painter, qt.QRectF(0, 0, printer.width(), printer.height()),
- qt.QRectF(self.page.rect().x(), self.page.rect().y(),
- self.page.rect().width(), self.page.rect().height()),
- qt.Qt.KeepAspectRatio)
+ self.scene.render(
+ painter,
+ qt.QRectF(0, 0, printer.width(), printer.height()),
+ qt.QRectF(
+ self.page.rect().x(),
+ self.page.rect().y(),
+ self.page.rect().width(),
+ self.page.rect().height(),
+ ),
+ qt.Qt.KeepAspectRatio,
+ )
painter.end()
self.hide()
self.accept()
self._toBeCleared = True
- except: # FIXME
+ except: # FIXME
painter.end()
- qt.QMessageBox.critical(self, "ERROR",
- 'Printing problem:\n %s' % sys.exc_info()[1])
- _logger.error('printing problem:\n %s' % sys.exc_info()[1])
+ qt.QMessageBox.critical(
+ self, "ERROR", "Printing problem:\n %s" % sys.exc_info()[1]
+ )
+ _logger.error("printing problem:\n %s" % sys.exc_info()[1])
return
def _zoomPlus(self):
@@ -502,8 +525,7 @@ class PrintPreviewDialog(qt.QDialog):
self._toBeCleared = False
def _remove(self):
- """Remove selected item in :attr:`scene`.
- """
+ """Remove selected item in :attr:`scene`."""
itemlist = self.scene.items()
# this loop is not efficient if there are many items ...
@@ -519,6 +541,7 @@ class SingletonPrintPreviewDialog(PrintPreviewDialog):
a single print preview dialog. This enables sending
multiple images to a single page to be printed.
"""
+
_instance = None
def __new__(self, *var, **kw):
@@ -531,6 +554,7 @@ class _GraphicsSvgRectItem(qt.QGraphicsRectItem):
""":class:`qt.QGraphicsRectItem` with an attached
:class:`qt.QSvgRenderer`, and with a painter redefined to render
the SVG item."""
+
def setSvgRenderer(self, renderer):
"""
@@ -544,6 +568,7 @@ class _GraphicsSvgRectItem(qt.QGraphicsRectItem):
class _GraphicsResizeRectItem(qt.QGraphicsRectItem):
"""Resizable QGraphicsRectItem."""
+
def __init__(self, parent=None, scene=None, keepratio=True):
qt.QGraphicsRectItem.__init__(self, parent)
rect = parent.boundingRect()
@@ -562,7 +587,7 @@ class _GraphicsResizeRectItem(qt.QGraphicsRectItem):
pen.setStyle(qt.Qt.NoPen)
self.setPen(pen)
self.setBrush(color)
- self.setFlag(self.ItemIsMovable, True)
+ self.setFlag(qt.QGraphicsItem.ItemIsMovable, True)
self.show()
def hoverEnterEvent(self, event):
@@ -603,10 +628,7 @@ class _GraphicsResizeRectItem(qt.QGraphicsRectItem):
self._h = rect.height()
self._ratio = self._w / self._h
self._newRect = qt.QGraphicsRectItem(parent)
- self._newRect.setRect(qt.QRectF(self._x,
- self._y,
- self._w,
- self._h))
+ self._newRect.setRect(qt.QRectF(self._x, self._y, self._w, self._h))
qt.QGraphicsRectItem.mousePressEvent(self, event)
def mouseMoveEvent(self, event):
@@ -617,20 +639,27 @@ class _GraphicsResizeRectItem(qt.QGraphicsRectItem):
r1 = (self._w + deltax) / self._w
r2 = (self._h + deltay) / self._h
if r1 < r2:
- self._newRect.setRect(qt.QRectF(self._x,
- self._y,
- self._w + deltax,
- (self._w + deltax) / self._ratio))
+ self._newRect.setRect(
+ qt.QRectF(
+ self._x,
+ self._y,
+ self._w + deltax,
+ (self._w + deltax) / self._ratio,
+ )
+ )
else:
- self._newRect.setRect(qt.QRectF(self._x,
- self._y,
- (self._h + deltay) * self._ratio,
- self._h + deltay))
+ self._newRect.setRect(
+ qt.QRectF(
+ self._x,
+ self._y,
+ (self._h + deltay) * self._ratio,
+ self._h + deltay,
+ )
+ )
else:
- self._newRect.setRect(qt.QRectF(self._x,
- self._y,
- self._w + deltax,
- self._h + deltay))
+ self._newRect.setRect(
+ qt.QRectF(self._x, self._y, self._w + deltax, self._h + deltay)
+ )
qt.QGraphicsRectItem.mouseMoveEvent(self, event)
def mouseReleaseEvent(self, event):
@@ -650,8 +679,7 @@ class _GraphicsResizeRectItem(qt.QGraphicsRectItem):
# apply the scale to the previous transformation matrix
previousTransform = parent.transform()
- parent.setTransform(
- previousTransform.scale(scalex, scaley))
+ parent.setTransform(previousTransform.scale(scalex, scaley))
self.scene().removeItem(self._newRect)
self._newRect = None
@@ -659,8 +687,7 @@ class _GraphicsResizeRectItem(qt.QGraphicsRectItem):
def main():
- """
- """
+ """ """
if len(sys.argv) < 2:
print("give an image file as parameter please.")
sys.exit(1)
@@ -679,19 +706,20 @@ def main():
if filename[-3:] == "svg":
item = qt.QSvgRenderer(filename, w.page)
- w.addSvgItem(item, title=filename,
- comment=comment, commentPosition="CENTER")
+ w.addSvgItem(item, title=filename, comment=comment, commentPosition="CENTER")
else:
- w.addPixmap(qt.QPixmap.fromImage(qt.QImage(filename)),
- title=filename,
- comment=comment,
- commentPosition="CENTER")
+ w.addPixmap(
+ qt.QPixmap.fromImage(qt.QImage(filename)),
+ title=filename,
+ comment=comment,
+ commentPosition="CENTER",
+ )
w.addImage(qt.QImage(filename), comment=comment, commentPosition="LEFT")
sys.exit(w.exec())
-if __name__ == '__main__':
+if __name__ == "__main__":
a = qt.QApplication(sys.argv)
main()
a.exec()
diff --git a/src/silx/gui/widgets/RangeSlider.py b/src/silx/gui/widgets/RangeSlider.py
index 61b73fc..c96ae14 100644
--- a/src/silx/gui/widgets/RangeSlider.py
+++ b/src/silx/gui/widgets/RangeSlider.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,11 +26,10 @@
.. image:: img/RangeSlider.png
:align: center
"""
-from __future__ import absolute_import, division
__authors__ = ["D. Naudet", "T. Vincent"]
__license__ = "MIT"
-__date__ = "26/11/2018"
+__date__ = "14/12/2023"
import numpy as numpy
@@ -93,10 +91,10 @@ class RangeSlider(qt.QWidget):
def __init__(self, parent=None):
self.__pixmap = None
self.__positionCount = None
- self.__firstValue = 0.
- self.__secondValue = 1.
- self.__minValue = 0.
- self.__maxValue = 1.
+ self.__firstValue = 0.0
+ self.__secondValue = 1.0
+ self.__minValue = 0.0
+ self.__maxValue = 1.0
self.__hoverRect = qt.QRect()
self.__hoverControl = None
@@ -104,8 +102,8 @@ class RangeSlider(qt.QWidget):
self.__moving = None
self.__icons = {
- 'first': icons.getQIcon('previous'),
- 'second': icons.getQIcon('next')
+ "first": icons.getQIcon("previous"),
+ "second": icons.getQIcon("next"),
}
# call the super constructor AFTER defining all members that
@@ -123,8 +121,17 @@ class RangeSlider(qt.QWidget):
def event(self, event):
t = event.type()
- if t == qt.QEvent.HoverEnter or t == qt.QEvent.HoverLeave or t == qt.QEvent.HoverMove:
- return self.__updateHoverControl(event.pos())
+ if (
+ t == qt.QEvent.HoverEnter
+ or t == qt.QEvent.HoverLeave
+ or t == qt.QEvent.HoverMove
+ ):
+ if qt.BINDING in ("PyQt5",):
+ # qt-5
+ return self.__updateHoverControl(event.pos())
+ else:
+ # qt-6
+ return self.__updateHoverControl(event.position().toPoint())
else:
return super(RangeSlider, self).event(event)
@@ -258,8 +265,7 @@ class RangeSlider(qt.QWidget):
:param int first:
:param int second:
"""
- self.setValues(self.__positionToValue(first),
- self.__positionToValue(second))
+ self.setValues(self.__positionToValue(first), self.__positionToValue(second))
# Value (float) API
@@ -502,15 +508,25 @@ class RangeSlider(qt.QWidget):
self.setGroovePixmap(qpixmap)
# Handle interaction
+ def _mouseEventPosition(self, event):
+ if qt.BINDING in ("PyQt5",):
+ # qt-5 returns QPoint
+ position = event.pos()
+ else:
+ # qt-6 returns QPointF
+ # convert it to QPoint
+ position = event.position().toPoint()
+ return position
def mousePressEvent(self, event):
super(RangeSlider, self).mousePressEvent(event)
if event.buttons() == qt.Qt.LeftButton:
picked = None
- for name in ('first', 'second'):
+ for name in ("first", "second"):
area = self.__sliderRect(name)
- if area.contains(event.pos()):
+ position = self._mouseEventPosition(event)
+ if area.contains(position):
picked = name
break
@@ -522,12 +538,13 @@ class RangeSlider(qt.QWidget):
super(RangeSlider, self).mouseMoveEvent(event)
if self.__moving is not None:
+ event_pos = self._mouseEventPosition(event)
delta = self._SLIDER_WIDTH // 2
- if self.__moving == 'first':
- position = self.__xPixelToPosition(event.pos().x() + delta)
+ if self.__moving == "first":
+ position = self.__xPixelToPosition(event_pos.x() + delta)
self.setFirstPosition(position)
else:
- position = self.__xPixelToPosition(event.pos().x() - delta)
+ position = self.__xPixelToPosition(event_pos.x() - delta)
self.setSecondPosition(position)
def mouseReleaseEvent(self, event):
@@ -547,13 +564,13 @@ class RangeSlider(qt.QWidget):
key = event.key()
if event.modifiers() == qt.Qt.NoModifier and self.__focus is not None:
if key in (qt.Qt.Key_Left, qt.Qt.Key_Down):
- if self.__focus == 'first':
+ if self.__focus == "first":
self.setFirstPosition(self.getFirstPosition() - 1)
else:
self.setSecondPosition(self.getSecondPosition() - 1)
return # accept event
elif key in (qt.Qt.Key_Right, qt.Qt.Key_Up):
- if self.__focus == 'first':
+ if self.__focus == "first":
self.setFirstPosition(self.getFirstPosition() + 1)
else:
self.setSecondPosition(self.getSecondPosition() + 1)
@@ -567,8 +584,10 @@ class RangeSlider(qt.QWidget):
super(RangeSlider, self).resizeEvent(event)
# If no step, signal position update when width change
- if (self.getPositionCount() is None and
- event.size().width() != event.oldSize().width()):
+ if (
+ self.getPositionCount() is None
+ and event.size().width() != event.oldSize().width()
+ ):
self.sigPositionChanged.emit(*self.getPositions())
# Handle repaint
@@ -591,15 +610,15 @@ class RangeSlider(qt.QWidget):
:rtype: QRect
:raise ValueError: If wrong name
"""
- assert name in ('first', 'second')
- if name == 'first':
- offset = - self._SLIDER_WIDTH
+ assert name in ("first", "second")
+ if name == "first":
+ offset = -self._SLIDER_WIDTH
position = self.getFirstPosition()
- elif name == 'second':
+ elif name == "second":
offset = 0
position = self.getSecondPosition()
else:
- raise ValueError('Unknown name')
+ raise ValueError("Unknown name")
sliderArea = self.__sliderAreaRect()
@@ -607,26 +626,20 @@ class RangeSlider(qt.QWidget):
xOffset = int((sliderArea.width() - 1) * position / maxPos)
xPos = sliderArea.left() + xOffset + offset
- return qt.QRect(xPos,
- sliderArea.top(),
- self._SLIDER_WIDTH,
- sliderArea.height())
+ return qt.QRect(xPos, sliderArea.top(), self._SLIDER_WIDTH, sliderArea.height())
def __drawArea(self):
- return self.rect().adjusted(self._SLIDER_WIDTH, 0,
- -self._SLIDER_WIDTH, 0)
+ return self.rect().adjusted(self._SLIDER_WIDTH, 0, -self._SLIDER_WIDTH, 0)
def __sliderAreaRect(self):
- return self.__drawArea().adjusted(self._SLIDER_WIDTH // 2,
- 0,
- -self._SLIDER_WIDTH // 2 + 1,
- 0)
+ return self.__drawArea().adjusted(
+ self._SLIDER_WIDTH // 2, 0, -self._SLIDER_WIDTH // 2 + 1, 0
+ )
def __pixMapRect(self):
- return self.__sliderAreaRect().adjusted(0,
- self._PIXMAP_VOFFSET,
- -1,
- -self._PIXMAP_VOFFSET)
+ return self.__sliderAreaRect().adjusted(
+ 0, self._PIXMAP_VOFFSET, -1, -self._PIXMAP_VOFFSET
+ )
def paintEvent(self, event):
painter = qt.QPainter(self)
@@ -640,12 +653,10 @@ class RangeSlider(qt.QWidget):
option = qt.QStyleOptionProgressBar()
option.initFrom(self)
option.rect = area
- option.state = (qt.QStyle.State_Enabled if self.isEnabled()
- else qt.QStyle.State_None)
- style.drawControl(qt.QStyle.CE_ProgressBarGroove,
- option,
- painter,
- self)
+ option.state = (
+ qt.QStyle.State_Enabled if self.isEnabled() else qt.QStyle.State_None
+ )
+ style.drawControl(qt.QStyle.CE_ProgressBarGroove, option, painter, self)
painter.save()
pen = painter.pen()
@@ -656,13 +667,13 @@ class RangeSlider(qt.QWidget):
painter.restore()
if self.isEnabled():
- rect = area.adjusted(self._SLIDER_WIDTH // 2,
- self._PIXMAP_VOFFSET,
- -self._SLIDER_WIDTH // 2,
- -self._PIXMAP_VOFFSET + 1)
- painter.drawPixmap(rect,
- self.__pixmap,
- self.__pixmap.rect())
+ rect = area.adjusted(
+ self._SLIDER_WIDTH // 2,
+ self._PIXMAP_VOFFSET,
+ -self._SLIDER_WIDTH // 2,
+ -self._PIXMAP_VOFFSET + 1,
+ )
+ painter.drawPixmap(rect, self.__pixmap, self.__pixmap.rect())
else:
option = StyleOptionRangeSlider()
option.initFrom(self)
@@ -673,8 +684,9 @@ class RangeSlider(qt.QWidget):
option.handlerRect2 = self.__sliderRect("second")
option.minimum = self.__minValue
option.maximum = self.__maxValue
- option.state = (qt.QStyle.State_Enabled if self.isEnabled()
- else qt.QStyle.State_None)
+ option.state = (
+ qt.QStyle.State_Enabled if self.isEnabled() else qt.QStyle.State_None
+ )
if self.__hoverControl == "groove":
option.state |= qt.QStyle.State_MouseOver
elif option.state & qt.QStyle.State_MouseOver:
@@ -684,7 +696,7 @@ class RangeSlider(qt.QWidget):
# Avoid glitch when moving handles
hoverControl = self.__moving or self.__hoverControl
- for name in ('first', 'second'):
+ for name in ("first", "second"):
rect = self.__sliderRect(name)
option = qt.QStyleOptionButton()
option.initFrom(self)
@@ -699,8 +711,7 @@ class RangeSlider(qt.QWidget):
elif option.state & qt.QStyle.State_HasFocus:
option.state ^= qt.QStyle.State_HasFocus
option.rect = rect
- style.drawControl(
- qt.QStyle.CE_PushButton, option, painter, self)
+ style.drawControl(qt.QStyle.CE_PushButton, option, painter, self)
def sizeHint(self):
return qt.QSize(200, self.minimumHeight())
@@ -733,17 +744,24 @@ class RangeSlider(qt.QWidget):
buttonColor = option.palette.button().color()
val = qt.qGray(buttonColor.rgb())
buttonColor = buttonColor.lighter(100 + max(1, (180 - val) // 6))
- buttonColor.setHsv(buttonColor.hue(), (buttonColor.saturation() * 3) // 4, buttonColor.value())
+ buttonColor.setHsv(
+ buttonColor.hue(), (buttonColor.saturation() * 3) // 4, buttonColor.value()
+ )
grooveColor = qt.QColor()
- grooveColor.setHsv(buttonColor.hue(),
- min(255, (int)(buttonColor.saturation())),
- min(255, (int)(buttonColor.value() * 0.9)))
+ grooveColor.setHsv(
+ buttonColor.hue(),
+ min(255, (int)(buttonColor.saturation())),
+ min(255, (int)(buttonColor.value() * 0.9)),
+ )
selectedInnerContrastLine = qt.QColor(255, 255, 255, 30)
outline = option.palette.color(qt.QPalette.Window).darker(140)
- if (option.state & qt.QStyle.State_HasFocus and option.state & qt.QStyle.State_KeyboardFocusChange):
+ if (
+ option.state & qt.QStyle.State_HasFocus
+ and option.state & qt.QStyle.State_KeyboardFocusChange
+ ):
outline = highlight.darker(125)
if outline.value() > 160:
outline.setHsl(highlight.hue(), highlight.saturation(), 160)
@@ -762,7 +780,9 @@ class RangeSlider(qt.QWidget):
# Draw slider background for the value
gradient = qt.QLinearGradient()
gradient.setStart(selectedRangeRect.center().x(), selectedRangeRect.top())
- gradient.setFinalStop(selectedRangeRect.center().x(), selectedRangeRect.bottom())
+ gradient.setFinalStop(
+ selectedRangeRect.center().x(), selectedRangeRect.bottom()
+ )
painter.setRenderHint(qt.QPainter.Antialiasing, True)
painter.setPen(qt.QPen(selectedOutline))
gradient.setColorAt(0, activeHighlight)
diff --git a/src/silx/gui/widgets/StackedProgressBar.py b/src/silx/gui/widgets/StackedProgressBar.py
new file mode 100644
index 0000000..87a5896
--- /dev/null
+++ b/src/silx/gui/widgets/StackedProgressBar.py
@@ -0,0 +1,314 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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.
+#
+# ###########################################################################*/
+
+from __future__ import annotations
+
+from typing import NamedTuple, Any, ValuesView
+from silx.gui import qt
+
+
+class ProgressItem(NamedTuple):
+ """Item storing the state of a stacked progress item"""
+
+ value: int
+ """Progression of the item"""
+
+ visible: bool
+ """Is the item displayed"""
+
+ color: qt.QColor
+ """Color of the progress"""
+
+ striped: bool
+ """If true, apply a stripe color to the gradiant"""
+
+ animated: bool
+ """If true, the stripe is animated"""
+
+ toolTip: str
+ """Tool tip of this item"""
+
+ userData: Any
+ """Any user data"""
+
+
+class _UndefinedType:
+ pass
+
+
+_Undefined = _UndefinedType()
+
+
+class StackedProgressBar(qt.QProgressBar):
+ """
+ Multiple stacked progress bar in single component
+ """
+
+ def __init__(self, parent: qt.Qwidget | None = None):
+ super().__init__(parent=parent)
+ self.__stacks: dict[str, ProgressItem] = {}
+ self._animated: int = 0
+ self._timer = qt.QTimer(self)
+ self._timer.setInterval(80)
+ self._timer.timeout.connect(self._tick)
+ self._spacing: int = 0
+ self._spacingCollapsible: bool = True
+
+ def _tick(self):
+ self._animated += 2
+ self.update()
+
+ def setSpacing(self, spacing: int):
+ """Spacing between items, in pixels"""
+ if self._spacing == spacing:
+ return
+ self._spacing = spacing
+ self.update()
+
+ def spacing(self) -> int:
+ return self._spacing
+
+ def setSpacingCollapsible(self, collapse: bool):
+ """
+ Set whether consecutive spacing should be collapsed.
+
+ It can be useful to disable that to ensure pixel perfect
+ rendering is some use cases.
+
+
+ By default, this property is true.
+ """
+ if self._spacingCollapsible == collapse:
+ return
+ self._spacingCollapsible = collapse
+ self.update()
+
+ def spacingCollapsible(self) -> bool:
+ return self._spacingCollapsible
+
+ def clear(self):
+ """Remove every stacked items from the widget"""
+ if len(self.__stacks) == 0:
+ return
+ self.__stacks.clear()
+ self.update()
+
+ def setProgressItem(
+ self,
+ name: str,
+ value: int | None | _UndefinedType = _Undefined,
+ visible: bool | _UndefinedType = _Undefined,
+ color: qt.QColor | None | _UndefinedType = _Undefined,
+ striped: bool | _UndefinedType = _Undefined,
+ animated: bool | _UndefinedType = _Undefined,
+ toolTip: str | None | _UndefinedType = _Undefined,
+ userData: Any = _Undefined,
+ ):
+ """Add or update a stacked items by its name"""
+
+ previousItem = self.__stacks.get(name)
+
+ if previousItem is not None:
+ if value is _Undefined:
+ value = previousItem.value
+ if visible is _Undefined:
+ visible = previousItem.visible
+ if striped is _Undefined:
+ striped = previousItem.striped
+ if color is _Undefined:
+ color = previousItem.color
+ if toolTip is _Undefined:
+ toolTip = previousItem.toolTip
+ if animated is _Undefined:
+ animated = previousItem.animated
+ if userData is _Undefined:
+ userData = previousItem.userData
+ else:
+ if value is _Undefined:
+ value = 0
+ if visible is _Undefined:
+ visible = True
+ if striped is _Undefined:
+ striped = False
+ if color is _Undefined:
+ color = qt.QColor()
+ if toolTip is _Undefined:
+ toolTip = ""
+ if animated is _Undefined:
+ animated = False
+ if userData is _Undefined:
+ userData = None
+
+ newItem = ProgressItem(
+ value=value,
+ visible=visible,
+ color=color,
+ striped=striped,
+ animated=animated,
+ toolTip=toolTip,
+ userData=userData,
+ )
+ if previousItem == newItem:
+ return
+ self.__stacks[name] = newItem
+ animated = any([s.animated for s in self.__stacks.values()])
+ self._setAnimated(animated)
+ self.update()
+
+ def _setAnimated(self, animated: bool):
+ if animated == self._timer.isActive():
+ return
+ if animated:
+ self._timer.start()
+ else:
+ self._timer.stop()
+
+ def removeProgressItem(self, name: str):
+ """Remove a stacked item by its name"""
+ s = self.__stacks.pop(name, None)
+ if s is None:
+ return
+ self.update()
+
+ def _brushFromProgressItem(self, item: ProgressItem) -> qt.QPalette | None:
+ if item.color is None:
+ return None
+
+ palette = qt.QPalette()
+ color = qt.QColor(item.color)
+
+ if item.striped:
+ if item.animated:
+ delta = self._animated
+ else:
+ delta = 0
+ color2 = color.lighter(120)
+ shadowGradient = qt.QLinearGradient()
+ shadowGradient.setSpread(qt.QGradient.RepeatSpread)
+ shadowGradient.setStart(-delta, 0)
+ shadowGradient.setFinalStop(8 - delta, -8)
+ shadowGradient.setColorAt(0.0, color)
+ shadowGradient.setColorAt(0.5, color)
+ shadowGradient.setColorAt(0.50001, color2)
+ shadowGradient.setColorAt(1.0, color2)
+ brush = qt.QBrush(shadowGradient)
+ palette.setBrush(qt.QPalette.Highlight, brush)
+ palette.setBrush(qt.QPalette.Window, color2)
+ else:
+ palette.setColor(qt.QPalette.Highlight, color)
+
+ return palette
+
+ def paintEvent(self, event):
+ painter = qt.QStylePainter(self)
+ opt = qt.QStyleOptionProgressBar()
+ self.initStyleOption(opt)
+ painter.drawControl(qt.QStyle.CE_ProgressBarGroove, opt)
+ self._drawProgressItems(painter, self.__stacks.values())
+
+ def _drawProgressItems(self, painter: qt.QPainter, items: ValuesView[ProgressItem]):
+ opt = qt.QStyleOptionProgressBar()
+ self.initStyleOption(opt)
+
+ visibleItems = [i for i in items if i.value and i.visible]
+ xpos: int = 0
+ w = opt.rect.width()
+ if self._spacingCollapsible:
+ cumspacing = max(0, len(visibleItems) - 1) * self._spacing
+ w -= cumspacing
+ vw = opt.maximum - opt.minimum
+ opt.minimum = 0
+ opt.maximum = w
+
+ for item in visibleItems:
+ xwidth = int(item.value * w / vw)
+ opt.progress = xwidth * 2
+ palette = self._brushFromProgressItem(item)
+ if palette is not None:
+ opt.palette = palette
+ self._drawProgressItem(painter, opt, xpos, xwidth)
+ xpos += xwidth + self._spacing
+
+ def _drawProgressItem(
+ self,
+ painter: qt.QPainter,
+ option: qt.QStyleOptionProgressBar,
+ xpos: int,
+ xwidth: int,
+ ):
+ if xwidth == 0:
+ return
+ rect: qt.QRect = option.rect
+ style = self.style()
+
+ if option.minimum == 0 and option.maximum == 0:
+ return
+ x0 = rect.x() + 3
+ y0 = rect.y()
+
+ h = rect.height()
+ w = rect.width()
+ xmaxwith = min(x0 + xpos + xwidth, w - 1) - x0 - xpos
+ if xmaxwith < 0:
+ return
+ rect = qt.QRect(x0 + xpos, y0, xmaxwith, h)
+ opt = qt.QStyleOptionProgressBar()
+ opt.state = qt.QStyle.State_None
+ margin = 1
+ opt.rect = rect.marginsAdded(qt.QMargins(margin, margin, margin, margin))
+ opt.palette = option.palette
+ style.drawPrimitive(qt.QStyle.PE_IndicatorProgressChunk, opt, painter, self)
+
+ def getProgressItemByPosition(self, pos: qt.QPoint) -> ProgressItem | None:
+ """Returns the stacked item at a position of the component."""
+ minimum = self.minimum()
+ maximum = self.maximum()
+ vRange = maximum - minimum
+ w = self.width()
+ v = pos.x() * vRange / w
+ current = 0
+ for item in self.__stacks.values():
+ if not item.visible:
+ continue
+ current += item.value
+ if v < current:
+ return item
+ return None
+
+ def tooltipFromProgressItem(self, item: ProgressItem) -> str | None:
+ """Returns the tooltip to display over an item.
+
+ It is triggered when the tooltip have to be displayed.
+ """
+ return item.toolTip
+
+ def event(self, event: qt.QEvent):
+ if event.type() == qt.QEvent.ToolTip:
+ item = self.getProgressItemByPosition(event.pos())
+ if item is not None:
+ toolTip = self.tooltipFromProgressItem(item)
+ if toolTip:
+ qt.QToolTip.showText(event.globalPos(), toolTip, self)
+ return True
+ return super().event(event)
diff --git a/src/silx/gui/widgets/TableWidget.py b/src/silx/gui/widgets/TableWidget.py
index 50eb9e2..7f6c1eb 100644
--- a/src/silx/gui/widgets/TableWidget.py
+++ b/src/silx/gui/widgets/TableWidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
@@ -83,10 +82,12 @@ class CopySelectedCellsAction(qt.QAction):
:param table: :class:`QTableView` to which this action belongs.
"""
+
def __init__(self, table):
if not isinstance(table, qt.QTableView):
- raise ValueError('CopySelectedCellsAction must be initialised ' +
- 'with a QTableWidget.')
+ raise ValueError(
+ "CopySelectedCellsAction must be initialised " + "with a QTableWidget."
+ )
super(CopySelectedCellsAction, self).__init__(table)
self.setText("Copy selection")
self.setToolTip("Copy selected cells into the clipboard.")
@@ -126,11 +127,11 @@ class CopySelectedCellsAction(qt.QAction):
data_model.setData(index, "")
copied_text += col_separator
# remove the right-most tabulation
- copied_text = copied_text[:-len(col_separator)]
+ copied_text = copied_text[: -len(col_separator)]
# add a newline
copied_text += row_separator
# remove final newline
- copied_text = copied_text[:-len(row_separator)]
+ copied_text = copied_text[: -len(row_separator)]
# put this text into clipboard
qapp = qt.QApplication.instance()
@@ -147,10 +148,12 @@ class CopyAllCellsAction(qt.QAction):
:param table: :class:`QTableView` to which this action belongs.
"""
+
def __init__(self, table):
if not isinstance(table, qt.QTableView):
- raise ValueError('CopyAllCellsAction must be initialised ' +
- 'with a QTableWidget.')
+ raise ValueError(
+ "CopyAllCellsAction must be initialised " + "with a QTableWidget."
+ )
super(CopyAllCellsAction, self).__init__(table)
self.setText("Copy all")
self.setToolTip("Copy all cells into the clipboard.")
@@ -176,11 +179,11 @@ class CopyAllCellsAction(qt.QAction):
data_model.setData(index, "")
copied_text += col_separator
# remove the right-most tabulation
- copied_text = copied_text[:-len(col_separator)]
+ copied_text = copied_text[: -len(col_separator)]
# add a newline
copied_text += row_separator
# remove final newline
- copied_text = copied_text[:-len(row_separator)]
+ copied_text = copied_text[: -len(row_separator)]
# put this text into clipboard
qapp = qt.QApplication.instance()
@@ -207,6 +210,7 @@ class CutSelectedCellsAction(CopySelectedCellsAction):
corresponding cell in the origin table.
:param table: :class:`QTableView` to which this action belongs."""
+
def __init__(self, table):
super(CutSelectedCellsAction, self).__init__(table)
self.setText("Cut selection")
@@ -229,6 +233,7 @@ class CutAllCellsAction(CopyAllCellsAction):
newline characters.
:param table: :class:`QTableView` to which this action belongs."""
+
def __init__(self, table):
super(CutAllCellsAction, self).__init__(table)
self.setText("Cut all")
@@ -267,17 +272,21 @@ class PasteCellsAction(qt.QAction):
:param table: :class:`QTableView` to which this action belongs.
"""
+
def __init__(self, table):
if not isinstance(table, qt.QTableView):
- raise ValueError('PasteCellsAction must be initialised ' +
- 'with a QTableWidget.')
+ raise ValueError(
+ "PasteCellsAction must be initialised " + "with a QTableWidget."
+ )
super(PasteCellsAction, self).__init__(table)
self.table = table
self.setText("Paste")
self.setShortcut(qt.QKeySequence.Paste)
self.setShortcutContext(qt.Qt.WidgetShortcut)
- self.setToolTip("Paste data. The selected cell is the top-left" +
- "corner of the paste area.")
+ self.setToolTip(
+ "Paste data. The selected cell is the top-left"
+ + "corner of the paste area."
+ )
self.triggered.connect(self.pasteCellFromClipboard)
def pasteCellFromClipboard(self):
@@ -310,8 +319,10 @@ class PasteCellsAction(qt.QAction):
target_row = selected_row + row_offset
target_col = selected_col + col_offset
- if target_row >= data_model.rowCount() or\
- target_col >= data_model.columnCount():
+ if (
+ target_row >= data_model.rowCount()
+ or target_col >= data_model.columnCount()
+ ):
out_of_range_cells += 1
continue
@@ -349,10 +360,12 @@ class CopySingleCellAction(qt.QAction):
:param table: :class:`QTableView` to which this action belongs.
"""
+
def __init__(self, table):
if not isinstance(table, qt.QTableView):
- raise ValueError('CopySingleCellAction must be initialised ' +
- 'with a QTableWidget.')
+ raise ValueError(
+ "CopySingleCellAction must be initialised " + "with a QTableWidget."
+ )
super(CopySingleCellAction, self).__init__(table)
self.setText("Copy cell")
self.setToolTip("Copy cell content into the clipboard.")
@@ -360,8 +373,7 @@ class CopySingleCellAction(qt.QAction):
self.table = table
def copyCellToClipboard(self):
- """
- """
+ """ """
cell_text = self.table._text_last_cell_clicked
if cell_text is None:
return
@@ -393,6 +405,7 @@ class TableWidget(qt.QTableWidget):
:param bool cut: Enable cut action
:param bool paste: Enable paste action
"""
+
def __init__(self, parent=None, cut=False, paste=False):
super(TableWidget, self).__init__(parent)
self._text_last_cell_clicked = None
@@ -458,8 +471,10 @@ class TableWidget(qt.QTableWidget):
self.cutSelectedCellsAction.setEnabled(False)
if self.copySingleCellAction is None:
self.copySingleCellAction = CopySingleCellAction(self)
- self.insertAction(self.copySelectedCellsAction, # before first action
- self.copySingleCellAction)
+ self.insertAction(
+ self.copySelectedCellsAction, # before first action
+ self.copySingleCellAction,
+ )
self.copySingleCellAction.setVisible(True)
self.copySingleCellAction.setEnabled(True)
else:
@@ -499,6 +514,7 @@ class TableView(qt.QTableView):
:param bool cut: Enable cut action
:param bool paste: Enable paste action
"""
+
def __init__(self, parent=None, cut=False, paste=False):
super(TableView, self).__init__(parent)
self._text_last_cell_clicked = None
@@ -515,7 +531,7 @@ class TableView(qt.QTableView):
def mousePressEvent(self, event):
qindex = self.indexAt(event.pos())
- if self.copyAllCellsAction is not None: # model was set
+ if self.copyAllCellsAction is not None: # model was set
self._text_last_cell_clicked = self.model().data(qindex)
super(TableView, self).mousePressEvent(event)
@@ -568,8 +584,7 @@ class TableView(qt.QTableView):
# compare action type and parent widget with those of existing actions
for existing_action in self.actions():
if type(action) == type(existing_action):
- if hasattr(action, "table") and\
- action.table is existing_action.table:
+ if hasattr(action, "table") and action.table is existing_action.table:
return None
super(TableView, self).addAction(action)
@@ -588,8 +603,10 @@ class TableView(qt.QTableView):
self.cutSelectedCellsAction.setEnabled(False)
if self.copySingleCellAction is None:
self.copySingleCellAction = CopySingleCellAction(self)
- self.insertAction(self.copySelectedCellsAction, # before first action
- self.copySingleCellAction)
+ self.insertAction(
+ self.copySelectedCellsAction, # before first action
+ self.copySingleCellAction,
+ )
self.copySingleCellAction.setVisible(True)
self.copySingleCellAction.setEnabled(True)
else:
diff --git a/src/silx/gui/widgets/ThreadPoolPushButton.py b/src/silx/gui/widgets/ThreadPoolPushButton.py
index 949b6ef..12eb95b 100644
--- a/src/silx/gui/widgets/ThreadPoolPushButton.py
+++ b/src/silx/gui/widgets/ThreadPoolPushButton.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
@@ -58,7 +57,9 @@ class _Wrapper(qt.QRunnable):
except Exception as e:
module = self.__callable.__module__
name = self.__callable.__name__
- _logger.error("Error while executing callable %s.%s.", module, name, exc_info=True)
+ _logger.error(
+ "Error while executing callable %s.%s.", module, name, exc_info=True
+ )
holder.failed.emit(e)
finally:
holder.finished.emit()
diff --git a/src/silx/gui/widgets/UrlList.py b/src/silx/gui/widgets/UrlList.py
new file mode 100644
index 0000000..3800d10
--- /dev/null
+++ b/src/silx/gui/widgets/UrlList.py
@@ -0,0 +1,139 @@
+# /*##########################################################################
+#
+# Copyright (c) 2023 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.
+#
+# ###########################################################################*/
+
+from __future__ import annotations
+
+import typing
+import logging
+from collections.abc import Iterable
+from silx.io.url import DataUrl
+from silx.gui import qt
+from silx.utils.deprecation import deprecated
+
+_logger = logging.getLogger(__name__)
+
+
+class UrlList(qt.QListWidget):
+ """List of URLs with user selection"""
+
+ sigCurrentUrlChanged = qt.Signal(str)
+ """Signal emitted when the active/current URL has changed.
+
+ This signal emits the empty string when there is no longer an active URL.
+ """
+
+ sigUrlRemoved = qt.Signal(str)
+ """Signal emit when an url is removed from the URL list.
+
+ Provides the url (DataUrl) as a string
+ """
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self._editable = False
+ # are we in 'editable' mode: for now if true then we can remove some items from the list
+
+ # menu to be triggered when in edition from right-click
+ self._menu = qt.QMenu()
+ self._removeAction = qt.QAction(text="Remove", parent=self)
+ self._removeAction.setShortcuts(
+ [
+ # qt.Qt.Key_Delete,
+ qt.QKeySequence.Delete,
+ ]
+ )
+ self._menu.addAction(self._removeAction)
+
+ # connect signal / Slot
+ self.currentItemChanged.connect(self._notifyCurrentUrlChanged)
+
+ def setEditable(self, editable: bool):
+ """Toggle whether the user can remove some URLs from the list"""
+ if editable != self._editable:
+ self._editable = editable
+ # discusable choice: should we change the selection mode ? No much meaning
+ # to be in ExtendedSelection if we are not in editable mode. But does it has more
+ # meaning to change the selection mode ?
+ if editable:
+ self._removeAction.triggered.connect(self._removeSelectedItems)
+ self.addAction(self._removeAction)
+ else:
+ self._removeAction.triggered.disconnect(self._removeSelectedItems)
+ self.removeAction(self._removeAction)
+
+ @deprecated(replacement="addUrls", since_version="2.0")
+ def setUrls(self, urls: Iterable[DataUrl]) -> None:
+ self.addUrls(urls)
+
+ def addUrls(self, urls: Iterable[DataUrl]) -> None:
+ """Append multiple DataUrl to the list"""
+ self.addItems([url.path() for url in urls])
+
+ def removeUrl(self, url: str):
+ """Remove given URL from the list"""
+ sel_items = self.findItems(url, qt.Qt.MatchExactly)
+ if len(sel_items) > 0:
+ assert len(sel_items) == 0, "at most one item expected"
+ self.removeItemWidget(sel_items[0])
+
+ def _notifyCurrentUrlChanged(self, current, previous):
+ if current is None:
+ self.sigCurrentUrlChanged.emit("")
+ else:
+ self.sigCurrentUrlChanged.emit(current.text())
+
+ def setUrl(self, url: typing.Optional[DataUrl]) -> None:
+ """Set the current URL.
+
+ :param url: The new selected URL. Use `None` to clear the selection.
+ """
+ if url is None:
+ self.clearSelection()
+ self.sigCurrentUrlChanged.emit("")
+ else:
+ assert isinstance(url, DataUrl)
+ sel_items = self.findItems(url.path(), qt.Qt.MatchExactly)
+ if sel_items is None:
+ _logger.warning(url.path(), " is not registered in the list.")
+ elif len(sel_items) > 0:
+ item = sel_items[0]
+ self.setCurrentItem(item)
+ self.sigCurrentUrlChanged.emit(item.text())
+
+ def _removeSelectedItems(self):
+ if not self._editable:
+ raise ValueError("UrlList is not set as 'editable'")
+ urls = []
+ for item in self.selectedItems():
+ url = item.text()
+ self.takeItem(self.row(item))
+ urls.append(url)
+ # as the connected slot of 'sigUrlRemoved' can modify the items, better handling all at the end
+ for url in urls:
+ self.sigUrlRemoved.emit(url)
+
+ def contextMenuEvent(self, event):
+ if self._editable:
+ globalPos = self.mapToGlobal(event.pos())
+ self._menu.exec_(globalPos)
diff --git a/src/silx/gui/widgets/UrlSelectionTable.py b/src/silx/gui/widgets/UrlSelectionTable.py
index bc75d32..051ff32 100644
--- a/src/silx/gui/widgets/UrlSelectionTable.py
+++ b/src/silx/gui/widgets/UrlSelectionTable.py
@@ -1,5 +1,5 @@
# /*##########################################################################
-# Copyright (C) 2017-2021 European Synchrotron Radiation Facility
+# Copyright (C) 2017-2023 European Synchrotron Radiation Facility
#
# This file is part of the PyMca X-ray Fluorescence Toolkit developed at
# the ESRF by the Software group.
@@ -29,25 +29,88 @@ __author__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "19/03/2018"
-from silx.gui import qt
-from collections import OrderedDict
-from silx.gui.widgets.TableWidget import TableWidget
-from silx.io.url import DataUrl
+import os
import functools
import logging
-import os
+from silx.gui import qt
+from silx.gui import utils as qtutils
+from silx.gui.widgets.TableWidget import TableWidget
+from silx.io.url import DataUrl, slice_sequence_to_string
+from silx.utils.deprecation import deprecated, deprecated_warning
+from silx.gui import constants
logger = logging.getLogger(__name__)
+class _IntegratedRadioButton(qt.QWidget):
+ """RadioButton integrated in the QTableWidget as a centered widget"""
+
+ toggled = qt.Signal()
+
+ def __init__(self, parent=None):
+ qt.QWidget.__init__(self, parent=parent)
+ self.setContentsMargins(1, 1, 1, 1)
+ layout = qt.QHBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(1)
+
+ self._radio = qt.QRadioButton(parent=self)
+ self._radio.setObjectName("radio")
+ self._radio.setAutoExclusive(False)
+ self._radio.setMinimumSize(self._radio.minimumSizeHint())
+ self._radio.setMaximumSize(self._radio.minimumSizeHint())
+ self._radio.toggled.connect(self.toggled.emit)
+ layout.addWidget(self._radio)
+ self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
+
+ def setChecked(self, checked: bool):
+ self._radio.setChecked(checked)
+
+ def isChecked(self) -> bool:
+ return self._radio.isChecked()
+
+
+class _DataUrlItem(qt.QTableWidgetItem):
+ FILENAME = 0
+ DATAPATH = 1
+ SLICE = 2
+
+ def __init__(self, url, display: int):
+ qt.QTableWidgetItem.__init__(self)
+ self._url = url
+ self._display = display
+
+ if self._display == self.FILENAME:
+ text = os.path.basename(self._url.file_path())
+ elif self._display == self.DATAPATH:
+ text = self._url.data_path()
+ elif self._display == self.SLICE:
+ s = self._url.data_slice()
+ if s is not None:
+ text = slice_sequence_to_string(self._url.data_slice())
+ else:
+ text = ""
+ else:
+ raise RuntimeError(f"Unsupported display node: {self._display}")
+
+ toolTip = self._url.path()
+
+ self.setText(text)
+ self.setToolTip(toolTip)
+
+ def dataUrl(self):
+ return self._url
+
+
class UrlSelectionTable(TableWidget):
"""Table used to select the color channel to be displayed for each"""
- COLUMS_INDEX = OrderedDict([
- ('url', 0),
- ('img A', 1),
- ('img B', 2),
- ])
+ FILENAME_COLUMN = 0
+ DATAPATH_COLUMN = 1
+ SLICE_COLUMN = 2
+ IMG_A_COLUMN = 3
+ IMG_B_COLUMN = 4
+ NB_COLUMNS = 5
sigImageAChanged = qt.Signal(str)
"""Signal emitted when the image A change. Param is the image url path"""
@@ -62,12 +125,38 @@ class UrlSelectionTable(TableWidget):
def clear(self):
qt.QTableWidget.clear(self)
self.setRowCount(0)
- self.setColumnCount(len(self.COLUMS_INDEX))
- self.setHorizontalHeaderLabels(list(self.COLUMS_INDEX.keys()))
- self.verticalHeader().hide()
- self.horizontalHeader().setSectionResizeMode(0,
- qt.QHeaderView.Stretch)
+ self.setColumnCount(self.NB_COLUMNS)
+ self.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
+ self.setSelectionMode(qt.QAbstractItemView.NoSelection)
+ item = qt.QTableWidgetItem()
+ item.setText("Filename")
+ item.setToolTip("Filename to the data")
+ self.setHorizontalHeaderItem(self.FILENAME_COLUMN, item)
+ item = qt.QTableWidgetItem()
+ item.setText("Datapath")
+ item.setToolTip("Data path to the dataset")
+ self.setHorizontalHeaderItem(self.DATAPATH_COLUMN, item)
+ item = qt.QTableWidgetItem()
+ item.setText("Slice")
+ item.setToolTip("Slice applied to the dataset")
+ self.setHorizontalHeaderItem(self.SLICE_COLUMN, item)
+ item = qt.QTableWidgetItem()
+ item.setText("A")
+ item.setToolTip("Selected image as A")
+ self.setHorizontalHeaderItem(self.IMG_A_COLUMN, item)
+ item = qt.QTableWidgetItem()
+ item.setText("B")
+ item.setToolTip("Selected image as B")
+ self.setHorizontalHeaderItem(self.IMG_B_COLUMN, item)
+
+ self.verticalHeader().hide()
+ setSectionResizeMode = self.horizontalHeader().setSectionResizeMode
+ setSectionResizeMode(self.FILENAME_COLUMN, qt.QHeaderView.ResizeToContents)
+ setSectionResizeMode(self.DATAPATH_COLUMN, qt.QHeaderView.Stretch)
+ setSectionResizeMode(self.SLICE_COLUMN, qt.QHeaderView.ResizeToContents)
+ setSectionResizeMode(self.IMG_A_COLUMN, qt.QHeaderView.ResizeToContents)
+ setSectionResizeMode(self.IMG_B_COLUMN, qt.QHeaderView.ResizeToContents)
self.setSortingEnabled(True)
self._checkBoxes = {}
@@ -79,11 +168,12 @@ class UrlSelectionTable(TableWidget):
for url in urls:
self.addUrl(url=url)
- def addUrl(self, url, **kwargs):
+ def addUrl(self, url: DataUrl, **kwargs):
"""
+ Append this DataUrl to the end of the list of URLs.
- :param url:
- :param args:
+ :param url:
+ :param args:
:return: index of the created items row
:rtype int
"""
@@ -91,79 +181,167 @@ class UrlSelectionTable(TableWidget):
row = self.rowCount()
self.setRowCount(row + 1)
- _item = qt.QTableWidgetItem()
- _item.setText(os.path.basename(url.path()))
- _item.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
- self.setItem(row, self.COLUMS_INDEX['url'], _item)
+ item = _DataUrlItem(url, _DataUrlItem.FILENAME)
+ item.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
+ self.setItem(row, self.FILENAME_COLUMN, item)
- widgetImgA = qt.QRadioButton(parent=self)
- widgetImgA.setAutoExclusive(False)
- self.setCellWidget(row, self.COLUMS_INDEX['img A'], widgetImgA)
- callbackImgA = functools.partial(self._activeImgAChanged, url.path())
+ item = _DataUrlItem(url, _DataUrlItem.DATAPATH)
+ item.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
+ self.setItem(row, self.DATAPATH_COLUMN, item)
+
+ item = _DataUrlItem(url, _DataUrlItem.SLICE)
+ item.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
+ self.setItem(row, self.SLICE_COLUMN, item)
+
+ widgetImgA = _IntegratedRadioButton(parent=self)
+ self.setCellWidget(row, self.IMG_A_COLUMN, widgetImgA)
+ callbackImgA = functools.partial(self._activeImgAChanged, row)
widgetImgA.toggled.connect(callbackImgA)
- widgetImgB = qt.QRadioButton(parent=self)
- widgetImgA.setAutoExclusive(False)
- self.setCellWidget(row, self.COLUMS_INDEX['img B'], widgetImgB)
- callbackImgB = functools.partial(self._activeImgBChanged, url.path())
+ widgetImgB = _IntegratedRadioButton(parent=self)
+ self.setCellWidget(row, self.IMG_B_COLUMN, widgetImgB)
+ callbackImgB = functools.partial(self._activeImgBChanged, row)
widgetImgB.toggled.connect(callbackImgB)
- self._checkBoxes[url.path()] = {'img A': widgetImgA,
- 'img B': widgetImgB}
+ self._checkBoxes[row] = {
+ self.IMG_A_COLUMN: widgetImgA,
+ self.IMG_B_COLUMN: widgetImgB,
+ }
self.resizeColumnsToContents()
return row
- def _activeImgAChanged(self, name):
- self._updatecheckBoxes('img A', name)
- self.sigImageAChanged.emit(name)
+ def _getItemFromUrlPath(self, urlPath: str) -> _DataUrlItem:
+ """Returns the Qt item storing this urlPath, else None"""
+ for r in range(self.rowCount()):
+ item = self.item(r, self.FILENAME_COLUMN)
+ url = item.dataUrl()
+ if url.path() == urlPath:
+ return item
+ return None
+
+ def setError(self, urlPath: str, message: str):
+ """Flag this urlPath with an error in the UI."""
+ item = self._getItemFromUrlPath(urlPath)
+ if item is None:
+ return
+ if message == "":
+ item.setIcon(qt.QIcon())
+ item.setToolTip("")
+ else:
+ style = qt.QApplication.style()
+ icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical)
+ item.setIcon(icon)
+ item.setToolTip(f"Error: {message}")
- def _activeImgBChanged(self, name):
- self._updatecheckBoxes('img B', name)
- self.sigImageBChanged.emit(name)
+ def _activeImgAChanged(self, row):
+ if self._checkBoxes[row][self.IMG_A_COLUMN].isChecked():
+ self._updateCheckBoxes(self.IMG_A_COLUMN, row)
+ url = self.item(row, self.FILENAME_COLUMN).dataUrl()
+ self.sigImageAChanged.emit(url.path())
+ else:
+ self.sigImageAChanged.emit(None)
- def _updatecheckBoxes(self, whichImg, name):
- assert name in self._checkBoxes
- assert whichImg in self._checkBoxes[name]
- if self._checkBoxes[name][whichImg].isChecked():
- for radioUrl in self._checkBoxes:
- if radioUrl != name:
- self._checkBoxes[radioUrl][whichImg].blockSignals(True)
- self._checkBoxes[radioUrl][whichImg].setChecked(False)
- self._checkBoxes[radioUrl][whichImg].blockSignals(False)
+ def _activeImgBChanged(self, row):
+ if self._checkBoxes[row][self.IMG_B_COLUMN].isChecked():
+ self._updateCheckBoxes(self.IMG_B_COLUMN, row)
+ url = self.item(row, self.FILENAME_COLUMN).dataUrl()
+ self.sigImageBChanged.emit(url.path())
+ else:
+ self.sigImageBChanged.emit(None)
+ def _updateCheckBoxes(self, column, row):
+ for r in range(self.rowCount()):
+ if r == row:
+ continue
+ c = self._checkBoxes[r][column]
+ with qtutils.blockSignals(c):
+ c.setChecked(False)
+
+ @deprecated(
+ replacement="getUrlSelection",
+ since_version="2.0",
+ reason="Conflict with Qt API",
+ )
def getSelection(self):
+ return self.getUrlSelection()
+
+ def setSelection(self, url_img_a, url_img_b):
+ if isinstance(url_img_a, qt.QRect):
+ return super().setSelection(url_img_a, url_img_b)
+ deprecated_warning(
+ "Function",
+ "setSelection",
+ replacement="setUrlSelection",
+ since_version="2.0",
+ reason="Conflict with Qt API",
+ )
+ return self.setUrlSelection(url_img_a, url_img_b)
+
+ def getUrlSelection(self):
"""
:return: url selected for img A and img B.
"""
imgA = imgB = None
- for radioUrl in self._checkBoxes:
- if self._checkBoxes[radioUrl]['img A'].isChecked():
- imgA = radioUrl
- if self._checkBoxes[radioUrl]['img B'].isChecked():
- imgB = radioUrl
+ for row in range(self.rowCount()):
+ url = self.item(row, self.FILENAME_COLUMN).dataUrl()
+ if self._checkBoxes[row][self.IMG_A_COLUMN].isChecked():
+ imgA = url
+ if self._checkBoxes[row][self.IMG_B_COLUMN].isChecked():
+ imgB = url
return imgA, imgB
- def setSelection(self, url_img_a, url_img_b):
+ def setUrlSelection(self, url_img_a, url_img_b):
"""
:param ddict: key: image url, values: list of active channels
"""
- for radioUrl in self._checkBoxes:
- for img in ('img A', 'img B'):
- self._checkBoxes[radioUrl][img].blockSignals(True)
- self._checkBoxes[radioUrl][img].setChecked(False)
- self._checkBoxes[radioUrl][img].blockSignals(False)
-
- self._checkBoxes[radioUrl][img].blockSignals(True)
- self._checkBoxes[url_img_a]['img A'].setChecked(True)
- self._checkBoxes[radioUrl][img].blockSignals(False)
-
- self._checkBoxes[radioUrl][img].blockSignals(True)
- self._checkBoxes[url_img_b]['img B'].setChecked(True)
- self._checkBoxes[radioUrl][img].blockSignals(False)
+ rowA = None
+ rowB = None
+ for row in range(self.rowCount()):
+ for img in (self.IMG_A_COLUMN, self.IMG_B_COLUMN):
+ c = self._checkBoxes[row][img]
+ with qtutils.blockSignals(c):
+ c.setChecked(False)
+ url = self.item(row, self.FILENAME_COLUMN).dataUrl()
+ if url.path() == url_img_a:
+ rowA = row
+ if url.path() == url_img_b:
+ rowB = row
+
+ if rowA is not None:
+ c = self._checkBoxes[rowA][self.IMG_A_COLUMN]
+ with qtutils.blockSignals(c):
+ c.setChecked(True)
+
+ if rowB is not None:
+ c = self._checkBoxes[rowB][self.IMG_B_COLUMN]
+ with qtutils.blockSignals(c):
+ c.setChecked(True)
+
self.sigImageAChanged.emit(url_img_a)
self.sigImageBChanged.emit(url_img_b)
def removeUrl(self, url):
raise NotImplementedError("")
+
+ def supportedDropActions(self):
+ """Inherited method to redefine supported drop actions."""
+ return qt.Qt.CopyAction | qt.Qt.MoveAction
+
+ def mimeTypes(self):
+ """Inherited method to redefine draggable mime types."""
+ return [constants.SILX_URI_MIMETYPE]
+
+ def dropMimeData(
+ self, row: int, column: int, mimedata: qt.QMimeType, action: qt.Qt.DropAction
+ ):
+ """Inherited method to handle a drop operation to this model."""
+ if action == qt.Qt.IgnoreAction:
+ return True
+ if mimedata.hasFormat(constants.SILX_URI_MIMETYPE):
+ urlText = str(mimedata.data(constants.SILX_URI_MIMETYPE), "utf-8")
+ url = DataUrl(urlText)
+ self.addUrl(url)
+ return True
+ return False
diff --git a/src/silx/gui/widgets/WaitingOverlay.py b/src/silx/gui/widgets/WaitingOverlay.py
new file mode 100644
index 0000000..f6872d6
--- /dev/null
+++ b/src/silx/gui/widgets/WaitingOverlay.py
@@ -0,0 +1,111 @@
+import weakref
+from typing import Optional
+from silx.gui.widgets.WaitingPushButton import WaitingPushButton
+from silx.gui import qt
+from silx.gui.qt import inspect as qt_inspect
+from silx.gui.plot import PlotWidget
+
+
+class WaitingOverlay(qt.QWidget):
+ """Widget overlaying another widget with a processing wheel icon.
+
+ :param parent: widget on top of which to display the "processing/waiting wheel"
+ """
+
+ def __init__(self, parent: qt.QWidget) -> None:
+ super().__init__(parent)
+ self.setContentsMargins(0, 0, 0, 0)
+
+ self._waitingButton = WaitingPushButton(self)
+ self._waitingButton.setDown(True)
+ self._waitingButton.setWaiting(True)
+ self._waitingButton.setStyleSheet(
+ "QPushButton { background-color: rgba(150, 150, 150, 40); border: 0px; border-radius: 10px; }"
+ )
+ self._registerParent(parent)
+
+ def text(self) -> str:
+ """Returns displayed text"""
+ return self._waitingButton.text()
+
+ def setText(self, text: str):
+ """Set displayed text"""
+ self._waitingButton.setText(text)
+ self._resize()
+
+ def _listenedWidget(self, parent: qt.QWidget) -> qt.QWidget:
+ """Returns widget to register event filter to according to parent"""
+ if isinstance(parent, PlotWidget):
+ return parent.getWidgetHandle()
+ return parent
+
+ def _backendChanged(self):
+ self._listenedWidget(self.parent()).installEventFilter(self)
+ self._resizeLater()
+
+ def _registerParent(self, parent: Optional[qt.QWidget]):
+ if parent is None:
+ return
+ self._listenedWidget(parent).installEventFilter(self)
+ if isinstance(parent, PlotWidget):
+ parent.sigBackendChanged.connect(self._backendChanged)
+ self._resize()
+
+ def _unregisterParent(self, parent: Optional[qt.QWidget]):
+ if parent is None:
+ return
+ if isinstance(parent, PlotWidget):
+ parent.sigBackendChanged.disconnect(self._backendChanged)
+ self._listenedWidget(parent).removeEventFilter(self)
+
+ def setParent(self, parent: qt.QWidget):
+ self._unregisterParent(self.parent())
+ super().setParent(parent)
+ self._registerParent(parent)
+
+ def showEvent(self, event: qt.QShowEvent):
+ super().showEvent(event)
+ self._waitingButton.setVisible(True)
+
+ def hideEvent(self, event: qt.QHideEvent):
+ super().hideEvent(event)
+ self._waitingButton.setVisible(False)
+
+ def _resize(self):
+ if not qt_inspect.isValid(self):
+ return # For _resizeLater in case the widget has been deleted
+
+ parent = self.parent()
+ if parent is None:
+ return
+
+ size = self._waitingButton.sizeHint()
+ if isinstance(parent, PlotWidget):
+ offset = parent.getWidgetHandle().mapTo(parent, qt.QPoint(0, 0))
+ left, top, width, height = parent.getPlotBoundsInPixels()
+ rect = qt.QRect(
+ qt.QPoint(
+ int(offset.x() + left + width / 2 - size.width() / 2),
+ int(offset.y() + top + height / 2 - size.height() / 2),
+ ),
+ size,
+ )
+ else:
+ position = parent.size()
+ position = (position - size) / 2
+ rect = qt.QRect(qt.QPoint(position.width(), position.height()), size)
+ self.setGeometry(rect)
+ self.raise_()
+
+ def _resizeLater(self):
+ qt.QTimer.singleShot(0, self._resize)
+
+ def eventFilter(self, watched: qt.QWidget, event: qt.QEvent):
+ if event.type() == qt.QEvent.Resize:
+ self._resize()
+ self._resizeLater() # Defer resize for the receiver to have handled it
+ return super().eventFilter(watched, event)
+
+ # expose Waiting push button API
+ def setIconSize(self, size):
+ self._waitingButton.setIconSize(size)
diff --git a/src/silx/gui/widgets/WaitingPushButton.py b/src/silx/gui/widgets/WaitingPushButton.py
index 443dc9a..ff31286 100644
--- a/src/silx/gui/widgets/WaitingPushButton.py
+++ b/src/silx/gui/widgets/WaitingPushButton.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2021 European Synchrotron Radiation Facility
@@ -105,8 +104,10 @@ class WaitingPushButton(qt.QPushButton):
w += self.style().pixelMetric(qt.QStyle.PM_MenuButtonIndicator, opt, self)
contentSize = qt.QSize(w, h)
- sizeHint = self.style().sizeFromContents(qt.QStyle.CT_PushButton, opt, contentSize, self)
- if qt.BINDING in ('PySide2', 'PyQt5'): # Qt6: globalStrut not available
+ sizeHint = self.style().sizeFromContents(
+ qt.QStyle.CT_PushButton, opt, contentSize, self
+ )
+ if qt.BINDING == "PyQt5": # Qt6: globalStrut not available
sizeHint = sizeHint.expandedTo(qt.QApplication.globalStrut())
return sizeHint
@@ -127,7 +128,9 @@ class WaitingPushButton(qt.QPushButton):
"""
return self.__disabled_when_waiting
- disabledWhenWaiting = qt.Property(bool, isDisabledWhenWaiting, setDisabledWhenWaiting)
+ disabledWhenWaiting = qt.Property(
+ bool, isDisabledWhenWaiting, setDisabledWhenWaiting
+ )
"""Property to enable/disable the auto disabled state when the button is waiting."""
def __setWaitingIcon(self, icon):
diff --git a/src/silx/gui/widgets/__init__.py b/src/silx/gui/widgets/__init__.py
index 9d0299d..cab7ef6 100644
--- a/src/silx/gui/widgets/__init__.py
+++ b/src/silx/gui/widgets/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/widgets/test/__init__.py b/src/silx/gui/widgets/test/__init__.py
index 243dbc7..03af6f2 100644
--- a/src/silx/gui/widgets/test/__init__.py
+++ b/src/silx/gui/widgets/test/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2020 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/widgets/test/test_boxlayoutdockwidget.py b/src/silx/gui/widgets/test/test_boxlayoutdockwidget.py
index 5df8df9..45f0152 100644
--- a/src/silx/gui/widgets/test/test_boxlayoutdockwidget.py
+++ b/src/silx/gui/widgets/test/test_boxlayoutdockwidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "06/03/2018"
-import unittest
-
from silx.gui.widgets.BoxLayoutDockWidget import BoxLayoutDockWidget
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt
@@ -54,8 +51,8 @@ class TestBoxLayoutDockWidget(TestCaseQt):
"""Test update of layout direction according to dock area"""
# Create a widget with a QBoxLayout
layout = qt.QBoxLayout(qt.QBoxLayout.LeftToRight)
- layout.addWidget(qt.QLabel('First'))
- layout.addWidget(qt.QLabel('Second'))
+ layout.addWidget(qt.QLabel("First"))
+ layout.addWidget(qt.QLabel("Second"))
widget = qt.QWidget()
widget.setLayout(layout)
diff --git a/src/silx/gui/widgets/test/test_elidedlabel.py b/src/silx/gui/widgets/test/test_elidedlabel.py
index 693e43c..fbf63f0 100644
--- a/src/silx/gui/widgets/test/test_elidedlabel.py
+++ b/src/silx/gui/widgets/test/test_elidedlabel.py
@@ -1,7 +1,6 @@
-# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2020 European Synchrotron Radiation Facility
+# Copyright (c) 2020-2022 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,15 +26,12 @@
__license__ = "MIT"
__date__ = "08/06/2020"
-import unittest
-
from silx.gui import qt
from silx.gui.widgets.ElidedLabel import ElidedLabel
from silx.gui.utils import testutils
class TestElidedLabel(testutils.TestCaseQt):
-
def setUp(self):
self.label = ElidedLabel()
self.label.show()
@@ -47,11 +43,18 @@ class TestElidedLabel(testutils.TestCaseQt):
del self.label
self.qapp.processEvents()
+ def testQLabelApi(self):
+ """Test overrided API from QLabel"""
+ self.label.setText("a")
+ assert self.label.text() == "a"
+ self.label.setToolTip("b")
+ assert self.label.toolTip() == "b"
+
def testElidedValue(self):
"""Test elided text"""
raw = "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"
self.label.setText(raw)
- self.label.setFixedWidth(30)
+ self.label.setFixedWidth(40)
displayedText = qt.QLabel.text(self.label)
self.assertNotEqual(raw, displayedText)
self.assertIn("…", displayedText)
@@ -98,3 +101,21 @@ class TestElidedLabel(testutils.TestCaseQt):
displayedTooltip = qt.QLabel.toolTip(self.label)
self.assertNotIn(raw1, displayedTooltip)
self.assertIn(raw2, displayedTooltip)
+
+ def testTooltip(self):
+ """Test tooltip when elided"""
+ self.label.setToolTip("Fooo")
+ assert self.label.toolTip() == "Fooo"
+ displayedTooltip = qt.QLabel.toolTip(self.label)
+ assert displayedTooltip == "Fooo"
+
+ def testElidedTextAndTooltip(self):
+ """Test tooltip when elided"""
+ raw1 = "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn"
+ self.label.setText(raw1)
+ self.label.setFixedWidth(30)
+ self.label.setToolTip("Fooo")
+ displayedTooltip = qt.QLabel.toolTip(self.label)
+ assert self.label.toolTip() == "Fooo"
+ assert "Fooo" in displayedTooltip
+ assert raw1 in displayedTooltip
diff --git a/src/silx/gui/widgets/test/test_floatedit.py b/src/silx/gui/widgets/test/test_floatedit.py
new file mode 100644
index 0000000..c5edded
--- /dev/null
+++ b/src/silx/gui/widgets/test/test_floatedit.py
@@ -0,0 +1,82 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Tests for FloatEdit"""
+
+__license__ = "MIT"
+
+import pytest
+from silx.gui import qt
+from silx.gui.widgets.FloatEdit import FloatEdit
+
+
+@pytest.fixture
+def floatEdit(qWidgetFactory):
+ widget = qWidgetFactory(FloatEdit)
+ yield widget
+
+
+@pytest.fixture
+def floatEditHolder(qWidgetFactory, floatEdit):
+ widget = qWidgetFactory(qt.QWidget)
+ layout = qt.QHBoxLayout(widget)
+ layout.addStretch()
+ layout.addWidget(floatEdit)
+ yield widget
+
+
+def test_show(floatEdit):
+ pass
+
+
+def test_value(floatEdit):
+ floatEdit.setValue(1.5)
+ assert floatEdit.value() == 1.5
+
+
+def test_no_widgetresize(floatEditHolder, floatEdit):
+ floatEditHolder.resize(50, 50)
+ floatEdit.setValue(123)
+ a = floatEdit.width()
+ floatEdit.setValue(123456789123456789.123456789123456789)
+ b = floatEdit.width()
+ assert b == a
+
+
+def test_widgetresize(qapp_utils, floatEditHolder, floatEdit):
+ floatEditHolder.resize(50, 50)
+ floatEdit.setWidgetResizable(True)
+ # Initial
+ floatEdit.setValue(123)
+ qapp_utils.qWait()
+ a = floatEdit.width()
+ # Grow
+ floatEdit.setValue(123456789123456789.123456789123456789)
+ qapp_utils.qWait()
+ b = floatEdit.width()
+ # Shrink
+ floatEdit.setValue(123)
+ qapp_utils.qWait()
+ c = floatEdit.width()
+ assert b > a
+ assert a <= c < b
diff --git a/src/silx/gui/widgets/test/test_flowlayout.py b/src/silx/gui/widgets/test/test_flowlayout.py
index 85d7cfe..c39e2a5 100644
--- a/src/silx/gui/widgets/test/test_flowlayout.py
+++ b/src/silx/gui/widgets/test/test_flowlayout.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "02/08/2018"
-import unittest
-
from silx.gui.widgets.FlowLayout import FlowLayout
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt
@@ -56,8 +53,8 @@ class TestFlowLayout(TestCaseQt):
layout = FlowLayout()
self.widget.setLayout(layout)
- layout.addWidget(qt.QLabel('first'))
- layout.addWidget(qt.QLabel('second'))
+ layout.addWidget(qt.QLabel("first"))
+ layout.addWidget(qt.QLabel("second"))
self.assertEqual(layout.count(), 2)
layout.setHorizontalSpacing(10)
diff --git a/src/silx/gui/widgets/test/test_framebrowser.py b/src/silx/gui/widgets/test/test_framebrowser.py
index 8233622..bb80a58 100644
--- a/src/silx/gui/widgets/test/test_framebrowser.py
+++ b/src/silx/gui/widgets/test/test_framebrowser.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -27,8 +26,6 @@ __license__ = "MIT"
__date__ = "23/03/2018"
-import unittest
-
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.widgets.FrameBrowser import FrameBrowser
diff --git a/src/silx/gui/widgets/test/test_hierarchicaltableview.py b/src/silx/gui/widgets/test/test_hierarchicaltableview.py
index 302086a..5ef36a0 100644
--- a/src/silx/gui/widgets/test/test_hierarchicaltableview.py
+++ b/src/silx/gui/widgets/test/test_hierarchicaltableview.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -26,15 +25,12 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "07/04/2017"
-import unittest
-
from .. import HierarchicalTableView
from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
class TableModel(HierarchicalTableView.HierarchicalTableModel):
-
def __init__(self, parent):
HierarchicalTableView.HierarchicalTableModel.__init__(self, parent)
self.__content = {}
diff --git a/src/silx/gui/widgets/test/test_legendiconwidget.py b/src/silx/gui/widgets/test/test_legendiconwidget.py
index fe320f6..d31de23 100644
--- a/src/silx/gui/widgets/test/test_legendiconwidget.py
+++ b/src/silx/gui/widgets/test/test_legendiconwidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2020 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "23/10/2020"
-import unittest
-
from silx.gui import qt
from silx.gui.widgets.LegendIconWidget import LegendIconWidget
from silx.gui.utils.testutils import TestCaseQt
diff --git a/src/silx/gui/widgets/test/test_periodictable.py b/src/silx/gui/widgets/test/test_periodictable.py
index de9e1af..a2efed1 100644
--- a/src/silx/gui/widgets/test/test_periodictable.py
+++ b/src/silx/gui/widgets/test/test_periodictable.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
@@ -26,8 +25,6 @@ __authors__ = ["P. Knobel"]
__license__ = "MIT"
__date__ = "05/12/2016"
-import unittest
-
from .. import PeriodicTable
from silx.gui.utils.testutils import TestCaseQt
from silx.gui import qt
@@ -50,9 +47,8 @@ class TestPeriodicTable(TestCaseQt):
def testCustomElements(self):
PTI = PeriodicTable.ColoredPeriodicTableItem
my_items = [
- PTI("Xx", 42, 43, 44, "xaxatorium", 1002.2,
- bgcolor="#FF0000"),
- PTI("Yy", 25, 22, 44, "yoyotrium", 8.8)
+ PTI("Xx", 42, 43, 44, "xaxatorium", 1002.2, bgcolor="#FF0000"),
+ PTI("Yy", 25, 22, 44, "yoyotrium", 8.8),
]
pt = PeriodicTable.PeriodicTable(elements=my_items)
@@ -64,8 +60,7 @@ class TestPeriodicTable(TestCaseQt):
self.assertEqual(selection[0].Z, 42)
self.assertEqual(selection[0].col, 43)
self.assertAlmostEqual(selection[0].mass, 1002.2)
- self.assertEqual(qt.QColor(selection[0].bgcolor),
- qt.QColor(qt.Qt.red))
+ self.assertEqual(qt.QColor(selection[0].bgcolor), qt.QColor(qt.Qt.red))
self.assertTrue(pt.isElementSelected("Xx"))
self.assertFalse(pt.isElementSelected("Yy"))
@@ -79,7 +74,7 @@ class TestPeriodicTable(TestCaseQt):
my_items = [
MyPTI("Xx", 42, 43, 44, "xaxatorium", 1002.2, "spam"),
- MyPTI("Yy", 25, 22, 44, "yoyotrium", 8.8, "eggs")
+ MyPTI("Yy", 25, 22, 44, "yoyotrium", 8.8, "eggs"),
]
pt = PeriodicTable.PeriodicTable(elements=my_items)
@@ -97,6 +92,7 @@ class TestPeriodicTable(TestCaseQt):
class TestPeriodicCombo(TestCaseQt):
"""Basic test for ArrayTableWidget with a numpy array"""
+
def setUp(self):
super(TestPeriodicCombo, self).setUp()
self.pc = PeriodicTable.PeriodicCombo()
@@ -113,8 +109,7 @@ class TestPeriodicCombo(TestCaseQt):
def testSelect(self):
self.pc.setSelection("Sb")
selection = self.pc.getSelection()
- self.assertIsInstance(selection,
- PeriodicTable.PeriodicTableItem)
+ self.assertIsInstance(selection, PeriodicTable.PeriodicTableItem)
self.assertEqual(selection.symbol, "Sb")
self.assertEqual(selection.Z, 51)
self.assertEqual(selection.name, "antimony")
@@ -122,6 +117,7 @@ class TestPeriodicCombo(TestCaseQt):
class TestPeriodicList(TestCaseQt):
"""Basic test for ArrayTableWidget with a numpy array"""
+
def setUp(self):
super(TestPeriodicList, self).setUp()
self.pl = PeriodicTable.PeriodicList()
@@ -139,8 +135,7 @@ class TestPeriodicList(TestCaseQt):
self.pl.setSelectedElements(["Li", "He", "Au"])
sel_elmts = self.pl.getSelection()
- self.assertEqual(len(sel_elmts), 3,
- "Wrong number of elements selected")
+ self.assertEqual(len(sel_elmts), 3, "Wrong number of elements selected")
for e in sel_elmts:
self.assertIsInstance(e, PeriodicTable.PeriodicTableItem)
self.assertIn(e.symbol, ["Li", "He", "Au"])
diff --git a/src/silx/gui/widgets/test/test_printpreview.py b/src/silx/gui/widgets/test/test_printpreview.py
index 8602666..e88853b 100644
--- a/src/silx/gui/widgets/test/test_printpreview.py
+++ b/src/silx/gui/widgets/test/test_printpreview.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017 European Synchrotron Radiation Facility
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "19/07/2017"
-import unittest
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.widgets.PrintPreview import PrintPreviewDialog
from silx.gui import qt
@@ -53,11 +51,17 @@ class TestPrintPreview(TestCaseQt):
def testAddSvg(self):
p = qt.QPrinter()
d = PrintPreviewDialog(printer=p)
- d.addSvgItem(qt.QSvgRenderer(resource_filename("gui/icons/clipboard.svg"), d.page))
+ d.addSvgItem(
+ qt.QSvgRenderer(resource_filename("gui/icons/clipboard.svg"), d.page)
+ )
self.qapp.processEvents()
def testAddPixmap(self):
p = qt.QPrinter()
d = PrintPreviewDialog(printer=p)
- d.addPixmap(qt.QPixmap.fromImage(qt.QImage(resource_filename("gui/icons/clipboard.png"))))
+ d.addPixmap(
+ qt.QPixmap.fromImage(
+ qt.QImage(resource_filename("gui/icons/clipboard.png"))
+ )
+ )
self.qapp.processEvents()
diff --git a/src/silx/gui/widgets/test/test_rangeslider.py b/src/silx/gui/widgets/test/test_rangeslider.py
index f829857..a59315b 100644
--- a/src/silx/gui/widgets/test/test_rangeslider.py
+++ b/src/silx/gui/widgets/test/test_rangeslider.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2018 European Synchrotron Radiation Facility
@@ -28,8 +27,6 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "01/08/2018"
-import unittest
-
from silx.gui import qt, colors
from silx.gui.widgets.RangeSlider import RangeSlider
from silx.gui.utils.testutils import TestCaseQt
@@ -55,26 +52,26 @@ class TestRangeSlider(TestCaseQt, ParametricTestCase):
# Play with range
self.slider.setRange(1, 2)
- self.assertEqual(self.slider.getRange(), (1., 2.))
- self.assertEqual(self.slider.getValues(), (1., 1.))
+ self.assertEqual(self.slider.getRange(), (1.0, 2.0))
+ self.assertEqual(self.slider.getValues(), (1.0, 1.0))
self.slider.setMinimum(-1)
- self.assertEqual(self.slider.getRange(), (-1., 2.))
- self.assertEqual(self.slider.getValues(), (1., 1.))
+ self.assertEqual(self.slider.getRange(), (-1.0, 2.0))
+ self.assertEqual(self.slider.getValues(), (1.0, 1.0))
self.slider.setMaximum(0)
- self.assertEqual(self.slider.getRange(), (-1., 0.))
- self.assertEqual(self.slider.getValues(), (0., 0.))
+ self.assertEqual(self.slider.getRange(), (-1.0, 0.0))
+ self.assertEqual(self.slider.getValues(), (0.0, 0.0))
# Play with values
- self.slider.setFirstValue(-2.)
- self.assertEqual(self.slider.getValues(), (-1., 0.))
+ self.slider.setFirstValue(-2.0)
+ self.assertEqual(self.slider.getValues(), (-1.0, 0.0))
self.slider.setFirstValue(-0.5)
- self.assertEqual(self.slider.getValues(), (-0.5, 0.))
+ self.assertEqual(self.slider.getValues(), (-0.5, 0.0))
- self.slider.setSecondValue(2.)
- self.assertEqual(self.slider.getValues(), (-0.5, 0.))
+ self.slider.setSecondValue(2.0)
+ self.assertEqual(self.slider.getValues(), (-0.5, 0.0))
self.slider.setSecondValue(-0.1)
self.assertEqual(self.slider.getValues(), (-0.5, -0.1))
@@ -88,14 +85,14 @@ class TestRangeSlider(TestCaseQt, ParametricTestCase):
self.assertEqual(self.slider.getFirstPosition(), 3)
self.slider.setPositionCount(3) # Value is adjusted
- self.assertEqual(self.slider.getValues(), (0.5, 1.))
+ self.assertEqual(self.slider.getValues(), (0.5, 1.0))
self.assertEqual(self.slider.getPositions(), (1, 2))
def testGroove(self):
"""Test Groove pixmap"""
profile = list(range(100))
- for cmap in ('jet', colors.Colormap('viridis')):
+ for cmap in ("jet", colors.Colormap("viridis")):
with self.subTest(str(cmap)):
self.slider.setGroovePixmapFromProfile(profile, cmap)
pixmap = self.slider.getGroovePixmap()
diff --git a/src/silx/gui/widgets/test/test_stackedprogressbar.py b/src/silx/gui/widgets/test/test_stackedprogressbar.py
new file mode 100644
index 0000000..17267b9
--- /dev/null
+++ b/src/silx/gui/widgets/test/test_stackedprogressbar.py
@@ -0,0 +1,60 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Tests for StackedProgressBar"""
+
+__license__ = "MIT"
+
+import pytest
+from silx.gui import qt
+from silx.gui.widgets.StackedProgressBar import StackedProgressBar
+
+
+@pytest.fixture
+def stackedProgressBar(qWidgetFactory):
+ yield qWidgetFactory(StackedProgressBar)
+
+
+def test_show(qapp_utils, stackedProgressBar: StackedProgressBar):
+ pass
+
+
+def test_value(qapp_utils, stackedProgressBar: StackedProgressBar):
+ stackedProgressBar.setRange(0, 100)
+ stackedProgressBar.setProgressItem("foo", value=0)
+ stackedProgressBar.setProgressItem("foo", value=50)
+ stackedProgressBar.setProgressItem("foo", value=100)
+
+
+def test_animation(qapp_utils, stackedProgressBar: StackedProgressBar):
+ stackedProgressBar.setRange(0, 100)
+ stackedProgressBar.setProgressItem("foo", value=0, striped=True, animated=True)
+ stackedProgressBar.setProgressItem("foo", value=50)
+ stackedProgressBar.setProgressItem("foo", value=100)
+
+
+def test_stack(qapp_utils, stackedProgressBar: StackedProgressBar):
+ stackedProgressBar.setRange(0, 100)
+ stackedProgressBar.setProgressItem("foo1", value=10, color=qt.QColor("#FF0000"))
+ stackedProgressBar.setProgressItem("foo2", value=50, color=qt.QColor("#00FF00"))
+ stackedProgressBar.setProgressItem("foo3", value=20, color=qt.QColor("#0000FF"))
diff --git a/src/silx/gui/widgets/test/test_tablewidget.py b/src/silx/gui/widgets/test/test_tablewidget.py
index 09122ca..d631e45 100644
--- a/src/silx/gui/widgets/test/test_tablewidget.py
+++ b/src/silx/gui/widgets/test/test_tablewidget.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016 European Synchrotron Radiation Facility
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "05/12/2016"
-import unittest
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.widgets.TableWidget import TableWidget
diff --git a/src/silx/gui/widgets/test/test_threadpoolpushbutton.py b/src/silx/gui/widgets/test/test_threadpoolpushbutton.py
index 3808be0..cc0b0c5 100644
--- a/src/silx/gui/widgets/test/test_threadpoolpushbutton.py
+++ b/src/silx/gui/widgets/test/test_threadpoolpushbutton.py
@@ -1,4 +1,3 @@
-# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2016-2021 European Synchrotron Radiation Facility
@@ -29,7 +28,6 @@ __license__ = "MIT"
__date__ = "17/01/2018"
-import unittest
import time
from silx.gui import qt
from silx.gui.utils.testutils import TestCaseQt
@@ -39,7 +37,6 @@ from silx.utils.testutils import LoggingValidator
class TestThreadPoolPushButton(TestCaseQt):
-
def setUp(self):
super(TestThreadPoolPushButton, self).setUp()
self._result = []
@@ -114,7 +111,7 @@ class TestThreadPoolPushButton(TestCaseQt):
button.succeeded.connect(listener.partial(test="Unexpected success"))
button.failed.connect(listener.partial(test="exception"))
button.finished.connect(listener.partial(test="f"))
- with LoggingValidator('silx.gui.widgets.ThreadPoolPushButton', error=1):
+ with LoggingValidator("silx.gui.widgets.ThreadPoolPushButton", error=1):
button.executeCallable()
self.qapp.processEvents()
time.sleep(0.1)
diff --git a/src/silx/gui/widgets/test/test_urlselectiontable.py b/src/silx/gui/widgets/test/test_urlselectiontable.py
new file mode 100644
index 0000000..dd75f08
--- /dev/null
+++ b/src/silx/gui/widgets/test/test_urlselectiontable.py
@@ -0,0 +1,72 @@
+# /*##########################################################################
+#
+# Copyright (c) 2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Tests for UrlSelectionTable"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "09/05/2023"
+
+import pytest
+import weakref
+from silx.gui.widgets.UrlSelectionTable import UrlSelectionTable
+from silx.gui import qt
+from silx.io.url import DataUrl
+
+
+@pytest.fixture
+def urlSelectionTable(qapp, qapp_utils):
+ widget = UrlSelectionTable()
+ widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ yield widget
+ widget.close()
+ ref = weakref.ref(widget)
+ widget = None
+ qapp_utils.qWaitForDestroy(ref)
+
+
+def test_show(qapp_utils, urlSelectionTable):
+ qapp_utils.qWaitForWindowExposed(urlSelectionTable)
+
+
+def test_add_urls(urlSelectionTable):
+ urlSelectionTable.addUrl(DataUrl("aaaa"))
+ urlSelectionTable.addUrl(DataUrl("bbbb"))
+ urlSelectionTable.addUrl(DataUrl("cccc"))
+ assert urlSelectionTable.rowCount() == 3
+
+
+def test_clear(urlSelectionTable):
+ urlSelectionTable.addUrl(DataUrl("aaaa"))
+ assert urlSelectionTable.rowCount() == 1
+ urlSelectionTable.clear()
+ assert urlSelectionTable.rowCount() == 0
+
+
+def test_set_remove_error(urlSelectionTable):
+ urlSelectionTable.addUrl(DataUrl("aaaa"))
+ item = urlSelectionTable._getItemFromUrlPath("aaaa")
+ urlSelectionTable.setError("aaaa", "Oh... no...")
+ assert not item.icon().isNull()
+ urlSelectionTable.setError("aaaa", "")
+ assert item.icon().isNull()
diff --git a/src/silx/gui/widgets/test/test_waitingoverlay.py b/src/silx/gui/widgets/test/test_waitingoverlay.py
new file mode 100644
index 0000000..713c4cb
--- /dev/null
+++ b/src/silx/gui/widgets/test/test_waitingoverlay.py
@@ -0,0 +1,31 @@
+import pytest
+from silx.gui import qt
+from silx.gui.widgets.WaitingOverlay import WaitingOverlay
+from silx.gui.plot import Plot2D
+from silx.gui.plot.PlotWidget import PlotWidget
+
+
+@pytest.mark.parametrize("widget_parent", (Plot2D, qt.QFrame))
+def test_show(qapp, qapp_utils, widget_parent):
+ """Simple test of the WaitingOverlay component"""
+ widget = widget_parent()
+ widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+
+ waitingOverlay = WaitingOverlay(widget)
+ waitingOverlay.setAttribute(qt.Qt.WA_DeleteOnClose)
+
+ widget.show()
+ qapp_utils.qWaitForWindowExposed(widget)
+ assert waitingOverlay._waitingButton.isWaiting()
+
+ waitingOverlay.setText("test")
+ qapp.processEvents()
+ assert waitingOverlay.text() == "test"
+ qapp_utils.qWait(1000)
+
+ waitingOverlay.hide()
+ qapp.processEvents()
+
+ widget.close()
+ waitingOverlay.close()
+ qapp.processEvents()