From d4e8e2b8837b8b7232d3b37ddff82f1218050c9a Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 1 Mar 2012 21:55:32 -0500 Subject: [PATCH 001/238] Imported major changes from acq4 project. --- GradientWidget.py | 452 - GraphicsScene.py | 747 ++ GraphicsScene.pyc | Bin 0 -> 27489 bytes ImageViewTemplate.ui | 283 - PlotItem.py | 1284 --- Point.py | 17 +- Point.pyc | Bin 0 -> 6601 bytes Qt.py | 5 + Qt.pyc | Bin 0 -> 367 bytes SignalProxy.py | 76 +- SignalProxy.pyc | Bin 0 -> 3604 bytes Transform.py | 6 +- Transform.pyc | Bin 0 -> 9368 bytes WidgetGroup.py | 282 + WidgetGroup.pyc | Bin 0 -> 9737 bytes __init__.py | 81 +- __init__.pyc | Bin 0 -> 3585 bytes canvas/Canvas.py | 554 ++ canvas/CanvasItem.py | 490 + canvas/CanvasManager.py | 76 + canvas/CanvasTemplate.py | 96 + canvas/CanvasTemplate.ui | 142 + canvas/TransformGuiTemplate.py | 53 + canvas/TransformGuiTemplate.ui | 64 + canvas/__init__.py | 3 + debug.py | 2 +- debug.pyc | Bin 0 -> 29902 bytes dockarea/Container.py | 267 + dockarea/Dock.py | 350 + dockarea/DockArea.py | 267 + dockarea/DockDrop.py | 129 + dockarea/__init__.py | 2 + dockarea/__main__.py | 83 + documentation/Makefile | 130 + .../build/doctrees/apireference.doctree | Bin 0 -> 3040 bytes .../build/doctrees/environment.pickle | Bin 0 -> 77056 bytes .../build/doctrees/functions.doctree | Bin 0 -> 54676 bytes .../doctrees/graphicsItems/arrowitem.doctree | Bin 0 -> 5249 bytes .../doctrees/graphicsItems/axisitem.doctree | Bin 0 -> 11126 bytes .../doctrees/graphicsItems/buttonitem.doctree | Bin 0 -> 5775 bytes .../doctrees/graphicsItems/curvearrow.doctree | Bin 0 -> 6107 bytes .../doctrees/graphicsItems/curvepoint.doctree | Bin 0 -> 6727 bytes .../graphicsItems/gradienteditoritem.doctree | Bin 0 -> 6649 bytes .../graphicsItems/gradientlegend.doctree | Bin 0 -> 7223 bytes .../graphicsItems/graphicslayout.doctree | Bin 0 -> 8137 bytes .../graphicsItems/graphicsobject.doctree | Bin 0 -> 17301 bytes .../graphicsItems/graphicswidget.doctree | Bin 0 -> 5637 bytes .../doctrees/graphicsItems/griditem.doctree | Bin 0 -> 4913 bytes .../graphicsItems/histogramlutitem.doctree | Bin 0 -> 4895 bytes .../doctrees/graphicsItems/imageitem.doctree | Bin 0 -> 16931 bytes .../doctrees/graphicsItems/index.doctree | Bin 0 -> 4809 bytes .../graphicsItems/infiniteline.doctree | Bin 0 -> 11533 bytes .../doctrees/graphicsItems/labelitem.doctree | Bin 0 -> 10554 bytes .../graphicsItems/linearregionitem.doctree | Bin 0 -> 8537 bytes .../graphicsItems/plotcurveitem.doctree | Bin 0 -> 9555 bytes .../graphicsItems/plotdataitem.doctree | Bin 0 -> 30382 bytes .../doctrees/graphicsItems/plotitem.doctree | Bin 0 -> 31151 bytes .../build/doctrees/graphicsItems/roi.doctree | Bin 0 -> 21560 bytes .../doctrees/graphicsItems/scalebar.doctree | Bin 0 -> 6287 bytes .../graphicsItems/scatterplotitem.doctree | Bin 0 -> 17644 bytes .../graphicsItems/uigraphicsitem.doctree | Bin 0 -> 15483 bytes .../doctrees/graphicsItems/viewbox.doctree | Bin 0 -> 27898 bytes .../doctrees/graphicsItems/vtickgroup.doctree | Bin 0 -> 5951 bytes .../build/doctrees/graphicswindow.doctree | Bin 0 -> 3501 bytes .../build/doctrees/how_to_use.doctree | Bin 0 -> 9236 bytes documentation/build/doctrees/images.doctree | Bin 0 -> 11808 bytes documentation/build/doctrees/index.doctree | Bin 0 -> 6042 bytes .../build/doctrees/introduction.doctree | Bin 0 -> 13863 bytes .../build/doctrees/parametertree.doctree | Bin 0 -> 3458 bytes documentation/build/doctrees/plotting.doctree | Bin 0 -> 26167 bytes .../build/doctrees/region_of_interest.doctree | Bin 0 -> 4508 bytes documentation/build/doctrees/style.doctree | Bin 0 -> 5421 bytes .../build/doctrees/widgets/checktable.doctree | Bin 0 -> 4775 bytes .../doctrees/widgets/colorbutton.doctree | Bin 0 -> 5607 bytes .../doctrees/widgets/datatreewidget.doctree | Bin 0 -> 6995 bytes .../build/doctrees/widgets/dockarea.doctree | Bin 0 -> 2862 bytes .../build/doctrees/widgets/filedialog.doctree | Bin 0 -> 4763 bytes .../doctrees/widgets/gradientwidget.doctree | Bin 0 -> 5742 bytes .../widgets/graphicslayoutwidget.doctree | Bin 0 -> 5215 bytes .../doctrees/widgets/graphicsview.doctree | Bin 0 -> 12436 bytes .../widgets/histogramlutwidget.doctree | Bin 0 -> 5455 bytes .../build/doctrees/widgets/imageview.doctree | Bin 0 -> 12149 bytes .../build/doctrees/widgets/index.doctree | Bin 0 -> 4236 bytes .../doctrees/widgets/joystickbutton.doctree | Bin 0 -> 4859 bytes .../doctrees/widgets/multiplotwidget.doctree | Bin 0 -> 5295 bytes .../doctrees/widgets/parametertree.doctree | Bin 0 -> 2912 bytes .../build/doctrees/widgets/plotwidget.doctree | Bin 0 -> 5476 bytes .../doctrees/widgets/progressdialog.doctree | Bin 0 -> 10374 bytes .../doctrees/widgets/rawimagewidget.doctree | Bin 0 -> 7770 bytes .../build/doctrees/widgets/spinbox.doctree | Bin 0 -> 12210 bytes .../doctrees/widgets/tablewidget.doctree | Bin 0 -> 11304 bytes .../build/doctrees/widgets/treewidget.doctree | Bin 0 -> 7154 bytes .../doctrees/widgets/verticallabel.doctree | Bin 0 -> 5483 bytes documentation/build/html/.buildinfo | 4 + .../build/html/_images/plottingClasses.png | Bin 0 -> 68667 bytes documentation/build/html/_modules/index.html | 89 + .../build/html/_modules/pyqtgraph.html | 192 + .../build/html/_sources/apireference.txt | 11 + .../build/html/_sources/functions.txt | 53 + .../html/_sources/graphicsItems/arrowitem.txt | 8 + .../html/_sources/graphicsItems/axisitem.txt | 8 + .../_sources/graphicsItems/buttonitem.txt | 8 + .../_sources/graphicsItems/curvearrow.txt | 8 + .../_sources/graphicsItems/curvepoint.txt | 8 + .../graphicsItems/gradienteditoritem.txt | 8 + .../_sources/graphicsItems/gradientlegend.txt | 8 + .../_sources/graphicsItems/graphicslayout.txt | 8 + .../_sources/graphicsItems/graphicsobject.txt | 8 + .../_sources/graphicsItems/graphicswidget.txt | 8 + .../html/_sources/graphicsItems/griditem.txt | 8 + .../graphicsItems/histogramlutitem.txt | 8 + .../html/_sources/graphicsItems/imageitem.txt | 8 + .../html/_sources/graphicsItems/index.txt | 37 + .../_sources/graphicsItems/infiniteline.txt | 8 + .../html/_sources/graphicsItems/labelitem.txt | 8 + .../graphicsItems/linearregionitem.txt | 8 + .../_sources/graphicsItems/plotcurveitem.txt | 8 + .../_sources/graphicsItems/plotdataitem.txt | 8 + .../html/_sources/graphicsItems/plotitem.txt | 7 + .../build/html/_sources/graphicsItems/roi.txt | 8 + .../html/_sources/graphicsItems/scalebar.txt | 8 + .../graphicsItems/scatterplotitem.txt | 8 + .../_sources/graphicsItems/uigraphicsitem.txt | 8 + .../html/_sources/graphicsItems/viewbox.txt | 8 + .../_sources/graphicsItems/vtickgroup.txt | 8 + .../build/html/_sources/graphicswindow.txt | 8 + .../build/html/_sources/how_to_use.txt | 47 + documentation/build/html/_sources/images.txt | 26 + documentation/build/html/_sources/index.txt | 32 + .../build/html/_sources/introduction.txt | 51 + .../build/html/_sources/parametertree.txt | 7 + .../build/html/_sources/plotting.txt | 73 + .../html/_sources/region_of_interest.txt | 19 + documentation/build/html/_sources/style.txt | 17 + .../html/_sources/widgets/checktable.txt | 8 + .../html/_sources/widgets/colorbutton.txt | 8 + .../html/_sources/widgets/datatreewidget.txt | 8 + .../build/html/_sources/widgets/dockarea.txt | 5 + .../html/_sources/widgets/filedialog.txt | 8 + .../html/_sources/widgets/gradientwidget.txt | 8 + .../_sources/widgets/graphicslayoutwidget.txt | 8 + .../html/_sources/widgets/graphicsview.txt | 8 + .../_sources/widgets/histogramlutwidget.txt | 8 + .../build/html/_sources/widgets/imageview.txt | 8 + .../build/html/_sources/widgets/index.txt | 31 + .../html/_sources/widgets/joystickbutton.txt | 8 + .../html/_sources/widgets/multiplotwidget.txt | 8 + .../html/_sources/widgets/parametertree.txt | 5 + .../html/_sources/widgets/plotwidget.txt | 8 + .../html/_sources/widgets/progressdialog.txt | 8 + .../html/_sources/widgets/rawimagewidget.txt | 8 + .../build/html/_sources/widgets/spinbox.txt | 8 + .../html/_sources/widgets/tablewidget.txt | 8 + .../html/_sources/widgets/treewidget.txt | 8 + .../html/_sources/widgets/verticallabel.txt | 8 + documentation/build/html/_static/basic.css | 509 + documentation/build/html/_static/default.css | 255 + documentation/build/html/_static/doctools.js | 247 + documentation/build/html/_static/file.png | Bin 0 -> 392 bytes documentation/build/html/_static/jquery.js | 8176 +++++++++++++++++ documentation/build/html/_static/minus.png | Bin 0 -> 199 bytes documentation/build/html/_static/plus.png | Bin 0 -> 199 bytes documentation/build/html/_static/pygments.css | 61 + .../build/html/_static/searchtools.js | 518 ++ documentation/build/html/_static/sidebar.js | 147 + .../build/html/_static/underscore.js | 16 + documentation/build/html/apireference.html | 178 + documentation/build/html/functions.html | 341 + documentation/build/html/genindex.html | 457 + .../build/html/graphicsItems/arrowitem.html | 132 + .../build/html/graphicsItems/axisitem.html | 154 + .../build/html/graphicsItems/buttonitem.html | 131 + .../build/html/graphicsItems/curvearrow.html | 132 + .../build/html/graphicsItems/curvepoint.html | 136 + .../graphicsItems/gradienteditoritem.html | 136 + .../html/graphicsItems/gradientlegend.html | 138 + .../html/graphicsItems/graphicslayout.html | 144 + .../html/graphicsItems/graphicsobject.html | 189 + .../html/graphicsItems/graphicswidget.html | 132 + .../build/html/graphicsItems/griditem.html | 132 + .../html/graphicsItems/histogramlutitem.html | 130 + .../build/html/graphicsItems/imageitem.html | 184 + .../build/html/graphicsItems/index.html | 149 + .../html/graphicsItems/infiniteline.html | 155 + .../build/html/graphicsItems/labelitem.html | 154 + .../html/graphicsItems/linearregionitem.html | 138 + .../html/graphicsItems/plotcurveitem.html | 141 + .../html/graphicsItems/plotdataitem.html | 289 + .../build/html/graphicsItems/plotitem.html | 245 + .../build/html/graphicsItems/roi.html | 185 + .../build/html/graphicsItems/scalebar.html | 131 + .../html/graphicsItems/scatterplotitem.html | 171 + .../html/graphicsItems/uigraphicsitem.html | 179 + .../build/html/graphicsItems/viewbox.html | 227 + .../build/html/graphicsItems/vtickgroup.html | 132 + documentation/build/html/graphicswindow.html | 123 + documentation/build/html/how_to_use.html | 161 + documentation/build/html/images.html | 135 + documentation/build/html/index.html | 160 + documentation/build/html/introduction.html | 163 + documentation/build/html/objects.inv | Bin 0 -> 2225 bytes documentation/build/html/parametertree.html | 123 + documentation/build/html/plotting.html | 217 + documentation/build/html/py-modindex.html | 118 + .../build/html/region_of_interest.html | 140 + documentation/build/html/search.html | 102 + documentation/build/html/searchindex.js | 1 + documentation/build/html/style.html | 127 + .../build/html/widgets/checktable.html | 130 + .../build/html/widgets/colorbutton.html | 130 + .../build/html/widgets/datatreewidget.html | 138 + .../build/html/widgets/dockarea.html | 120 + .../build/html/widgets/filedialog.html | 130 + .../build/html/widgets/gradientwidget.html | 130 + .../html/widgets/graphicslayoutwidget.html | 130 + .../build/html/widgets/graphicsview.html | 162 + .../html/widgets/histogramlutwidget.html | 130 + .../build/html/widgets/imageview.html | 158 + documentation/build/html/widgets/index.html | 144 + .../build/html/widgets/joystickbutton.html | 130 + .../build/html/widgets/multiplotwidget.html | 131 + .../build/html/widgets/parametertree.html | 120 + .../build/html/widgets/plotwidget.html | 131 + .../build/html/widgets/progressdialog.html | 153 + .../build/html/widgets/rawimagewidget.html | 132 + documentation/build/html/widgets/spinbox.html | 164 + .../build/html/widgets/tablewidget.html | 168 + .../build/html/widgets/treewidget.html | 140 + .../build/html/widgets/verticallabel.html | 130 + documentation/make.bat | 155 + documentation/source/apireference.rst | 11 + documentation/source/conf.py | 217 + documentation/source/functions.rst | 53 + .../source/graphicsItems/arrowitem.rst | 8 + .../source/graphicsItems/axisitem.rst | 8 + .../source/graphicsItems/buttonitem.rst | 8 + .../source/graphicsItems/curvearrow.rst | 8 + .../source/graphicsItems/curvepoint.rst | 8 + .../graphicsItems/gradienteditoritem.rst | 8 + .../source/graphicsItems/gradientlegend.rst | 8 + .../source/graphicsItems/graphicslayout.rst | 8 + .../source/graphicsItems/graphicsobject.rst | 8 + .../source/graphicsItems/graphicswidget.rst | 8 + .../source/graphicsItems/griditem.rst | 8 + .../source/graphicsItems/histogramlutitem.rst | 8 + .../source/graphicsItems/imageitem.rst | 8 + documentation/source/graphicsItems/index.rst | 37 + .../source/graphicsItems/infiniteline.rst | 8 + .../source/graphicsItems/labelitem.rst | 8 + .../source/graphicsItems/linearregionitem.rst | 8 + documentation/source/graphicsItems/make | 37 + .../source/graphicsItems/plotcurveitem.rst | 8 + .../source/graphicsItems/plotdataitem.rst | 8 + .../source/graphicsItems/plotitem.rst | 7 + documentation/source/graphicsItems/roi.rst | 8 + .../source/graphicsItems/scalebar.rst | 8 + .../source/graphicsItems/scatterplotitem.rst | 8 + .../source/graphicsItems/uigraphicsitem.rst | 8 + .../source/graphicsItems/viewbox.rst | 8 + .../source/graphicsItems/vtickgroup.rst | 8 + documentation/source/graphicswindow.rst | 8 + documentation/source/how_to_use.rst | 47 + documentation/source/images.rst | 26 + .../source/images/plottingClasses.png | Bin 0 -> 68667 bytes .../source/images/plottingClasses.svg | 580 ++ documentation/source/index.rst | 32 + documentation/source/internals.rst | 9 + documentation/source/introduction.rst | 51 + documentation/source/parametertree.rst | 7 + documentation/source/plotting.rst | 73 + documentation/source/region_of_interest.rst | 19 + documentation/source/style.rst | 17 + documentation/source/widgets/checktable.rst | 8 + documentation/source/widgets/colorbutton.rst | 8 + .../source/widgets/datatreewidget.rst | 8 + documentation/source/widgets/dockarea.rst | 5 + documentation/source/widgets/filedialog.rst | 8 + .../source/widgets/gradientwidget.rst | 8 + .../source/widgets/graphicslayoutwidget.rst | 8 + documentation/source/widgets/graphicsview.rst | 8 + .../source/widgets/histogramlutwidget.rst | 8 + documentation/source/widgets/imageview.rst | 8 + documentation/source/widgets/index.rst | 31 + .../source/widgets/joystickbutton.rst | 8 + documentation/source/widgets/make | 31 + .../source/widgets/multiplotwidget.rst | 8 + .../source/widgets/parametertree.rst | 5 + documentation/source/widgets/plotwidget.rst | 8 + .../source/widgets/progressdialog.rst | 8 + .../source/widgets/rawimagewidget.rst | 8 + documentation/source/widgets/spinbox.rst | 8 + documentation/source/widgets/tablewidget.rst | 8 + documentation/source/widgets/treewidget.rst | 8 + .../source/widgets/verticallabel.rst | 8 + examples/{test_Arrow.py => Arrow.py} | 5 +- examples/CLIexample.py | 22 + examples/DataSlicing.py | 55 + examples/{test_draw.py => Draw.py} | 11 +- examples/Flowchart.py | 61 + examples/GradientEditor.py | 27 + examples/GraphicsLayout.py | 46 + examples/GraphicsScene.py | 65 + examples/HistogramLUT.py | 49 + examples/{test_ImageItem.py => ImageItem.py} | 45 +- examples/{test_ImageView.py => ImageView.py} | 0 ..._MultiPlotWidget.py => MultiPlotWidget.py} | 2 +- examples/PlotSpeedTest.py | 46 + .../{test_PlotWidget.py => PlotWidget.py} | 20 +- examples/Plotting.py | 72 + examples/{test_ROItypes.py => ROItypes.py} | 77 +- examples/ScatterPlot.py | 87 + examples/VideoSpeedTest.py | 139 + examples/VideoTemplate.py | 149 + examples/VideoTemplate.ui | 250 + examples/{test_viewBox.py => ViewBox.py} | 27 +- examples/__init__.py | 1 + examples/__main__.py | 101 + examples/exampleLoaderTemplate.py | 55 + examples/exampleLoaderTemplate.ui | 65 + examples/test_scatterPlot.py | 82 - flowchart/Flowchart.py | 920 ++ flowchart/FlowchartCtrlTemplate.py | 71 + flowchart/FlowchartCtrlTemplate.ui | 120 + flowchart/FlowchartGraphicsView.py | 109 + flowchart/FlowchartTemplate.py | 59 + flowchart/FlowchartTemplate.ui | 98 + flowchart/Node.py | 561 ++ flowchart/Terminal.py | 555 ++ flowchart/__init__.py | 4 + flowchart/eq.py | 29 + flowchart/library/Data.py | 352 + flowchart/library/Display.py | 245 + flowchart/library/EventDetection.py | 187 + flowchart/library/Filters.py | 245 + flowchart/library/Operators.py | 64 + flowchart/library/__init__.py | 100 + flowchart/library/common.py | 148 + functions.py | 554 +- functions.pyc | Bin 0 -> 19701 bytes graphicsItems.py | 2997 ------ graphicsItems/ArrowItem.py | 60 + graphicsItems/AxisItem.py | 441 + graphicsItems/ButtonItem.py | 51 + graphicsItems/CurvePoint.py | 113 + graphicsItems/GradientEditorItem.py | 624 ++ graphicsItems/GradientLegend.py | 112 + graphicsItems/GraphicsItemMethods.py | 256 + graphicsItems/GraphicsLayout.py | 97 + graphicsItems/GraphicsObject.py | 19 + graphicsItems/GraphicsWidget.py | 44 + graphicsItems/GridItem.py | 116 + graphicsItems/HistogramLUTItem.py | 178 + graphicsItems/ImageItem.old | 398 + graphicsItems/ImageItem.py | 537 ++ graphicsItems/InfiniteLine.py | 255 + graphicsItems/ItemGroup.py | 23 + graphicsItems/LabelItem.py | 91 + graphicsItems/LinearRegionItem.py | 232 + .../MultiPlotItem.py | 30 +- graphicsItems/PlotCurveItem.py | 444 + graphicsItems/PlotDataItem.py | 534 ++ graphicsItems/PlotItem/PlotItem.py | 1389 +++ graphicsItems/PlotItem/__init__.py | 1 + graphicsItems/PlotItem/auto.png | Bin 0 -> 1022 bytes graphicsItems/PlotItem/ctrl.png | Bin 0 -> 934 bytes graphicsItems/PlotItem/icons.svg | 135 + graphicsItems/PlotItem/lock.png | Bin 0 -> 913 bytes graphicsItems/PlotItem/plotConfigTemplate.py | 130 + graphicsItems/PlotItem/plotConfigTemplate.ui | 258 + widgets.py => graphicsItems/ROI.py | 717 +- graphicsItems/ScaleBar.py | 50 + graphicsItems/ScatterPlotItem.py | 377 + graphicsItems/UIGraphicsItem.py | 127 + graphicsItems/VTickGroup.py | 154 + graphicsItems/ViewBox.pyc.renamed1 | Bin 0 -> 22064 bytes graphicsItems/ViewBox/ViewBox.py | 978 ++ graphicsItems/ViewBox/ViewBoxMenu.py | 222 + graphicsItems/ViewBox/__init__.py | 1 + graphicsItems/ViewBox/axisCtrlTemplate.py | 73 + graphicsItems/ViewBox/axisCtrlTemplate.ui | 110 + graphicsItems/__init__.py | 21 + graphicsWindows.py | 62 +- graphicsWindows.pyc | Bin 0 -> 4242 bytes ImageView.py => imageview/ImageView.py | 210 +- .../ImageViewTemplate.py | 69 +- imageview/ImageViewTemplate.ui | 252 + imageview/__init__.py | 6 + parametertree/Parameter.py | 465 + parametertree/ParameterItem.py | 148 + parametertree/ParameterTree.py | 108 + parametertree/__init__.py | 5 + parametertree/__main__.py | 140 + parametertree/default.png | Bin 0 -> 810 bytes parametertree/parameterTypes.py | 480 + plotConfigTemplate.py | 295 - plotConfigTemplate.ui | 563 -- ptime.pyc | Bin 0 -> 1273 bytes widgets/CheckTable.py | 89 + ColorButton.py => widgets/ColorButton.py | 22 +- widgets/DataTreeWidget.py | 106 + widgets/FileDialog.py | 14 + widgets/GradientWidget.py | 620 ++ widgets/GraphicsLayoutWidget.py | 12 + GraphicsView.py => widgets/GraphicsView.py | 170 +- widgets/HistogramLUTWidget.py | 33 + widgets/JoystickButton.py | 90 + .../MultiPlotWidget.py | 7 +- PlotWidget.py => widgets/PlotWidget.py | 4 +- widgets/ProgressDialog.py | 105 + widgets/RawImageWidget.py | 79 + widgets/SpinBox.py | 481 + widgets/TableWidget.py | 249 + widgets/TreeWidget.py | 194 + widgets/VerticalLabel.py | 99 + widgets/__init__.py | 21 + 415 files changed, 45250 insertions(+), 6650 deletions(-) delete mode 100644 GradientWidget.py create mode 100644 GraphicsScene.py create mode 100644 GraphicsScene.pyc delete mode 100644 ImageViewTemplate.ui delete mode 100644 PlotItem.py create mode 100644 Point.pyc create mode 100644 Qt.py create mode 100644 Qt.pyc create mode 100644 SignalProxy.pyc create mode 100644 Transform.pyc create mode 100644 WidgetGroup.py create mode 100644 WidgetGroup.pyc create mode 100644 __init__.pyc create mode 100644 canvas/Canvas.py create mode 100644 canvas/CanvasItem.py create mode 100644 canvas/CanvasManager.py create mode 100644 canvas/CanvasTemplate.py create mode 100644 canvas/CanvasTemplate.ui create mode 100644 canvas/TransformGuiTemplate.py create mode 100644 canvas/TransformGuiTemplate.ui create mode 100644 canvas/__init__.py create mode 100644 debug.pyc create mode 100644 dockarea/Container.py create mode 100644 dockarea/Dock.py create mode 100644 dockarea/DockArea.py create mode 100644 dockarea/DockDrop.py create mode 100644 dockarea/__init__.py create mode 100644 dockarea/__main__.py create mode 100644 documentation/Makefile create mode 100644 documentation/build/doctrees/apireference.doctree create mode 100644 documentation/build/doctrees/environment.pickle create mode 100644 documentation/build/doctrees/functions.doctree create mode 100644 documentation/build/doctrees/graphicsItems/arrowitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/axisitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/buttonitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/curvearrow.doctree create mode 100644 documentation/build/doctrees/graphicsItems/curvepoint.doctree create mode 100644 documentation/build/doctrees/graphicsItems/gradienteditoritem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/gradientlegend.doctree create mode 100644 documentation/build/doctrees/graphicsItems/graphicslayout.doctree create mode 100644 documentation/build/doctrees/graphicsItems/graphicsobject.doctree create mode 100644 documentation/build/doctrees/graphicsItems/graphicswidget.doctree create mode 100644 documentation/build/doctrees/graphicsItems/griditem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/histogramlutitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/imageitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/index.doctree create mode 100644 documentation/build/doctrees/graphicsItems/infiniteline.doctree create mode 100644 documentation/build/doctrees/graphicsItems/labelitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/linearregionitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/plotcurveitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/plotdataitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/plotitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/roi.doctree create mode 100644 documentation/build/doctrees/graphicsItems/scalebar.doctree create mode 100644 documentation/build/doctrees/graphicsItems/scatterplotitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/uigraphicsitem.doctree create mode 100644 documentation/build/doctrees/graphicsItems/viewbox.doctree create mode 100644 documentation/build/doctrees/graphicsItems/vtickgroup.doctree create mode 100644 documentation/build/doctrees/graphicswindow.doctree create mode 100644 documentation/build/doctrees/how_to_use.doctree create mode 100644 documentation/build/doctrees/images.doctree create mode 100644 documentation/build/doctrees/index.doctree create mode 100644 documentation/build/doctrees/introduction.doctree create mode 100644 documentation/build/doctrees/parametertree.doctree create mode 100644 documentation/build/doctrees/plotting.doctree create mode 100644 documentation/build/doctrees/region_of_interest.doctree create mode 100644 documentation/build/doctrees/style.doctree create mode 100644 documentation/build/doctrees/widgets/checktable.doctree create mode 100644 documentation/build/doctrees/widgets/colorbutton.doctree create mode 100644 documentation/build/doctrees/widgets/datatreewidget.doctree create mode 100644 documentation/build/doctrees/widgets/dockarea.doctree create mode 100644 documentation/build/doctrees/widgets/filedialog.doctree create mode 100644 documentation/build/doctrees/widgets/gradientwidget.doctree create mode 100644 documentation/build/doctrees/widgets/graphicslayoutwidget.doctree create mode 100644 documentation/build/doctrees/widgets/graphicsview.doctree create mode 100644 documentation/build/doctrees/widgets/histogramlutwidget.doctree create mode 100644 documentation/build/doctrees/widgets/imageview.doctree create mode 100644 documentation/build/doctrees/widgets/index.doctree create mode 100644 documentation/build/doctrees/widgets/joystickbutton.doctree create mode 100644 documentation/build/doctrees/widgets/multiplotwidget.doctree create mode 100644 documentation/build/doctrees/widgets/parametertree.doctree create mode 100644 documentation/build/doctrees/widgets/plotwidget.doctree create mode 100644 documentation/build/doctrees/widgets/progressdialog.doctree create mode 100644 documentation/build/doctrees/widgets/rawimagewidget.doctree create mode 100644 documentation/build/doctrees/widgets/spinbox.doctree create mode 100644 documentation/build/doctrees/widgets/tablewidget.doctree create mode 100644 documentation/build/doctrees/widgets/treewidget.doctree create mode 100644 documentation/build/doctrees/widgets/verticallabel.doctree create mode 100644 documentation/build/html/.buildinfo create mode 100644 documentation/build/html/_images/plottingClasses.png create mode 100644 documentation/build/html/_modules/index.html create mode 100644 documentation/build/html/_modules/pyqtgraph.html create mode 100644 documentation/build/html/_sources/apireference.txt create mode 100644 documentation/build/html/_sources/functions.txt create mode 100644 documentation/build/html/_sources/graphicsItems/arrowitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/axisitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/buttonitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/curvearrow.txt create mode 100644 documentation/build/html/_sources/graphicsItems/curvepoint.txt create mode 100644 documentation/build/html/_sources/graphicsItems/gradienteditoritem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/gradientlegend.txt create mode 100644 documentation/build/html/_sources/graphicsItems/graphicslayout.txt create mode 100644 documentation/build/html/_sources/graphicsItems/graphicsobject.txt create mode 100644 documentation/build/html/_sources/graphicsItems/graphicswidget.txt create mode 100644 documentation/build/html/_sources/graphicsItems/griditem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/histogramlutitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/imageitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/index.txt create mode 100644 documentation/build/html/_sources/graphicsItems/infiniteline.txt create mode 100644 documentation/build/html/_sources/graphicsItems/labelitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/linearregionitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/plotcurveitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/plotdataitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/plotitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/roi.txt create mode 100644 documentation/build/html/_sources/graphicsItems/scalebar.txt create mode 100644 documentation/build/html/_sources/graphicsItems/scatterplotitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/uigraphicsitem.txt create mode 100644 documentation/build/html/_sources/graphicsItems/viewbox.txt create mode 100644 documentation/build/html/_sources/graphicsItems/vtickgroup.txt create mode 100644 documentation/build/html/_sources/graphicswindow.txt create mode 100644 documentation/build/html/_sources/how_to_use.txt create mode 100644 documentation/build/html/_sources/images.txt create mode 100644 documentation/build/html/_sources/index.txt create mode 100644 documentation/build/html/_sources/introduction.txt create mode 100644 documentation/build/html/_sources/parametertree.txt create mode 100644 documentation/build/html/_sources/plotting.txt create mode 100644 documentation/build/html/_sources/region_of_interest.txt create mode 100644 documentation/build/html/_sources/style.txt create mode 100644 documentation/build/html/_sources/widgets/checktable.txt create mode 100644 documentation/build/html/_sources/widgets/colorbutton.txt create mode 100644 documentation/build/html/_sources/widgets/datatreewidget.txt create mode 100644 documentation/build/html/_sources/widgets/dockarea.txt create mode 100644 documentation/build/html/_sources/widgets/filedialog.txt create mode 100644 documentation/build/html/_sources/widgets/gradientwidget.txt create mode 100644 documentation/build/html/_sources/widgets/graphicslayoutwidget.txt create mode 100644 documentation/build/html/_sources/widgets/graphicsview.txt create mode 100644 documentation/build/html/_sources/widgets/histogramlutwidget.txt create mode 100644 documentation/build/html/_sources/widgets/imageview.txt create mode 100644 documentation/build/html/_sources/widgets/index.txt create mode 100644 documentation/build/html/_sources/widgets/joystickbutton.txt create mode 100644 documentation/build/html/_sources/widgets/multiplotwidget.txt create mode 100644 documentation/build/html/_sources/widgets/parametertree.txt create mode 100644 documentation/build/html/_sources/widgets/plotwidget.txt create mode 100644 documentation/build/html/_sources/widgets/progressdialog.txt create mode 100644 documentation/build/html/_sources/widgets/rawimagewidget.txt create mode 100644 documentation/build/html/_sources/widgets/spinbox.txt create mode 100644 documentation/build/html/_sources/widgets/tablewidget.txt create mode 100644 documentation/build/html/_sources/widgets/treewidget.txt create mode 100644 documentation/build/html/_sources/widgets/verticallabel.txt create mode 100644 documentation/build/html/_static/basic.css create mode 100644 documentation/build/html/_static/default.css create mode 100644 documentation/build/html/_static/doctools.js create mode 100644 documentation/build/html/_static/file.png create mode 100644 documentation/build/html/_static/jquery.js create mode 100644 documentation/build/html/_static/minus.png create mode 100644 documentation/build/html/_static/plus.png create mode 100644 documentation/build/html/_static/pygments.css create mode 100644 documentation/build/html/_static/searchtools.js create mode 100644 documentation/build/html/_static/sidebar.js create mode 100644 documentation/build/html/_static/underscore.js create mode 100644 documentation/build/html/apireference.html create mode 100644 documentation/build/html/functions.html create mode 100644 documentation/build/html/genindex.html create mode 100644 documentation/build/html/graphicsItems/arrowitem.html create mode 100644 documentation/build/html/graphicsItems/axisitem.html create mode 100644 documentation/build/html/graphicsItems/buttonitem.html create mode 100644 documentation/build/html/graphicsItems/curvearrow.html create mode 100644 documentation/build/html/graphicsItems/curvepoint.html create mode 100644 documentation/build/html/graphicsItems/gradienteditoritem.html create mode 100644 documentation/build/html/graphicsItems/gradientlegend.html create mode 100644 documentation/build/html/graphicsItems/graphicslayout.html create mode 100644 documentation/build/html/graphicsItems/graphicsobject.html create mode 100644 documentation/build/html/graphicsItems/graphicswidget.html create mode 100644 documentation/build/html/graphicsItems/griditem.html create mode 100644 documentation/build/html/graphicsItems/histogramlutitem.html create mode 100644 documentation/build/html/graphicsItems/imageitem.html create mode 100644 documentation/build/html/graphicsItems/index.html create mode 100644 documentation/build/html/graphicsItems/infiniteline.html create mode 100644 documentation/build/html/graphicsItems/labelitem.html create mode 100644 documentation/build/html/graphicsItems/linearregionitem.html create mode 100644 documentation/build/html/graphicsItems/plotcurveitem.html create mode 100644 documentation/build/html/graphicsItems/plotdataitem.html create mode 100644 documentation/build/html/graphicsItems/plotitem.html create mode 100644 documentation/build/html/graphicsItems/roi.html create mode 100644 documentation/build/html/graphicsItems/scalebar.html create mode 100644 documentation/build/html/graphicsItems/scatterplotitem.html create mode 100644 documentation/build/html/graphicsItems/uigraphicsitem.html create mode 100644 documentation/build/html/graphicsItems/viewbox.html create mode 100644 documentation/build/html/graphicsItems/vtickgroup.html create mode 100644 documentation/build/html/graphicswindow.html create mode 100644 documentation/build/html/how_to_use.html create mode 100644 documentation/build/html/images.html create mode 100644 documentation/build/html/index.html create mode 100644 documentation/build/html/introduction.html create mode 100644 documentation/build/html/objects.inv create mode 100644 documentation/build/html/parametertree.html create mode 100644 documentation/build/html/plotting.html create mode 100644 documentation/build/html/py-modindex.html create mode 100644 documentation/build/html/region_of_interest.html create mode 100644 documentation/build/html/search.html create mode 100644 documentation/build/html/searchindex.js create mode 100644 documentation/build/html/style.html create mode 100644 documentation/build/html/widgets/checktable.html create mode 100644 documentation/build/html/widgets/colorbutton.html create mode 100644 documentation/build/html/widgets/datatreewidget.html create mode 100644 documentation/build/html/widgets/dockarea.html create mode 100644 documentation/build/html/widgets/filedialog.html create mode 100644 documentation/build/html/widgets/gradientwidget.html create mode 100644 documentation/build/html/widgets/graphicslayoutwidget.html create mode 100644 documentation/build/html/widgets/graphicsview.html create mode 100644 documentation/build/html/widgets/histogramlutwidget.html create mode 100644 documentation/build/html/widgets/imageview.html create mode 100644 documentation/build/html/widgets/index.html create mode 100644 documentation/build/html/widgets/joystickbutton.html create mode 100644 documentation/build/html/widgets/multiplotwidget.html create mode 100644 documentation/build/html/widgets/parametertree.html create mode 100644 documentation/build/html/widgets/plotwidget.html create mode 100644 documentation/build/html/widgets/progressdialog.html create mode 100644 documentation/build/html/widgets/rawimagewidget.html create mode 100644 documentation/build/html/widgets/spinbox.html create mode 100644 documentation/build/html/widgets/tablewidget.html create mode 100644 documentation/build/html/widgets/treewidget.html create mode 100644 documentation/build/html/widgets/verticallabel.html create mode 100644 documentation/make.bat create mode 100644 documentation/source/apireference.rst create mode 100644 documentation/source/conf.py create mode 100644 documentation/source/functions.rst create mode 100644 documentation/source/graphicsItems/arrowitem.rst create mode 100644 documentation/source/graphicsItems/axisitem.rst create mode 100644 documentation/source/graphicsItems/buttonitem.rst create mode 100644 documentation/source/graphicsItems/curvearrow.rst create mode 100644 documentation/source/graphicsItems/curvepoint.rst create mode 100644 documentation/source/graphicsItems/gradienteditoritem.rst create mode 100644 documentation/source/graphicsItems/gradientlegend.rst create mode 100644 documentation/source/graphicsItems/graphicslayout.rst create mode 100644 documentation/source/graphicsItems/graphicsobject.rst create mode 100644 documentation/source/graphicsItems/graphicswidget.rst create mode 100644 documentation/source/graphicsItems/griditem.rst create mode 100644 documentation/source/graphicsItems/histogramlutitem.rst create mode 100644 documentation/source/graphicsItems/imageitem.rst create mode 100644 documentation/source/graphicsItems/index.rst create mode 100644 documentation/source/graphicsItems/infiniteline.rst create mode 100644 documentation/source/graphicsItems/labelitem.rst create mode 100644 documentation/source/graphicsItems/linearregionitem.rst create mode 100644 documentation/source/graphicsItems/make create mode 100644 documentation/source/graphicsItems/plotcurveitem.rst create mode 100644 documentation/source/graphicsItems/plotdataitem.rst create mode 100644 documentation/source/graphicsItems/plotitem.rst create mode 100644 documentation/source/graphicsItems/roi.rst create mode 100644 documentation/source/graphicsItems/scalebar.rst create mode 100644 documentation/source/graphicsItems/scatterplotitem.rst create mode 100644 documentation/source/graphicsItems/uigraphicsitem.rst create mode 100644 documentation/source/graphicsItems/viewbox.rst create mode 100644 documentation/source/graphicsItems/vtickgroup.rst create mode 100644 documentation/source/graphicswindow.rst create mode 100644 documentation/source/how_to_use.rst create mode 100644 documentation/source/images.rst create mode 100644 documentation/source/images/plottingClasses.png create mode 100644 documentation/source/images/plottingClasses.svg create mode 100644 documentation/source/index.rst create mode 100644 documentation/source/internals.rst create mode 100644 documentation/source/introduction.rst create mode 100644 documentation/source/parametertree.rst create mode 100644 documentation/source/plotting.rst create mode 100644 documentation/source/region_of_interest.rst create mode 100644 documentation/source/style.rst create mode 100644 documentation/source/widgets/checktable.rst create mode 100644 documentation/source/widgets/colorbutton.rst create mode 100644 documentation/source/widgets/datatreewidget.rst create mode 100644 documentation/source/widgets/dockarea.rst create mode 100644 documentation/source/widgets/filedialog.rst create mode 100644 documentation/source/widgets/gradientwidget.rst create mode 100644 documentation/source/widgets/graphicslayoutwidget.rst create mode 100644 documentation/source/widgets/graphicsview.rst create mode 100644 documentation/source/widgets/histogramlutwidget.rst create mode 100644 documentation/source/widgets/imageview.rst create mode 100644 documentation/source/widgets/index.rst create mode 100644 documentation/source/widgets/joystickbutton.rst create mode 100644 documentation/source/widgets/make create mode 100644 documentation/source/widgets/multiplotwidget.rst create mode 100644 documentation/source/widgets/parametertree.rst create mode 100644 documentation/source/widgets/plotwidget.rst create mode 100644 documentation/source/widgets/progressdialog.rst create mode 100644 documentation/source/widgets/rawimagewidget.rst create mode 100644 documentation/source/widgets/spinbox.rst create mode 100644 documentation/source/widgets/tablewidget.rst create mode 100644 documentation/source/widgets/treewidget.rst create mode 100644 documentation/source/widgets/verticallabel.rst rename examples/{test_Arrow.py => Arrow.py} (77%) create mode 100644 examples/CLIexample.py create mode 100644 examples/DataSlicing.py rename examples/{test_draw.py => Draw.py} (80%) mode change 100755 => 100644 create mode 100644 examples/Flowchart.py create mode 100644 examples/GradientEditor.py create mode 100755 examples/GraphicsLayout.py create mode 100644 examples/GraphicsScene.py create mode 100644 examples/HistogramLUT.py rename examples/{test_ImageItem.py => ImageItem.py} (53%) mode change 100755 => 100644 rename examples/{test_ImageView.py => ImageView.py} (100%) mode change 100755 => 100644 rename examples/{test_MultiPlotWidget.py => MultiPlotWidget.py} (94%) mode change 100755 => 100644 create mode 100644 examples/PlotSpeedTest.py rename examples/{test_PlotWidget.py => PlotWidget.py} (76%) mode change 100755 => 100644 create mode 100644 examples/Plotting.py rename examples/{test_ROItypes.py => ROItypes.py} (58%) mode change 100755 => 100644 create mode 100755 examples/ScatterPlot.py create mode 100644 examples/VideoSpeedTest.py create mode 100644 examples/VideoTemplate.py create mode 100644 examples/VideoTemplate.ui rename examples/{test_viewBox.py => ViewBox.py} (83%) create mode 100644 examples/__init__.py create mode 100644 examples/__main__.py create mode 100644 examples/exampleLoaderTemplate.py create mode 100644 examples/exampleLoaderTemplate.ui delete mode 100755 examples/test_scatterPlot.py create mode 100644 flowchart/Flowchart.py create mode 100644 flowchart/FlowchartCtrlTemplate.py create mode 100644 flowchart/FlowchartCtrlTemplate.ui create mode 100644 flowchart/FlowchartGraphicsView.py create mode 100644 flowchart/FlowchartTemplate.py create mode 100644 flowchart/FlowchartTemplate.ui create mode 100644 flowchart/Node.py create mode 100644 flowchart/Terminal.py create mode 100644 flowchart/__init__.py create mode 100644 flowchart/eq.py create mode 100644 flowchart/library/Data.py create mode 100644 flowchart/library/Display.py create mode 100644 flowchart/library/EventDetection.py create mode 100644 flowchart/library/Filters.py create mode 100644 flowchart/library/Operators.py create mode 100644 flowchart/library/__init__.py create mode 100644 flowchart/library/common.py create mode 100644 functions.pyc delete mode 100644 graphicsItems.py create mode 100644 graphicsItems/ArrowItem.py create mode 100644 graphicsItems/AxisItem.py create mode 100644 graphicsItems/ButtonItem.py create mode 100644 graphicsItems/CurvePoint.py create mode 100644 graphicsItems/GradientEditorItem.py create mode 100644 graphicsItems/GradientLegend.py create mode 100644 graphicsItems/GraphicsItemMethods.py create mode 100644 graphicsItems/GraphicsLayout.py create mode 100644 graphicsItems/GraphicsObject.py create mode 100644 graphicsItems/GraphicsWidget.py create mode 100644 graphicsItems/GridItem.py create mode 100644 graphicsItems/HistogramLUTItem.py create mode 100644 graphicsItems/ImageItem.old create mode 100644 graphicsItems/ImageItem.py create mode 100644 graphicsItems/InfiniteLine.py create mode 100644 graphicsItems/ItemGroup.py create mode 100644 graphicsItems/LabelItem.py create mode 100644 graphicsItems/LinearRegionItem.py rename MultiPlotItem.py => graphicsItems/MultiPlotItem.py (74%) create mode 100644 graphicsItems/PlotCurveItem.py create mode 100644 graphicsItems/PlotDataItem.py create mode 100644 graphicsItems/PlotItem/PlotItem.py create mode 100644 graphicsItems/PlotItem/__init__.py create mode 100644 graphicsItems/PlotItem/auto.png create mode 100644 graphicsItems/PlotItem/ctrl.png create mode 100644 graphicsItems/PlotItem/icons.svg create mode 100644 graphicsItems/PlotItem/lock.png create mode 100644 graphicsItems/PlotItem/plotConfigTemplate.py create mode 100644 graphicsItems/PlotItem/plotConfigTemplate.ui rename widgets.py => graphicsItems/ROI.py (67%) create mode 100644 graphicsItems/ScaleBar.py create mode 100644 graphicsItems/ScatterPlotItem.py create mode 100644 graphicsItems/UIGraphicsItem.py create mode 100644 graphicsItems/VTickGroup.py create mode 100644 graphicsItems/ViewBox.pyc.renamed1 create mode 100644 graphicsItems/ViewBox/ViewBox.py create mode 100644 graphicsItems/ViewBox/ViewBoxMenu.py create mode 100644 graphicsItems/ViewBox/__init__.py create mode 100644 graphicsItems/ViewBox/axisCtrlTemplate.py create mode 100644 graphicsItems/ViewBox/axisCtrlTemplate.ui create mode 100644 graphicsItems/__init__.py create mode 100644 graphicsWindows.pyc rename ImageView.py => imageview/ImageView.py (76%) rename ImageViewTemplate.py => imageview/ImageViewTemplate.py (80%) create mode 100644 imageview/ImageViewTemplate.ui create mode 100644 imageview/__init__.py create mode 100644 parametertree/Parameter.py create mode 100644 parametertree/ParameterItem.py create mode 100644 parametertree/ParameterTree.py create mode 100644 parametertree/__init__.py create mode 100644 parametertree/__main__.py create mode 100644 parametertree/default.png create mode 100644 parametertree/parameterTypes.py delete mode 100644 plotConfigTemplate.py delete mode 100644 plotConfigTemplate.ui create mode 100644 ptime.pyc create mode 100644 widgets/CheckTable.py rename ColorButton.py => widgets/ColorButton.py (83%) create mode 100644 widgets/DataTreeWidget.py create mode 100644 widgets/FileDialog.py create mode 100644 widgets/GradientWidget.py create mode 100644 widgets/GraphicsLayoutWidget.py rename GraphicsView.py => widgets/GraphicsView.py (81%) create mode 100644 widgets/HistogramLUTWidget.py create mode 100644 widgets/JoystickButton.py rename MultiPlotWidget.py => widgets/MultiPlotWidget.py (88%) rename PlotWidget.py => widgets/PlotWidget.py (95%) create mode 100644 widgets/ProgressDialog.py create mode 100644 widgets/RawImageWidget.py create mode 100644 widgets/SpinBox.py create mode 100644 widgets/TableWidget.py create mode 100644 widgets/TreeWidget.py create mode 100644 widgets/VerticalLabel.py create mode 100644 widgets/__init__.py diff --git a/GradientWidget.py b/GradientWidget.py deleted file mode 100644 index 033c62db..00000000 --- a/GradientWidget.py +++ /dev/null @@ -1,452 +0,0 @@ -# -*- coding: utf-8 -*- -from PyQt4 import QtGui, QtCore -import weakref - -class TickSlider(QtGui.QGraphicsView): - def __init__(self, parent=None, orientation='bottom', allowAdd=True, **kargs): - QtGui.QGraphicsView.__init__(self, parent) - #self.orientation = orientation - self.allowAdd = allowAdd - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor) - self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) - self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) - self.length = 100 - self.tickSize = 15 - self.orientations = { - 'left': (270, 1, -1), - 'right': (270, 1, 1), - 'top': (0, 1, -1), - 'bottom': (0, 1, 1) - } - - self.scene = QtGui.QGraphicsScene() - self.setScene(self.scene) - - self.ticks = {} - self.maxDim = 20 - self.setOrientation(orientation) - self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) - self.setBackgroundRole(QtGui.QPalette.NoRole) - self.setMouseTracking(True) - - def keyPressEvent(self, ev): - ev.ignore() - - def setMaxDim(self, mx=None): - if mx is None: - mx = self.maxDim - else: - self.maxDim = mx - - if self.orientation in ['bottom', 'top']: - self.setFixedHeight(mx) - self.setMaximumWidth(16777215) - else: - self.setFixedWidth(mx) - self.setMaximumHeight(16777215) - - def setOrientation(self, ort): - self.orientation = ort - self.resetTransform() - self.rotate(self.orientations[ort][0]) - self.scale(*self.orientations[ort][1:]) - self.setMaxDim() - - def addTick(self, x, color=None, movable=True): - if color is None: - color = QtGui.QColor(255,255,255) - tick = Tick(self, [x*self.length, 0], color, movable, self.tickSize) - self.ticks[tick] = x - self.scene.addItem(tick) - return tick - - def removeTick(self, tick): - del self.ticks[tick] - self.scene.removeItem(tick) - - def tickMoved(self, tick, pos): - #print "tick changed" - ## Correct position of tick if it has left bounds. - newX = min(max(0, pos.x()), self.length) - pos.setX(newX) - tick.setPos(pos) - self.ticks[tick] = float(newX) / self.length - - def tickClicked(self, tick, ev): - if ev.button() == QtCore.Qt.RightButton: - self.removeTick(tick) - - def widgetLength(self): - if self.orientation in ['bottom', 'top']: - return self.width() - else: - return self.height() - - def resizeEvent(self, ev): - wlen = max(40, self.widgetLength()) - self.setLength(wlen-self.tickSize) - bounds = self.scene.itemsBoundingRect() - bounds.setLeft(min(-self.tickSize*0.5, bounds.left())) - bounds.setRight(max(self.length + self.tickSize, bounds.right())) - #bounds.setTop(min(bounds.top(), self.tickSize)) - #bounds.setBottom(max(0, bounds.bottom())) - self.setSceneRect(bounds) - self.fitInView(bounds, QtCore.Qt.KeepAspectRatio) - - def setLength(self, newLen): - for t, x in self.ticks.items(): - t.setPos(x * newLen, t.pos().y()) - self.length = float(newLen) - - def mousePressEvent(self, ev): - QtGui.QGraphicsView.mousePressEvent(self, ev) - self.ignoreRelease = False - if len(self.items(ev.pos())) > 0: ## Let items handle their own clicks - self.ignoreRelease = True - - def mouseReleaseEvent(self, ev): - QtGui.QGraphicsView.mouseReleaseEvent(self, ev) - if self.ignoreRelease: - return - - pos = self.mapToScene(ev.pos()) - if pos.x() < 0 or pos.x() > self.length: - return - if pos.y() < 0 or pos.y() > self.tickSize: - return - - if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: - pos.setX(min(max(pos.x(), 0), self.length)) - self.addTick(pos.x()/self.length) - elif ev.button() == QtCore.Qt.RightButton: - self.showMenu(ev) - - - def showMenu(self, ev): - pass - - def setTickColor(self, tick, color): - tick = self.getTick(tick) - tick.color = color - tick.setBrush(QtGui.QBrush(QtGui.QColor(tick.color))) - - def setTickValue(self, tick, val): - tick = self.getTick(tick) - val = min(max(0.0, val), 1.0) - x = val * self.length - pos = tick.pos() - pos.setX(x) - tick.setPos(pos) - self.ticks[tick] = val - - def tickValue(self, tick): - tick = self.getTick(tick) - return self.ticks[tick] - - def getTick(self, tick): - if type(tick) is int: - tick = self.listTicks()[tick][0] - return tick - - def mouseMoveEvent(self, ev): - QtGui.QGraphicsView.mouseMoveEvent(self, ev) - #print ev.pos(), ev.buttons() - - def listTicks(self): - ticks = self.ticks.items() - ticks.sort(lambda a,b: cmp(a[1], b[1])) - return ticks - - -class GradientWidget(TickSlider): - - sigGradientChanged = QtCore.Signal(object) - - def __init__(self, *args, **kargs): - TickSlider.__init__(self, *args, **kargs) - self.currentTick = None - self.currentTickColor = None - self.rectSize = 15 - self.gradRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize)) - self.colorMode = 'rgb' - self.colorDialog = QtGui.QColorDialog() - self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) - self.colorDialog.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) - #QtCore.QObject.connect(self.colorDialog, QtCore.SIGNAL('currentColorChanged(const QColor&)'), self.currentColorChanged) - self.colorDialog.currentColorChanged.connect(self.currentColorChanged) - #QtCore.QObject.connect(self.colorDialog, QtCore.SIGNAL('rejected()'), self.currentColorRejected) - self.colorDialog.rejected.connect(self.currentColorRejected) - - #self.gradient = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(100,0)) - self.scene.addItem(self.gradRect) - self.addTick(0, QtGui.QColor(0,0,0), True) - self.addTick(1, QtGui.QColor(255,0,0), True) - - self.setMaxDim(self.rectSize + self.tickSize) - self.updateGradient() - - #self.btn = QtGui.QPushButton('RGB') - #self.btnProxy = self.scene.addWidget(self.btn) - #self.btnProxy.setFlag(self.btnProxy.ItemIgnoresTransformations) - #self.btnProxy.scale(0.7, 0.7) - #self.btnProxy.translate(-self.btnProxy.sceneBoundingRect().width()+self.tickSize/2., 0) - #if self.orientation == 'bottom': - #self.btnProxy.translate(0, -self.rectSize) - - def setColorMode(self, cm): - if cm not in ['rgb', 'hsv']: - raise Exception("Unknown color mode %s" % str(cm)) - self.colorMode = cm - self.updateGradient() - - def updateGradient(self): - self.gradient = self.getGradient() - self.gradRect.setBrush(QtGui.QBrush(self.gradient)) - #self.emit(QtCore.SIGNAL('gradientChanged'), self) - self.sigGradientChanged.emit(self) - - def setLength(self, newLen): - TickSlider.setLength(self, newLen) - self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize) - self.updateGradient() - - def currentColorChanged(self, color): - if color.isValid() and self.currentTick is not None: - self.setTickColor(self.currentTick, color) - self.updateGradient() - - def currentColorRejected(self): - self.setTickColor(self.currentTick, self.currentTickColor) - self.updateGradient() - - def tickClicked(self, tick, ev): - if ev.button() == QtCore.Qt.LeftButton: - if not tick.colorChangeAllowed: - return - self.currentTick = tick - self.currentTickColor = tick.color - self.colorDialog.setCurrentColor(tick.color) - self.colorDialog.open() - #color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) - #if color.isValid(): - #self.setTickColor(tick, color) - #self.updateGradient() - elif ev.button() == QtCore.Qt.RightButton: - if not tick.removeAllowed: - return - if len(self.ticks) > 2: - self.removeTick(tick) - self.updateGradient() - - def tickMoved(self, tick, pos): - TickSlider.tickMoved(self, tick, pos) - self.updateGradient() - - - def getGradient(self): - g = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(self.length,0)) - if self.colorMode == 'rgb': - ticks = self.listTicks() - g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks]) - elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop - ticks = self.listTicks() - stops = [] - stops.append((ticks[0][1], ticks[0][0].color)) - for i in range(1,len(ticks)): - x1 = ticks[i-1][1] - x2 = ticks[i][1] - dx = (x2-x1) / 10. - for j in range(1,10): - x = x1 + dx*j - stops.append((x, self.getColor(x))) - stops.append((x2, self.getColor(x2))) - g.setStops(stops) - return g - - def getColor(self, x): - ticks = self.listTicks() - if x <= ticks[0][1]: - return QtGui.QColor(ticks[0][0].color) # always copy colors before handing them out - if x >= ticks[-1][1]: - return QtGui.QColor(ticks[-1][0].color) - - x2 = ticks[0][1] - for i in range(1,len(ticks)): - x1 = x2 - x2 = ticks[i][1] - if x1 <= x and x2 >= x: - break - - dx = (x2-x1) - if dx == 0: - f = 0. - else: - f = (x-x1) / dx - c1 = ticks[i-1][0].color - c2 = ticks[i][0].color - if self.colorMode == 'rgb': - r = c1.red() * (1.-f) + c2.red() * f - g = c1.green() * (1.-f) + c2.green() * f - b = c1.blue() * (1.-f) + c2.blue() * f - a = c1.alpha() * (1.-f) + c2.alpha() * f - return QtGui.QColor(r, g, b,a) - elif self.colorMode == 'hsv': - h1,s1,v1,_ = c1.getHsv() - h2,s2,v2,_ = c2.getHsv() - h = h1 * (1.-f) + h2 * f - s = s1 * (1.-f) + s2 * f - v = v1 * (1.-f) + v2 * f - c = QtGui.QColor() - c.setHsv(h,s,v) - return c - - - - def mouseReleaseEvent(self, ev): - TickSlider.mouseReleaseEvent(self, ev) - self.updateGradient() - - def addTick(self, x, color=None, movable=True): - if color is None: - color = self.getColor(x) - t = TickSlider.addTick(self, x, color=color, movable=movable) - t.colorChangeAllowed = True - t.removeAllowed = True - return t - - def saveState(self): - ticks = [] - for t in self.ticks: - c = t.color - ticks.append((self.ticks[t], (c.red(), c.green(), c.blue(), c.alpha()))) - state = {'mode': self.colorMode, 'ticks': ticks} - return state - - def restoreState(self, state): - self.setColorMode(state['mode']) - for t in self.ticks.keys(): - self.removeTick(t) - for t in state['ticks']: - c = QtGui.QColor(*t[1]) - self.addTick(t[0], c) - self.updateGradient() - - - -class BlackWhiteSlider(GradientWidget): - def __init__(self, parent): - GradientWidget.__init__(self, parent) - self.getTick(0).colorChangeAllowed = False - self.getTick(1).colorChangeAllowed = False - self.allowAdd = False - self.setTickColor(self.getTick(1), QtGui.QColor(255,255,255)) - self.setOrientation('right') - - def getLevels(self): - return (self.tickValue(0), self.tickValue(1)) - - def setLevels(self, black, white): - self.setTickValue(0, black) - self.setTickValue(1, white) - - - - -class GammaWidget(TickSlider): - pass - - -class Tick(QtGui.QGraphicsPolygonItem): - def __init__(self, view, pos, color, movable=True, scale=10): - #QObjectWorkaround.__init__(self) - self.movable = movable - self.view = weakref.ref(view) - self.scale = scale - self.color = color - #self.endTick = endTick - self.pg = QtGui.QPolygonF([QtCore.QPointF(0,0), QtCore.QPointF(-scale/3**0.5,scale), QtCore.QPointF(scale/3**0.5,scale)]) - QtGui.QGraphicsPolygonItem.__init__(self, self.pg) - self.setPos(pos[0], pos[1]) - self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemIsSelectable) - self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) - if self.movable: - self.setZValue(1) - else: - self.setZValue(0) - - #def x(self): - #return self.pos().x()/100. - - def mouseMoveEvent(self, ev): - #print self, "move", ev.scenePos() - if not self.movable: - return - if not ev.buttons() & QtCore.Qt.LeftButton: - return - - - newPos = ev.scenePos() + self.mouseOffset - newPos.setY(self.pos().y()) - #newPos.setX(min(max(newPos.x(), 0), 100)) - self.setPos(newPos) - self.view().tickMoved(self, newPos) - self.movedSincePress = True - #self.emit(QtCore.SIGNAL('tickChanged'), self) - ev.accept() - - def mousePressEvent(self, ev): - self.movedSincePress = False - if ev.button() == QtCore.Qt.LeftButton: - ev.accept() - self.mouseOffset = self.pos() - ev.scenePos() - self.pressPos = ev.scenePos() - elif ev.button() == QtCore.Qt.RightButton: - ev.accept() - #if self.endTick: - #return - #self.view.tickChanged(self, delete=True) - - def mouseReleaseEvent(self, ev): - #print self, "release", ev.scenePos() - if not self.movedSincePress: - self.view().tickClicked(self, ev) - - #if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: - #color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) - #if color.isValid(): - #self.color = color - #self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) - ##self.emit(QtCore.SIGNAL('tickChanged'), self) - #self.view.tickChanged(self) - - - - - -if __name__ == '__main__': - app = QtGui.QApplication([]) - w = QtGui.QMainWindow() - w.show() - w.resize(400,400) - cw = QtGui.QWidget() - w.setCentralWidget(cw) - - l = QtGui.QGridLayout() - l.setSpacing(0) - cw.setLayout(l) - - w1 = GradientWidget(orientation='top') - w2 = GradientWidget(orientation='right', allowAdd=False) - w2.setTickColor(1, QtGui.QColor(255,255,255)) - w3 = GradientWidget(orientation='bottom') - w4 = TickSlider(orientation='left') - - l.addWidget(w1, 0, 1) - l.addWidget(w2, 1, 2) - l.addWidget(w3, 2, 1) - l.addWidget(w4, 1, 0) - - - \ No newline at end of file diff --git a/GraphicsScene.py b/GraphicsScene.py new file mode 100644 index 00000000..e9316796 --- /dev/null +++ b/GraphicsScene.py @@ -0,0 +1,747 @@ +from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL, QtSvg +import weakref +from pyqtgraph.Point import Point +import pyqtgraph.functions as fn +import pyqtgraph.ptime as ptime +import debug + +try: + import sip + HAVE_SIP = True +except: + HAVE_SIP = False + + +__all__ = ['GraphicsScene'] + +class GraphicsScene(QtGui.QGraphicsScene): + """ + Extension of QGraphicsScene that implements a complete, parallel mouse event system. + (It would have been preferred to just alter the way QGraphicsScene creates and delivers + events, but this turned out to be impossible because the constructor for QGraphicsMouseEvent + is private) + + - Generates MouseClicked events in addition to the usual press/move/release events. + (This works around a problem where it is impossible to have one item respond to a + drag if another is watching for a click.) + - Adjustable radius around click that will catch objects so you don't have to click *exactly* over small/thin objects + - Global context menu--if an item implements a context menu, then its parent(s) may also add items to the menu. + - Allows items to decide _before_ a mouse click which item will be the recipient of mouse events. + This lets us indicate unambiguously to the user which item they are about to click/drag on + - Eats mouseMove events that occur too soon after a mouse press. + - Reimplements items() and itemAt() to circumvent PyQt bug + + Mouse interaction is as follows: + 1) Every time the mouse moves, the scene delivers both the standard hoverEnter/Move/LeaveEvents + as well as custom HoverEvents. + 2) Items are sent HoverEvents in Z-order and each item may optionally call event.acceptClicks(button), + acceptDrags(button) or both. If this method call returns True, this informs the item that _if_ + the user clicks/drags the specified mouse button, the item is guaranteed to be the + recipient of click/drag events (the item may wish to change its appearance to indicate this). + If the call to acceptClicks/Drags returns False, then the item is guaranteed to NOT receive + the requested event (because another item has already accepted it). + 3) If the mouse is clicked, a mousePressEvent is generated as usual. If any items accept this press event, then + No click/drag events will be generated and mouse interaction proceeds as defined by Qt. This allows + items to function properly if they are expecting the usual press/move/release sequence of events. + (It is recommended that items do NOT accept press events, and instead use click/drag events) + Note: The default implementation of QGraphicsItem.mousePressEvent will ACCEPT the event if the + item is has its Selectable or Movable flags enabled. You may need to override this behavior. + 3) If no item accepts the mousePressEvent, then the scene will begin delivering mouseDrag and/or mouseClick events. + If the mouse is moved a sufficient distance (or moved slowly enough) before the button is released, + then a mouseDragEvent is generated. + If no drag events are generated before the button is released, then a mouseClickEvent is generated. + 4) Click/drag events are delivered to the item that called acceptClicks/acceptDrags on the HoverEvent + in step 1. If no such items exist, then the scene attempts to deliver the events to items near the event. + ClickEvents may be delivered in this way even if no + item originally claimed it could accept the click. DragEvents may only be delivered this way if it is the initial + move in a drag. + """ + + + _addressCache = weakref.WeakValueDictionary() + sigMouseHover = QtCore.Signal(object) ## emits a list of objects hovered over + sigMouseMoved = QtCore.Signal(object) ## emits position of mouse on every move + sigMouseClicked = QtCore.Signal(object) ## emitted when MouseClickEvent is not accepted by any items under the click. + + @classmethod + def registerObject(cls, obj): + """ + Workaround for PyQt bug in qgraphicsscene.items() + All subclasses of QGraphicsObject must register themselves with this function. + (otherwise, mouse interaction with those objects will likely fail) + """ + if HAVE_SIP and isinstance(obj, sip.wrapper): + cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj + + + def __init__(self, clickRadius=2, moveDistance=5): + QtGui.QGraphicsScene.__init__(self) + self.setClickRadius(clickRadius) + self.setMoveDistance(moveDistance) + self.clickEvents = [] + self.dragButtons = [] + self.mouseGrabber = None + self.dragItem = None + self.lastDrag = None + self.hoverItems = weakref.WeakKeyDictionary() + self.lastHoverEvent = None + #self.searchRect = QtGui.QGraphicsRectItem() + #self.searchRect.setPen(fn.mkPen(200,0,0)) + #self.addItem(self.searchRect) + + + def setClickRadius(self, r): + """ + Set the distance away from mouse clicks to search for interacting items. + When clicking, the scene searches first for items that directly intersect the click position + followed by any other items that are within a rectangle that extends r pixels away from the + click position. + """ + self._clickRadius = r + + def setMoveDistance(self, d): + """ + Set the distance the mouse must move after a press before mouseMoveEvents will be delivered. + This ensures that clicks with a small amount of movement are recognized as clicks instead of + drags. + """ + self._moveDistance = d + + def mousePressEvent(self, ev): + #print 'scenePress' + QtGui.QGraphicsScene.mousePressEvent(self, ev) + #print "mouseGrabberItem: ", self.mouseGrabberItem() + if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events + self.clickEvents.append(MouseClickEvent(ev)) + #else: + #addr = sip.unwrapinstance(sip.cast(self.mouseGrabberItem(), QtGui.QGraphicsItem)) + #item = GraphicsScene._addressCache.get(addr, self.mouseGrabberItem()) + #print "click grabbed by:", item + + def mouseMoveEvent(self, ev): + self.sigMouseMoved.emit(ev.scenePos()) + + ## First allow QGraphicsScene to deliver hoverEnter/Move/ExitEvents + QtGui.QGraphicsScene.mouseMoveEvent(self, ev) + + + ## Next deliver our own HoverEvents + self.sendHoverEvents(ev) + + + if int(ev.buttons()) != 0: ## button is pressed; send mouseMoveEvents and mouseDragEvents + QtGui.QGraphicsScene.mouseMoveEvent(self, ev) + if self.mouseGrabberItem() is None: + now = ptime.time() + init = False + ## keep track of which buttons are involved in dragging + for btn in [QtCore.Qt.LeftButton, QtCore.Qt.MidButton, QtCore.Qt.RightButton]: + if int(ev.buttons() & btn) == 0: + continue + if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet + cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0] + dist = Point(ev.screenPos() - cev.screenPos()) + if dist.length() < self._moveDistance and now - cev.time() < 0.5: + continue + init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True + self.dragButtons.append(int(btn)) + + ## If we have dragged buttons, deliver a drag event + if len(self.dragButtons) > 0: + if self.sendDragEvent(ev, init=init): + ev.accept() + + def leaveEvent(self, ev): ## inform items that mouse is gone + if len(self.dragButtons) == 0: + self.sendHoverEvents(ev, exitOnly=True) + + + def mouseReleaseEvent(self, ev): + #print 'sceneRelease' + if self.mouseGrabberItem() is None: + #print "sending click/drag event" + if ev.button() in self.dragButtons: + if self.sendDragEvent(ev, final=True): + #print "sent drag event" + ev.accept() + self.dragButtons.remove(ev.button()) + else: + cev = [e for e in self.clickEvents if int(e.button()) == int(ev.button())] + if self.sendClickEvent(cev[0]): + #print "sent click event" + ev.accept() + self.clickEvents.remove(cev[0]) + + if int(ev.buttons()) == 0: + self.dragItem = None + self.dragButtons = [] + self.clickEvents = [] + self.lastDrag = None + QtGui.QGraphicsScene.mouseReleaseEvent(self, ev) + + self.sendHoverEvents(ev) ## let items prepare for next click/drag + + def mouseDoubleClickEvent(self, ev): + QtGui.QGraphicsScene.mouseDoubleClickEvent(self, ev) + if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events + self.clickEvents.append(MouseClickEvent(ev, double=True)) + + def sendHoverEvents(self, ev, exitOnly=False): + ## if exitOnly, then just inform all previously hovered items that the mouse has left. + + if exitOnly: + acceptable=False + items = [] + event = HoverEvent(None, acceptable) + else: + acceptable = int(ev.buttons()) == 0 ## if we are in mid-drag, do not allow items to accept the hover event. + event = HoverEvent(ev, acceptable) + items = self.itemsNearEvent(event) + self.sigMouseHover.emit(items) + + prevItems = self.hoverItems.keys() + + for item in items: + if hasattr(item, 'hoverEvent'): + event.currentItem = item + if item not in self.hoverItems: + self.hoverItems[item] = None + event.enter = True + else: + prevItems.remove(item) + event.enter = False + + try: + item.hoverEvent(event) + except: + debug.printExc("Error sending hover event:") + + event.enter = False + event.exit = True + for item in prevItems: + event.currentItem = item + try: + item.hoverEvent(event) + except: + debug.printExc("Error sending hover exit event:") + finally: + del self.hoverItems[item] + + if hasattr(ev, 'buttons') and int(ev.buttons()) == 0: + self.lastHoverEvent = event ## save this so we can ask about accepted events later. + + + def sendDragEvent(self, ev, init=False, final=False): + ## Send a MouseDragEvent to the current dragItem or to + ## items near the beginning of the drag + event = MouseDragEvent(ev, self.clickEvents[0], self.lastDrag, start=init, finish=final) + #print "dragEvent: init=", init, 'final=', final, 'self.dragItem=', self.dragItem + if init and self.dragItem is None: + if self.lastHoverEvent is not None: + acceptedItem = self.lastHoverEvent.dragItems().get(event.button(), None) + else: + acceptedItem = None + + if acceptedItem is not None: + #print "Drag -> pre-selected item:", acceptedItem + self.dragItem = acceptedItem + event.currentItem = self.dragItem + try: + self.dragItem.mouseDragEvent(event) + except: + debug.printExc("Error sending drag event:") + + else: + #print "drag -> new item" + for item in self.itemsNearEvent(event): + #print "check item:", item + if hasattr(item, 'mouseDragEvent'): + event.currentItem = item + try: + item.mouseDragEvent(event) + except: + debug.printExc("Error sending drag event:") + if event.isAccepted(): + #print " --> accepted" + self.dragItem = item + break + elif self.dragItem is not None: + event.currentItem = self.dragItem + try: + self.dragItem.mouseDragEvent(event) + except: + debug.printExc("Error sending hover exit event:") + + self.lastDrag = event + + return event.isAccepted() + + + def sendClickEvent(self, ev): + ## if we are in mid-drag, click events may only go to the dragged item. + if self.dragItem is not None and hasattr(self.dragItem, 'mouseClickEvent'): + ev.currentItem = self.dragItem + self.dragItem.mouseClickEvent(ev) + + ## otherwise, search near the cursor + else: + if self.lastHoverEvent is not None: + acceptedItem = self.lastHoverEvent.clickItems().get(ev.button(), None) + else: + acceptedItem = None + + if acceptedItem is not None: + ev.currentItem = acceptedItem + try: + acceptedItem.mouseClickEvent(ev) + except: + debug.printExc("Error sending click event:") + else: + for item in self.itemsNearEvent(ev): + if hasattr(item, 'mouseClickEvent'): + ev.currentItem = item + try: + item.mouseClickEvent(ev) + except: + debug.printExc("Error sending click event:") + + if ev.isAccepted(): + break + if not ev.isAccepted() and ev.button() is QtCore.Qt.RightButton: + #print "GraphicsScene emitting sigSceneContextMenu" + self.sigMouseClicked.emit(ev) + ev.accept() + return ev.isAccepted() + + #def claimEvent(self, item, button, eventType): + #key = (button, eventType) + #if key in self.claimedEvents: + #return False + #self.claimedEvents[key] = item + #print "event", key, "claimed by", item + #return True + + + def items(self, *args): + #print 'args:', args + items = QtGui.QGraphicsScene.items(self, *args) + ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, + ## then the object returned will be different than the actual item that was originally added to the scene + items2 = map(self.translateGraphicsItem, items) + #if HAVE_SIP and isinstance(self, sip.wrapper): + #items2 = [] + #for i in items: + #addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem)) + #i2 = GraphicsScene._addressCache.get(addr, i) + ##print i, "==>", i2 + #items2.append(i2) + #print 'items:', items + return items2 + + def selectedItems(self, *args): + items = QtGui.QGraphicsScene.selectedItems(self, *args) + ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, + ## then the object returned will be different than the actual item that was originally added to the scene + #if HAVE_SIP and isinstance(self, sip.wrapper): + #items2 = [] + #for i in items: + #addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem)) + #i2 = GraphicsScene._addressCache.get(addr, i) + ##print i, "==>", i2 + #items2.append(i2) + items2 = map(self.translateGraphicsItem, items) + + #print 'items:', items + return items2 + + def itemAt(self, *args): + item = QtGui.QGraphicsScene.itemAt(self, *args) + + ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, + ## then the object returned will be different than the actual item that was originally added to the scene + #if HAVE_SIP and isinstance(self, sip.wrapper): + #addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem)) + #item = GraphicsScene._addressCache.get(addr, item) + #return item + return self.translateGraphicsItem(item) + + def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder): + """ + Return an iterator that iterates first through the items that directly intersect point (in Z order) + followed by any other items that are within the scene's click radius. + """ + #tr = self.getViewWidget(event.widget()).transform() + view = self.views()[0] + tr = view.viewportTransform() + r = self._clickRadius + rect = view.mapToScene(QtCore.QRect(0, 0, 2*r, 2*r)).boundingRect() + + seen = set() + if hasattr(event, 'buttonDownScenePos'): + point = event.buttonDownScenePos() + else: + point = event.scenePos() + w = rect.width() + h = rect.height() + rgn = QtCore.QRectF(point.x()-w, point.y()-h, 2*w, 2*h) + #self.searchRect.setRect(rgn) + + + items = self.items(point, selMode, sortOrder, tr) + + ## remove items whose shape does not contain point (scene.items() apparently sucks at this) + items2 = [] + for item in items: + shape = item.shape() + if shape is None: + continue + if item.mapToScene(shape).contains(point): + items2.append(item) + + ## Sort by descending Z-order (don't trust scene.itms() to do this either) + ## use 'absolute' z value, which is the sum of all item/parent ZValues + def absZValue(item): + if item is None: + return 0 + return item.zValue() + absZValue(item.parentItem()) + + items2.sort(lambda a,b: cmp(absZValue(b), absZValue(a))) + + return items2 + + #for item in items: + ##seen.add(item) + + #shape = item.mapToScene(item.shape()) + #if not shape.contains(point): + #continue + #yield item + #for item in self.items(rgn, selMode, sortOrder, tr): + ##if item not in seen: + #yield item + + def getViewWidget(self, widget): + ## same pyqt bug -- mouseEvent.widget() doesn't give us the original python object. + ## [[doesn't seem to work correctly]] + if HAVE_SIP and isinstance(self, sip.wrapper): + addr = sip.unwrapinstance(sip.cast(widget, QtGui.QWidget)) + #print "convert", widget, addr + for v in self.views(): + addr2 = sip.unwrapinstance(sip.cast(v, QtGui.QWidget)) + #print " check:", v, addr2 + if addr2 == addr: + return v + else: + return widget + + def addParentContextMenus(self, item, menu, event): + """ + Can be called by any item in the scene to expand its context menu to include parent context menus. + Parents may implement getContextMenus to add new menus / actions to the existing menu. + getContextMenus must accept 1 argument (the event that generated the original menu) and + return a single QMenu or a list of QMenus. + + The final menu will look like: + + Original Item 1 + Original Item 2 + ... + Original Item N + ------------------ + Parent Item 1 + Parent Item 2 + ... + Grandparent Item 1 + ... + + + Arguments: + item - The item that initially created the context menu + (This is probably the item making the call to this function) + menu - The context menu being shown by the item + event - The original event that triggered the menu to appear. + """ + + #items = self.itemsNearEvent(ev) + menusToAdd = [] + while item.parentItem() is not None: + item = item.parentItem() + #for item in items: + #if item is sender: + #continue + if not hasattr(item, "getContextMenus"): + continue + + + subMenus = item.getContextMenus(event) + if type(subMenus) is not list: ## so that some items (like FlowchartViewBox) can return multiple menus + subMenus = [subMenus] + + for sm in subMenus: + menusToAdd.append(sm) + + if len(menusToAdd) > 0: + menu.addSeparator() + + for m in menusToAdd: + menu.addMenu(m) + + return menu + + @staticmethod + def translateGraphicsItem(item): + ## for fixing pyqt bugs where the wrong item is returned + if HAVE_SIP and isinstance(item, sip.wrapper): + addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem)) + item = GraphicsScene._addressCache.get(addr, item) + return item + + @staticmethod + def translateGraphicsItems(items): + return map(GraphicsScene.translateGraphicsItem, items) + + +class MouseDragEvent: + def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False): + self.start = start + self.finish = finish + self.accepted = False + self.currentItem = None + self._buttonDownScenePos = {} + self._buttonDownScreenPos = {} + for btn in [QtCore.Qt.LeftButton, QtCore.Qt.MidButton, QtCore.Qt.RightButton]: + self._buttonDownScenePos[int(btn)] = moveEvent.buttonDownScenePos(btn) + self._buttonDownScreenPos[int(btn)] = moveEvent.buttonDownScreenPos(btn) + self._scenePos = moveEvent.scenePos() + self._screenPos = moveEvent.screenPos() + if lastEvent is None: + self._lastScenePos = pressEvent.scenePos() + self._lastScreenPos = pressEvent.screenPos() + else: + self._lastScenePos = lastEvent.scenePos() + self._lastScreenPos = lastEvent.screenPos() + self._buttons = moveEvent.buttons() + self._button = pressEvent.button() + self._modifiers = moveEvent.modifiers() + + def accept(self): + self.accepted = True + self.acceptedItem = self.currentItem + + def ignore(self): + self.accepted = False + + def isAccepted(self): + return self.accepted + + def scenePos(self): + return Point(self._scenePos) + + def screenPos(self): + return Point(self._screenPos) + + def buttonDownScenePos(self, btn=None): + if btn is None: + btn = self.button() + return Point(self._buttonDownScenePos[int(btn)]) + + def buttonDownScreenPos(self, btn=None): + if btn is None: + btn = self.button() + return Point(self._buttonDownScreenPos[int(btn)]) + + def lastScenePos(self): + return Point(self._lastScenePos) + + def lastScreenPos(self): + return Point(self._lastScreenPos) + + def buttons(self): + return self._buttons + + def button(self): + """Return the button that initiated the drag (may be different from the buttons currently pressed)""" + return self._button + + def pos(self): + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def buttonDownPos(self, btn=None): + if btn is None: + btn = self.button() + return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)])) + + def isStart(self): + return self.start + + def isFinish(self): + return self.finish + + def __repr__(self): + lp = self.lastPos() + p = self.pos() + return "(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish())) + + def modifiers(self): + return self._modifiers + + + +class MouseClickEvent: + def __init__(self, pressEvent, double=False): + self.accepted = False + self.currentItem = None + self._double = double + self._scenePos = pressEvent.scenePos() + self._screenPos = pressEvent.screenPos() + self._button = pressEvent.button() + self._buttons = pressEvent.buttons() + self._modifiers = pressEvent.modifiers() + self._time = ptime.time() + + + def accept(self): + self.accepted = True + self.acceptedItem = self.currentItem + + def ignore(self): + self.accepted = False + + def isAccepted(self): + return self.accepted + + def scenePos(self): + return Point(self._scenePos) + + def screenPos(self): + return Point(self._screenPos) + + def buttons(self): + return self._buttons + + def button(self): + return self._button + + def double(self): + return self._double + + def pos(self): + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def modifiers(self): + return self._modifiers + + def __repr__(self): + p = self.pos() + return "" % (p.x(), p.y(), int(self.button())) + + def time(self): + return self._time + + + +class HoverEvent: + """ + This event class both informs items that the mouse cursor is nearby and allows items to + communicate with one another about whether each item will accept _potential_ mouse events. + + It is common for multiple overlapping items to receive hover events and respond by changing + their appearance. This can be misleading to the user since, in general, only one item will + respond to mouse events. To avoid this, items make calls to event.acceptClicks(button) + and/or acceptDrags(button). + + Each item may make multiple calls to acceptClicks/Drags, each time for a different button. + If the method returns True, then the item is guaranteed to be + the recipient of the claimed event IF the user presses the specified mouse button before + moving. If claimEvent returns False, then this item is guaranteed NOT to get the specified + event (because another has already claimed it) and the item should change its appearance + accordingly. + + event.isEnter() returns True if the mouse has just entered the item's shape; + event.isExit() returns True if the mouse has just left. + """ + def __init__(self, moveEvent, acceptable): + self.enter = False + self.acceptable = acceptable + self.exit = False + self.__clickItems = weakref.WeakValueDictionary() + self.__dragItems = weakref.WeakValueDictionary() + self.currentItem = None + if moveEvent is not None: + self._scenePos = moveEvent.scenePos() + self._screenPos = moveEvent.screenPos() + self._lastScenePos = moveEvent.lastScenePos() + self._lastScreenPos = moveEvent.lastScreenPos() + self._buttons = moveEvent.buttons() + self._modifiers = moveEvent.modifiers() + else: + self.exit = True + + + + def isEnter(self): + return self.enter + + def isExit(self): + return self.exit + + def acceptClicks(self, button): + """""" + if not self.acceptable: + return False + if button not in self.__clickItems: + self.__clickItems[button] = self.currentItem + return True + return False + + def acceptDrags(self, button): + if not self.acceptable: + return False + if button not in self.__dragItems: + self.__dragItems[button] = self.currentItem + return True + return False + + def scenePos(self): + return Point(self._scenePos) + + def screenPos(self): + return Point(self._screenPos) + + def lastScenePos(self): + return Point(self._lastScenePos) + + def lastScreenPos(self): + return Point(self._lastScreenPos) + + def buttons(self): + return self._buttons + + def pos(self): + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def __repr__(self): + lp = self.lastPos() + p = self.pos() + return "(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit())) + + def modifiers(self): + return self._modifiers + + def clickItems(self): + return self.__clickItems + + def dragItems(self): + return self.__dragItems + + + \ No newline at end of file diff --git a/GraphicsScene.pyc b/GraphicsScene.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35431aa08b042fbceb3feb069a506181b5f0638b GIT binary patch literal 27489 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 - - Form - - - - 0 - 0 - 726 - 588 - - - - Form - - - - 0 - - - 0 - - - - - Qt::Vertical - - - - - 0 - - - 0 - - - - - - 10 - 10 - - - - - - - - - 0 - 1 - - - - - 30 - 16777215 - - - - R - - - true - - - - - - - - 0 - 100 - - - - - - - - - 0 - 1 - - - - - 30 - 16777215 - - - - N - - - true - - - - - - - Normalization - - - - 0 - - - 0 - - - - - Subtract - - - - - - - Divide - - - false - - - - - - - - 75 - true - - - - Operation: - - - - - - - - 75 - true - - - - Mean: - - - - - - - - 75 - true - - - - Blur: - - - - - - - ROI - - - - - - - - - - X - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Y - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - T - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Off - - - true - - - - - - - Time range - - - - - - - Frame - - - - - - - - - - - - - - - 0 - 0 - - - - - 0 - 40 - - - - - - - - - - GradientWidget - QWidget -
pyqtgraph.GradientWidget
- 1 -
- - GraphicsView - QWidget -
GraphicsView
- 1 -
- - PlotWidget - QWidget -
PlotWidget
- 1 -
-
- - -
diff --git a/PlotItem.py b/PlotItem.py deleted file mode 100644 index d53d2bfc..00000000 --- a/PlotItem.py +++ /dev/null @@ -1,1284 +0,0 @@ -# -*- coding: utf-8 -*- -""" -PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers. -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. - -This class is one of the workhorses of pyqtgraph. It implements a graphics item with -plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want -a widget that can be added to your GUI, see PlotWidget instead. - -This class is very heavily featured: - - Automatically creates and manages PlotCurveItems - - Fast display and update of plots - - Manages zoom/pan ViewBox, scale, and label elements - - Automatic scaling when data changes - - Control panel with a huge feature set including averaging, decimation, - display, power spectrum, svg/png export, plot linking, and more. -""" - -from graphicsItems import * -from plotConfigTemplate import * -from PyQt4 import QtGui, QtCore, QtSvg -from functions import * -#from ObjectWorkaround import * -#tryWorkaround(QtCore, QtGui) -import weakref -import numpy as np -#import debug - -try: - from WidgetGroup import * - HAVE_WIDGETGROUP = True -except: - HAVE_WIDGETGROUP = False - -try: - from metaarray import * - HAVE_METAARRAY = True -except: - HAVE_METAARRAY = False - - -class PlotItem(QtGui.QGraphicsWidget): - - sigYRangeChanged = QtCore.Signal(object, object) - sigXRangeChanged = QtCore.Signal(object, object) - sigRangeChanged = QtCore.Signal(object, object) - - """Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox.""" - lastFileDir = None - managers = {} - - def __init__(self, parent=None, name=None, labels=None, **kargs): - QtGui.QGraphicsWidget.__init__(self, parent) - - ## Set up control buttons - - self.ctrlBtn = QtGui.QToolButton() - self.ctrlBtn.setText('?') - self.autoBtn = QtGui.QToolButton() - self.autoBtn.setText('A') - self.autoBtn.hide() - self.proxies = [] - for b in [self.ctrlBtn, self.autoBtn]: - proxy = QtGui.QGraphicsProxyWidget(self) - proxy.setWidget(b) - proxy.setAcceptHoverEvents(False) - b.setStyleSheet("background-color: #000000; color: #888; font-size: 6pt") - self.proxies.append(proxy) - #QtCore.QObject.connect(self.ctrlBtn, QtCore.SIGNAL('clicked()'), self.ctrlBtnClicked) - self.ctrlBtn.clicked.connect(self.ctrlBtnClicked) - #QtCore.QObject.connect(self.autoBtn, QtCore.SIGNAL('clicked()'), self.enableAutoScale) - self.autoBtn.clicked.connect(self.enableAutoScale) - - - self.layout = QtGui.QGraphicsGridLayout() - self.layout.setContentsMargins(1,1,1,1) - self.setLayout(self.layout) - self.layout.setHorizontalSpacing(0) - self.layout.setVerticalSpacing(0) - - self.vb = ViewBox() - #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('xRangeChanged'), self.xRangeChanged) - self.vb.sigXRangeChanged.connect(self.xRangeChanged) - #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('yRangeChanged'), self.yRangeChanged) - self.vb.sigYRangeChanged.connect(self.yRangeChanged) - #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('rangeChangedManually'), self.enableManualScale) - self.vb.sigRangeChangedManually.connect(self.enableManualScale) - - #QtCore.QObject.connect(self.vb, QtCore.SIGNAL('viewChanged'), self.viewChanged) - self.vb.sigRangeChanged.connect(self.viewRangeChanged) - - self.layout.addItem(self.vb, 2, 1) - self.alpha = 1.0 - self.autoAlpha = True - self.spectrumMode = False - - self.autoScale = [True, True] - - ## Create and place scale items - self.scales = { - 'top': {'item': ScaleItem(orientation='top', linkView=self.vb), 'pos': (1, 1)}, - 'bottom': {'item': ScaleItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)}, - 'left': {'item': ScaleItem(orientation='left', linkView=self.vb), 'pos': (2, 0)}, - 'right': {'item': ScaleItem(orientation='right', linkView=self.vb), 'pos': (2, 2)} - } - for k in self.scales: - self.layout.addItem(self.scales[k]['item'], *self.scales[k]['pos']) - - ## Create and place label items - #self.labels = { - #'title': {'item': LabelItem('title', size='11pt'), 'pos': (0, 2), 'text': ''}, - #'top': {'item': LabelItem('top'), 'pos': (1, 2), 'text': '', 'units': '', 'unitPrefix': ''}, - #'bottom': {'item': LabelItem('bottom'), 'pos': (5, 2), 'text': '', 'units': '', 'unitPrefix': ''}, - #'left': {'item': LabelItem('left'), 'pos': (3, 0), 'text': '', 'units': '', 'unitPrefix': ''}, - #'right': {'item': LabelItem('right'), 'pos': (3, 4), 'text': '', 'units': '', 'unitPrefix': ''} - #} - #self.labels['left']['item'].setAngle(-90) - #self.labels['right']['item'].setAngle(-90) - #for k in self.labels: - #self.layout.addItem(self.labels[k]['item'], *self.labels[k]['pos']) - self.titleLabel = LabelItem('', size='11pt') - self.layout.addItem(self.titleLabel, 0, 1) - self.setTitle(None) ## hide - - - for i in range(4): - self.layout.setRowPreferredHeight(i, 0) - self.layout.setRowMinimumHeight(i, 0) - self.layout.setRowSpacing(i, 0) - self.layout.setRowStretchFactor(i, 1) - - for i in range(3): - self.layout.setColumnPreferredWidth(i, 0) - self.layout.setColumnMinimumWidth(i, 0) - self.layout.setColumnSpacing(i, 0) - self.layout.setColumnStretchFactor(i, 1) - self.layout.setRowStretchFactor(2, 100) - self.layout.setColumnStretchFactor(1, 100) - - - ## Wrap a few methods from viewBox - for m in ['setXRange', 'setYRange', 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled']: - setattr(self, m, getattr(self.vb, m)) - - self.items = [] - self.curves = [] - self.dataItems = [] - self.paramList = {} - self.avgCurves = {} - - ### Set up context menu - - w = QtGui.QWidget() - self.ctrl = c = Ui_Form() - c.setupUi(w) - dv = QtGui.QDoubleValidator(self) - self.ctrlMenu = QtGui.QMenu() - self.menuAction = QtGui.QWidgetAction(self) - self.menuAction.setDefaultWidget(w) - self.ctrlMenu.addAction(self.menuAction) - - if HAVE_WIDGETGROUP: - self.stateGroup = WidgetGroup(self.ctrlMenu) - - self.fileDialog = None - - self.xLinkPlot = None - self.yLinkPlot = None - self.linksBlocked = False - - - #self.ctrlBtn.setFixedWidth(60) - self.setAcceptHoverEvents(True) - - ## Connect control widgets - #QtCore.QObject.connect(c.xMinText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) - c.xMinText.editingFinished.connect(self.setManualXScale) - #QtCore.QObject.connect(c.xMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualXScale) - c.xMaxText.editingFinished.connect(self.setManualXScale) - #QtCore.QObject.connect(c.yMinText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) - c.yMinText.editingFinished.connect(self.setManualYScale) - #QtCore.QObject.connect(c.yMaxText, QtCore.SIGNAL('editingFinished()'), self.setManualYScale) - c.yMaxText.editingFinished.connect(self.setManualYScale) - - #QtCore.QObject.connect(c.xManualRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) - c.xManualRadio.clicked.connect(lambda: self.updateXScale()) - #QtCore.QObject.connect(c.yManualRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) - c.yManualRadio.clicked.connect(lambda: self.updateYScale()) - - #QtCore.QObject.connect(c.xAutoRadio, QtCore.SIGNAL('clicked()'), self.updateXScale) - c.xAutoRadio.clicked.connect(self.updateXScale) - #QtCore.QObject.connect(c.yAutoRadio, QtCore.SIGNAL('clicked()'), self.updateYScale) - c.yAutoRadio.clicked.connect(self.updateYScale) - - #QtCore.QObject.connect(c.xAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) - c.xAutoPercentSpin.valueChanged.connect(self.replot) - #QtCore.QObject.connect(c.yAutoPercentSpin, QtCore.SIGNAL('valueChanged(int)'), self.replot) - c.yAutoPercentSpin.valueChanged.connect(self.replot) - - #QtCore.QObject.connect(c.xLogCheck, QtCore.SIGNAL('toggled(bool)'), self.setXLog) - #QtCore.QObject.connect(c.yLogCheck, QtCore.SIGNAL('toggled(bool)'), self.setYLog) - - #QtCore.QObject.connect(c.alphaGroup, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) - c.alphaGroup.toggled.connect(self.updateAlpha) - #QtCore.QObject.connect(c.alphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateAlpha) - c.alphaSlider.valueChanged.connect(self.updateAlpha) - #QtCore.QObject.connect(c.autoAlphaCheck, QtCore.SIGNAL('toggled(bool)'), self.updateAlpha) - c.autoAlphaCheck.toggled.connect(self.updateAlpha) - - #QtCore.QObject.connect(c.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) - c.gridGroup.toggled.connect(self.updateGrid) - #QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) - c.gridAlphaSlider.valueChanged.connect(self.updateGrid) - - #QtCore.QObject.connect(c.powerSpectrumGroup, QtCore.SIGNAL('toggled(bool)'), self.updateSpectrumMode) - c.powerSpectrumGroup.toggled.connect(self.updateSpectrumMode) - #QtCore.QObject.connect(c.saveSvgBtn, QtCore.SIGNAL('clicked()'), self.saveSvgClicked) - c.saveSvgBtn.clicked.connect(self.saveSvgClicked) - #QtCore.QObject.connect(c.saveImgBtn, QtCore.SIGNAL('clicked()'), self.saveImgClicked) - c.saveImgBtn.clicked.connect(self.saveImgClicked) - #QtCore.QObject.connect(c.saveCsvBtn, QtCore.SIGNAL('clicked()'), self.saveCsvClicked) - c.saveCsvBtn.clicked.connect(self.saveCsvClicked) - - #QtCore.QObject.connect(c.gridGroup, QtCore.SIGNAL('toggled(bool)'), self.updateGrid) - #QtCore.QObject.connect(c.gridAlphaSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateGrid) - - #QtCore.QObject.connect(self.ctrl.xLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.xLinkComboChanged) - self.ctrl.xLinkCombo.currentIndexChanged.connect(self.xLinkComboChanged) - #QtCore.QObject.connect(self.ctrl.yLinkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.yLinkComboChanged) - self.ctrl.yLinkCombo.currentIndexChanged.connect(self.yLinkComboChanged) - - #QtCore.QObject.connect(c.downsampleSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDownsampling) - c.downsampleSpin.valueChanged.connect(self.updateDownsampling) - - #QtCore.QObject.connect(self.ctrl.avgParamList, QtCore.SIGNAL('itemClicked(QListWidgetItem*)'), self.avgParamListClicked) - self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked) - #QtCore.QObject.connect(self.ctrl.averageGroup, QtCore.SIGNAL('toggled(bool)'), self.avgToggled) - self.ctrl.averageGroup.toggled.connect(self.avgToggled) - - #QtCore.QObject.connect(self.ctrl.pointsGroup, QtCore.SIGNAL('toggled(bool)'), self.updatePointMode) - #QtCore.QObject.connect(self.ctrl.autoPointsCheck, QtCore.SIGNAL('toggled(bool)'), self.updatePointMode) - - #QtCore.QObject.connect(self.ctrl.maxTracesCheck, QtCore.SIGNAL('toggled(bool)'), self.updateDecimation) - self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) - #QtCore.QObject.connect(self.ctrl.maxTracesSpin, QtCore.SIGNAL('valueChanged(int)'), self.updateDecimation) - self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - #QtCore.QObject.connect(c.xMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) - c.xMouseCheck.toggled.connect(self.mouseCheckChanged) - #QtCore.QObject.connect(c.yMouseCheck, QtCore.SIGNAL('toggled(bool)'), self.mouseCheckChanged) - c.yMouseCheck.toggled.connect(self.mouseCheckChanged) - - self.xLinkPlot = None - self.yLinkPlot = None - self.linksBlocked = False - self.manager = None - - #self.showLabel('right', False) - #self.showLabel('top', False) - #self.showLabel('title', False) - #self.showLabel('left', False) - #self.showLabel('bottom', False) - self.showScale('right', False) - self.showScale('top', False) - self.showScale('left', True) - self.showScale('bottom', True) - - if name is not None: - self.registerPlot(name) - - if labels is not None: - for k in labels: - if isinstance(labels[k], basestring): - labels[k] = (labels[k],) - self.setLabel(k, *labels[k]) - - if len(kargs) > 0: - self.plot(**kargs) - - #def paint(self, *args): - #prof = debug.Profiler('PlotItem.paint', disabled=True) - #QtGui.QGraphicsWidget.paint(self, *args) - #prof.finish() - - - def close(self): - #print "delete", self - ## Most of this crap is needed to avoid PySide trouble. - ## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets) - ## the solution is to manually remove all widgets before scene.clear() is called - if self.ctrlMenu is None: ## already shut down - return - self.ctrlMenu.setParent(None) - self.ctrlMenu = None - - self.ctrlBtn.setParent(None) - self.ctrlBtn = None - self.autoBtn.setParent(None) - self.autoBtn = None - - for k in self.scales: - i = self.scales[k]['item'] - i.close() - - self.scales = None - self.scene().removeItem(self.vb) - self.vb = None - - ## causes invalid index errors: - #for i in range(self.layout.count()): - #self.layout.removeAt(i) - - for p in self.proxies: - try: - p.setWidget(None) - except RuntimeError: - break - self.scene().removeItem(p) - self.proxies = [] - - self.menuAction.releaseWidget(self.menuAction.defaultWidget()) - self.menuAction.setParent(None) - self.menuAction = None - - if self.manager is not None: - self.manager.sigWidgetListChanged.disconnect(self.updatePlotList) - self.manager.removeWidget(self.name) - #else: - #print "no manager" - - def registerPlot(self, name): - self.name = name - win = str(self.window()) - #print "register", name, win - if win not in PlotItem.managers: - PlotItem.managers[win] = PlotWidgetManager() - self.manager = PlotItem.managers[win] - self.manager.addWidget(self, name) - #QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList) - self.manager.sigWidgetListChanged.connect(self.updatePlotList) - self.updatePlotList() - - def updatePlotList(self): - """Update the list of all plotWidgets in the "link" combos""" - #print "update plot list", self - try: - for sc in [self.ctrl.xLinkCombo, self.ctrl.yLinkCombo]: - current = str(sc.currentText()) - sc.clear() - sc.addItem("") - if self.manager is not None: - for w in self.manager.listWidgets(): - #print w - if w == self.name: - continue - sc.addItem(w) - except: - import gc - refs= gc.get_referrers(self) - print " error during update of", self - print " Referrers are:", refs - raise - - def updateGrid(self, *args): - g = self.ctrl.gridGroup.isChecked() - if g: - g = self.ctrl.gridAlphaSlider.value() - for k in self.scales: - self.scales[k]['item'].setGrid(g) - - def viewGeometry(self): - """return the screen geometry of the viewbox""" - v = self.scene().views()[0] - b = self.vb.mapRectToScene(self.vb.boundingRect()) - wr = v.mapFromScene(b).boundingRect() - pos = v.mapToGlobal(v.pos()) - wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) - return wr - - - - - def viewRangeChanged(self, vb, range): - #self.emit(QtCore.SIGNAL('viewChanged'), *args) - self.sigRangeChanged.emit(self, range) - - def blockLink(self, b): - self.linksBlocked = b - - def xLinkComboChanged(self): - self.setXLink(str(self.ctrl.xLinkCombo.currentText())) - - def yLinkComboChanged(self): - self.setYLink(str(self.ctrl.yLinkCombo.currentText())) - - def setXLink(self, plot=None): - """Link this plot's X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" - if isinstance(plot, basestring): - if self.manager is None: - return - if self.xLinkPlot is not None: - self.manager.unlinkX(self, self.xLinkPlot) - plot = self.manager.getWidget(plot) - if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): - plot = plot.getPlotItem() - self.xLinkPlot = plot - if plot is not None: - self.setManualXScale() - self.manager.linkX(self, plot) - - def setYLink(self, plot=None): - """Link this plot's Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" - if isinstance(plot, basestring): - if self.manager is None: - return - if self.yLinkPlot is not None: - self.manager.unlinkY(self, self.yLinkPlot) - plot = self.manager.getWidget(plot) - if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): - plot = plot.getPlotItem() - self.yLinkPlot = plot - if plot is not None: - self.setManualYScale() - self.manager.linkY(self, plot) - - def linkXChanged(self, plot): - """Called when a linked plot has changed its X scale""" - #print "update from", plot - if self.linksBlocked: - return - pr = plot.vb.viewRect() - pg = plot.viewGeometry() - if pg is None: - #print " return early" - return - sg = self.viewGeometry() - upp = float(pr.width()) / pg.width() - x1 = pr.left() + (sg.x()-pg.x()) * upp - x2 = x1 + sg.width() * upp - plot.blockLink(True) - self.setManualXScale() - self.setXRange(x1, x2, padding=0) - plot.blockLink(False) - self.replot() - - def linkYChanged(self, plot): - """Called when a linked plot has changed its Y scale""" - if self.linksBlocked: - return - pr = plot.vb.viewRect() - pg = plot.vb.boundingRect() - sg = self.vb.boundingRect() - upp = float(pr.height()) / pg.height() - y1 = pr.bottom() + (sg.y()-pg.y()) * upp - y2 = y1 + sg.height() * upp - plot.blockLink(True) - self.setManualYScale() - self.setYRange(y1, y2, padding=0) - plot.blockLink(False) - self.replot() - - def avgToggled(self, b): - if b: - self.recomputeAverages() - for k in self.avgCurves: - self.avgCurves[k][1].setVisible(b) - - def avgParamListClicked(self, item): - name = str(item.text()) - self.paramList[name] = (item.checkState() == QtCore.Qt.Checked) - self.recomputeAverages() - - def recomputeAverages(self): - if not self.ctrl.averageGroup.isChecked(): - return - for k in self.avgCurves: - self.removeItem(self.avgCurves[k][1]) - #Qwt.QwtPlotCurve.detach(self.avgCurves[k][1]) - self.avgCurves = {} - for c in self.curves: - self.addAvgCurve(c) - self.replot() - - def addAvgCurve(self, curve): - """Add a single curve into the pool of curves averaged together""" - - ## If there are plot parameters, then we need to determine which to average together. - remKeys = [] - addKeys = [] - if self.ctrl.avgParamList.count() > 0: - - ### First determine the key of the curve to which this new data should be averaged - for i in range(self.ctrl.avgParamList.count()): - item = self.ctrl.avgParamList.item(i) - if item.checkState() == QtCore.Qt.Checked: - remKeys.append(str(item.text())) - else: - addKeys.append(str(item.text())) - - if len(remKeys) < 1: ## In this case, there would be 1 average plot for each data plot; not useful. - return - - p = curve.meta().copy() - for k in p: - if type(k) is tuple: - p['.'.join(k)] = p[k] - del p[k] - for rk in remKeys: - if rk in p: - del p[rk] - for ak in addKeys: - if ak not in p: - p[ak] = None - key = tuple(p.items()) - - ### Create a new curve if needed - if key not in self.avgCurves: - plot = PlotCurveItem() - plot.setPen(mkPen([0, 200, 0])) - plot.setShadowPen(mkPen([0, 0, 0, 100], 3)) - plot.setAlpha(1.0, False) - plot.setZValue(100) - self.addItem(plot) - #Qwt.QwtPlotCurve.attach(plot, self) - self.avgCurves[key] = [0, plot] - self.avgCurves[key][0] += 1 - (n, plot) = self.avgCurves[key] - - ### Average data together - (x, y) = curve.getData() - if plot.yData is not None: - newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n) - plot.setData(plot.xData, newData) - else: - plot.setData(x, y) - - - def mouseCheckChanged(self): - state = [self.ctrl.xMouseCheck.isChecked(), self.ctrl.yMouseCheck.isChecked()] - self.vb.setMouseEnabled(*state) - - def xRangeChanged(self, _, range): - if any(np.isnan(range)) or any(np.isinf(range)): - raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) - self.ctrl.xMinText.setText('%0.5g' % range[0]) - self.ctrl.xMaxText.setText('%0.5g' % range[1]) - - ## automatically change unit scale - maxVal = max(abs(range[0]), abs(range[1])) - (scale, prefix) = siScale(maxVal) - #for l in ['top', 'bottom']: - #if self.getLabel(l).isVisible(): - #self.setLabel(l, unitPrefix=prefix) - #self.getScale(l).setScale(scale) - #else: - #self.setLabel(l, unitPrefix='') - #self.getScale(l).setScale(1.0) - - #self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) - self.sigXRangeChanged.emit(self, range) - - def yRangeChanged(self, _, range): - if any(np.isnan(range)) or any(np.isinf(range)): - raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) - self.ctrl.yMinText.setText('%0.5g' % range[0]) - self.ctrl.yMaxText.setText('%0.5g' % range[1]) - - ## automatically change unit scale - maxVal = max(abs(range[0]), abs(range[1])) - (scale, prefix) = siScale(maxVal) - #for l in ['left', 'right']: - #if self.getLabel(l).isVisible(): - #self.setLabel(l, unitPrefix=prefix) - #self.getScale(l).setScale(scale) - #else: - #self.setLabel(l, unitPrefix='') - #self.getScale(l).setScale(1.0) - #self.emit(QtCore.SIGNAL('yRangeChanged'), self, range) - self.sigYRangeChanged.emit(self, range) - - - def enableAutoScale(self): - self.ctrl.xAutoRadio.setChecked(True) - self.ctrl.yAutoRadio.setChecked(True) - self.autoBtn.hide() - self.updateXScale() - self.updateYScale() - self.replot() - - def updateXScale(self): - """Set plot to autoscale or not depending on state of radio buttons""" - if self.ctrl.xManualRadio.isChecked(): - self.setManualXScale() - else: - self.setAutoXScale() - self.replot() - - def updateYScale(self, b=False): - """Set plot to autoscale or not depending on state of radio buttons""" - if self.ctrl.yManualRadio.isChecked(): - self.setManualYScale() - else: - self.setAutoYScale() - self.replot() - - def enableManualScale(self, v=[True, True]): - if v[0]: - self.autoScale[0] = False - self.ctrl.xManualRadio.setChecked(True) - #self.setManualXScale() - if v[1]: - self.autoScale[1] = False - self.ctrl.yManualRadio.setChecked(True) - #self.setManualYScale() - self.autoBtn.show() - #self.replot() - - def setManualXScale(self): - self.autoScale[0] = False - x1 = float(self.ctrl.xMinText.text()) - x2 = float(self.ctrl.xMaxText.text()) - self.ctrl.xManualRadio.setChecked(True) - self.setXRange(x1, x2, padding=0) - self.autoBtn.show() - #self.replot() - - def setManualYScale(self): - self.autoScale[1] = False - y1 = float(self.ctrl.yMinText.text()) - y2 = float(self.ctrl.yMaxText.text()) - self.ctrl.yManualRadio.setChecked(True) - self.setYRange(y1, y2, padding=0) - self.autoBtn.show() - #self.replot() - - def setAutoXScale(self): - self.autoScale[0] = True - self.ctrl.xAutoRadio.setChecked(True) - #self.replot() - - def setAutoYScale(self): - self.autoScale[1] = True - self.ctrl.yAutoRadio.setChecked(True) - #self.replot() - - def addItem(self, item, *args): - self.items.append(item) - self.vb.addItem(item, *args) - - def removeItem(self, item): - if not item in self.items: - return - self.items.remove(item) - if item in self.dataItems: - self.dataItems.remove(item) - - if item.scene() is not None: - self.vb.removeItem(item) - if item in self.curves: - self.curves.remove(item) - self.updateDecimation() - self.updateParamList() - #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) - item.sigPlotChanged.connect(self.plotChanged) - - def clear(self): - for i in self.items[:]: - self.removeItem(i) - self.avgCurves = {} - - def clearPlots(self): - for i in self.curves[:]: - self.removeItem(i) - self.avgCurves = {} - - - def plot(self, data=None, data2=None, x=None, y=None, clear=False, params=None, pen=None): - """Add a new plot curve. Data may be specified a few ways: - plot(yVals) # x vals will be integers - plot(xVals, yVals) - plot(y=yVals, x=xVals) - """ - if y is not None: - data = y - if data2 is not None: - x = data - data = data2 - - if clear: - self.clear() - if params is None: - params = {} - if HAVE_METAARRAY and isinstance(data, MetaArray): - curve = self._plotMetaArray(data, x=x) - elif isinstance(data, np.ndarray): - curve = self._plotArray(data, x=x) - elif isinstance(data, list): - if x is not None: - x = np.array(x) - curve = self._plotArray(np.array(data), x=x) - elif data is None: - curve = PlotCurveItem() - else: - raise Exception('Not sure how to plot object of type %s' % type(data)) - - #print data, curve - self.addCurve(curve, params) - if pen is not None: - curve.setPen(mkPen(pen)) - - return curve - - def scatterPlot(self, *args, **kargs): - sp = ScatterPlotItem(*args, **kargs) - self.addDataItem(sp) - return sp - - def addDataItem(self, item): - self.addItem(item) - self.dataItems.append(item) - - def addCurve(self, c, params=None): - if params is None: - params = {} - c.setMeta(params) - self.curves.append(c) - #Qwt.QwtPlotCurve.attach(c, self) - self.addItem(c) - - ## configure curve for this plot - (alpha, auto) = self.alphaState() - c.setAlpha(alpha, auto) - c.setSpectrumMode(self.ctrl.powerSpectrumGroup.isChecked()) - c.setDownsampling(self.downsampleMode()) - c.setPointMode(self.pointMode()) - - ## Hide older plots if needed - self.updateDecimation() - - ## Add to average if needed - self.updateParamList() - if self.ctrl.averageGroup.isChecked(): - self.addAvgCurve(c) - - #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) - c.sigPlotChanged.connect(self.plotChanged) - self.plotChanged() - - def plotChanged(self, curve=None): - ## Recompute auto range if needed - for ax in [0, 1]: - if self.autoScale[ax]: - percentScale = [self.ctrl.xAutoPercentSpin.value(), self.ctrl.yAutoPercentSpin.value()][ax] * 0.01 - mn = None - mx = None - for c in self.curves + [c[1] for c in self.avgCurves.values()] + self.dataItems: - if not c.isVisible(): - continue - cmn, cmx = c.getRange(ax, percentScale) - if mn is None or cmn < mn: - mn = cmn - if mx is None or cmx > mx: - mx = cmx - if mn is None or mx is None or any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])): - continue - if mn == mx: - mn -= 1 - mx += 1 - self.setRange(ax, mn, mx) - #print "Auto range:", ax, mn, mx - - def replot(self): - self.plotChanged() - self.update() - - def updateParamList(self): - self.ctrl.avgParamList.clear() - ## Check to see that each parameter for each curve is present in the list - #print "\nUpdate param list", self - #print "paramList:", self.paramList - for c in self.curves: - #print " curve:", c - for p in c.meta().keys(): - #print " param:", p - if type(p) is tuple: - p = '.'.join(p) - - ## If the parameter is not in the list, add it. - matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly) - #print " matches:", matches - if len(matches) == 0: - i = QtGui.QListWidgetItem(p) - if p in self.paramList and self.paramList[p] is True: - #print " set checked" - i.setCheckState(QtCore.Qt.Checked) - else: - #print " set unchecked" - i.setCheckState(QtCore.Qt.Unchecked) - self.ctrl.avgParamList.addItem(i) - else: - i = matches[0] - - self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) - #print "paramList:", self.paramList - - - ## This is bullshit. - def writeSvg(self, fileName=None): - if fileName is None: - fileName = QtGui.QFileDialog.getSaveFileName() - if isinstance(fileName, tuple): - raise Exception("Not implemented yet..") - fileName = str(fileName) - PlotItem.lastFileDir = os.path.dirname(fileName) - - rect = self.vb.viewRect() - xRange = rect.left(), rect.right() - - svg = "" - fh = open(fileName, 'w') - - dx = max(rect.right(),0) - min(rect.left(),0) - ymn = min(rect.top(), rect.bottom()) - ymx = max(rect.top(), rect.bottom()) - dy = max(ymx,0) - min(ymn,0) - sx = 1. - sy = 1. - while dx*sx < 10: - sx *= 1000 - while dy*sy < 10: - sy *= 1000 - sy *= -1 - - #fh.write('\n' % (rect.left()*sx, rect.top()*sx, rect.width()*sy, rect.height()*sy)) - fh.write('\n') - fh.write('\n' % (rect.left()*sx, rect.right()*sx)) - fh.write('\n' % (rect.top()*sy, rect.bottom()*sy)) - - - for item in self.curves: - if isinstance(item, PlotCurveItem): - color = colorStr(item.pen.color()) - opacity = item.pen.color().alpha() / 255. - color = color[:6] - x, y = item.getData() - mask = (x > xRange[0]) * (x < xRange[1]) - mask[:-1] += mask[1:] - m2 = mask.copy() - mask[1:] += m2[:-1] - x = x[mask] - y = y[mask] - - x *= sx - y *= sy - - #fh.write('\n' % color) - fh.write('') - #fh.write("") - for item in self.dataItems: - if isinstance(item, ScatterPlotItem): - - pRect = item.boundingRect() - vRect = pRect.intersected(rect) - - for point in item.points(): - pos = point.pos() - if not rect.contains(pos): - continue - color = colorStr(point.brush.color()) - opacity = point.brush.color().alpha() / 255. - color = color[:6] - x = pos.x() * sx - y = pos.y() * sy - - fh.write('\n' % (x, y, color, opacity)) - #fh.write('') - - ## get list of curves, scatter plots - - - fh.write("\n") - - - - #def writeSvg(self, fileName=None): - #if fileName is None: - #fileName = QtGui.QFileDialog.getSaveFileName() - #fileName = str(fileName) - #PlotItem.lastFileDir = os.path.dirname(fileName) - - #self.svg = QtSvg.QSvgGenerator() - #self.svg.setFileName(fileName) - #res = 120. - #view = self.scene().views()[0] - #bounds = view.viewport().rect() - #bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) - - #self.svg.setResolution(res) - #self.svg.setViewBox(bounds) - - #self.svg.setSize(QtCore.QSize(bounds.width(), bounds.height())) - - #painter = QtGui.QPainter(self.svg) - #view.render(painter, bounds) - - #painter.end() - - ### Workaround to set pen widths correctly - #import re - #data = open(fileName).readlines() - #for i in range(len(data)): - #line = data[i] - #m = re.match(r'(= split: - curves[i].show() - else: - if self.ctrl.forgetTracesCheck.isChecked(): - curves[i].free() - self.removeItem(curves[i]) - else: - curves[i].hide() - - - def updateAlpha(self, *args): - (alpha, auto) = self.alphaState() - for c in self.curves: - c.setAlpha(alpha**2, auto) - - #self.replot(autoRange=False) - - def alphaState(self): - enabled = self.ctrl.alphaGroup.isChecked() - auto = self.ctrl.autoAlphaCheck.isChecked() - alpha = float(self.ctrl.alphaSlider.value()) / self.ctrl.alphaSlider.maximum() - if auto: - alpha = 1.0 ## should be 1/number of overlapping plots - if not enabled: - auto = False - alpha = 1.0 - return (alpha, auto) - - def pointMode(self): - if self.ctrl.pointsGroup.isChecked(): - if self.ctrl.autoPointsCheck.isChecked(): - mode = None - else: - mode = True - else: - mode = False - return mode - - def wheelEvent(self, ev): - # disables default panning the whole scene by mousewheel - ev.accept() - - def resizeEvent(self, ev): - if self.ctrlBtn is None: ## already closed down - return - self.ctrlBtn.move(0, self.size().height() - self.ctrlBtn.size().height()) - self.autoBtn.move(self.ctrlBtn.width(), self.size().height() - self.autoBtn.size().height()) - - def hoverMoveEvent(self, ev): - self.mousePos = ev.pos() - self.mouseScreenPos = ev.screenPos() - - def ctrlBtnClicked(self): - self.ctrlMenu.popup(self.mouseScreenPos) - - def getLabel(self, key): - pass - - def _checkScaleKey(self, key): - if key not in self.scales: - raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(self.scales.keys()))) - - def getScale(self, key): - self._checkScaleKey(key) - return self.scales[key]['item'] - - def setLabel(self, key, text=None, units=None, unitPrefix=None, **args): - self.getScale(key).setLabel(text=text, units=units, unitPrefix=unitPrefix, **args) - - def showLabel(self, key, show=True): - self.getScale(key).showLabel(show) - - def setTitle(self, title=None, **args): - if title is None: - self.titleLabel.setVisible(False) - self.layout.setRowFixedHeight(0, 0) - self.titleLabel.setMaximumHeight(0) - else: - self.titleLabel.setMaximumHeight(30) - self.layout.setRowFixedHeight(0, 30) - self.titleLabel.setVisible(True) - self.titleLabel.setText(title, **args) - - def showScale(self, key, show=True): - s = self.getScale(key) - p = self.scales[key]['pos'] - if show: - s.show() - else: - s.hide() - - def _plotArray(self, arr, x=None): - if arr.ndim != 1: - raise Exception("Array must be 1D to plot (shape is %s)" % arr.shape) - if x is None: - x = np.arange(arr.shape[0]) - if x.ndim != 1: - raise Exception("X array must be 1D to plot (shape is %s)" % x.shape) - c = PlotCurveItem(arr, x=x) - return c - - - - def _plotMetaArray(self, arr, x=None, autoLabel=True): - inf = arr.infoCopy() - if arr.ndim != 1: - raise Exception('can only automatically plot 1 dimensional arrays.') - ## create curve - try: - xv = arr.xvals(0) - #print 'xvals:', xv - except: - if x is None: - xv = arange(arr.shape[0]) - else: - xv = x - c = PlotCurveItem() - c.setData(x=xv, y=arr.view(np.ndarray)) - - if autoLabel: - name = arr._info[0].get('name', None) - units = arr._info[0].get('units', None) - self.setLabel('bottom', text=name, units=units) - - name = arr._info[1].get('name', None) - units = arr._info[1].get('units', None) - self.setLabel('left', text=name, units=units) - - return c - - def saveSvgClicked(self): - fileName = QtGui.QFileDialog.getSaveFileName() - self.writeSvg(fileName) - - ## QFileDialog seems to be broken under OSX - #self.fileDialog = QtGui.QFileDialog() - ##if PlotItem.lastFileDir is not None: - ##self.fileDialog.setDirectory(PlotItem.lastFileDir) - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - #if PlotItem.lastFileDir is not None: - #self.fileDialog.setDirectory(PlotItem.lastFileDir) - - #self.fileDialog.show() - ##QtCore.QObject.connect(self.fileDialog, QtCore.SIGNAL('fileSelected(const QString)'), self.writeSvg) - #self.fileDialog.fileSelected.connect(self.writeSvg) - - #def svgFileSelected(self, fileName): - ##PlotWidget.lastFileDir = os.path.split(fileName)[0] - #self.writeSvg(str(fileName)) - - def saveImgClicked(self): - self.fileDialog = QtGui.QFileDialog() - #if PlotItem.lastFileDir is not None: - #self.fileDialog.setDirectory(PlotItem.lastFileDir) - if PlotItem.lastFileDir is not None: - self.fileDialog.setDirectory(PlotItem.lastFileDir) - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - self.fileDialog.show() - #QtCore.QObject.connect(self.fileDialog, QtCore.SIGNAL('fileSelected(const QString)'), self.writeImage) - self.fileDialog.fileSelected.connect(self.writeImage) - - def saveCsvClicked(self): - self.fileDialog = QtGui.QFileDialog() - #if PlotItem.lastFileDir is not None: - #self.fileDialog.setDirectory(PlotItem.lastFileDir) - if PlotItem.lastFileDir is not None: - self.fileDialog.setDirectory(PlotItem.lastFileDir) - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - self.fileDialog.show() - #QtCore.QObject.connect(self.fileDialog, QtCore.SIGNAL('fileSelected(const QString)'), self.writeCsv) - self.fileDialog.fileSelected.connect(self.writeCsv) - #def imgFileSelected(self, fileName): - ##PlotWidget.lastFileDir = os.path.split(fileName)[0] - #self.writeImage(str(fileName)) - - -class PlotWidgetManager(QtCore.QObject): - - sigWidgetListChanged = QtCore.Signal(object) - - """Used for managing communication between PlotWidgets""" - def __init__(self): - QtCore.QObject.__init__(self) - self.widgets = weakref.WeakValueDictionary() # Don't keep PlotWidgets around just because they are listed here - - def addWidget(self, w, name): - self.widgets[name] = w - #self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) - self.sigWidgetListChanged.emit(self.widgets.keys()) - - def removeWidget(self, name): - if name in self.widgets: - del self.widgets[name] - #self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) - self.sigWidgetListChanged.emit(self.widgets.keys()) - else: - print "plot %s not managed" % name - - - def listWidgets(self): - return self.widgets.keys() - - def getWidget(self, name): - if name not in self.widgets: - return None - else: - return self.widgets[name] - - def linkX(self, p1, p2): - #QtCore.QObject.connect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) - p1.sigXRangeChanged.connect(p2.linkXChanged) - #QtCore.QObject.connect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) - p2.sigXRangeChanged.connect(p1.linkXChanged) - p1.linkXChanged(p2) - #p2.setManualXScale() - - def unlinkX(self, p1, p2): - #QtCore.QObject.disconnect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) - p1.sigXRangeChanged.disconnect(p2.linkXChanged) - #QtCore.QObject.disconnect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) - p2.sigXRangeChanged.disconnect(p1.linkXChanged) - - def linkY(self, p1, p2): - #QtCore.QObject.connect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) - p1.sigYRangeChanged.connect(p2.linkYChanged) - #QtCore.QObject.connect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) - p2.sigYRangeChanged.connect(p1.linkYChanged) - p1.linkYChanged(p2) - #p2.setManualYScale() - - def unlinkY(self, p1, p2): - #QtCore.QObject.disconnect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) - p1.sigYRangeChanged.disconnect(p2.linkYChanged) - #QtCore.QObject.disconnect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) - p2.sigYRangeChanged.disconnect(p1.linkYChanged) diff --git a/Point.py b/Point.py index b98dfad0..c16d4df6 100644 --- a/Point.py +++ b/Point.py @@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from PyQt4 import QtCore +from Qt import QtCore import numpy as np def clip(x, mn, mx): @@ -23,12 +23,12 @@ class Point(QtCore.QPointF): if isinstance(args[0], QtCore.QSizeF): QtCore.QPointF.__init__(self, float(args[0].width()), float(args[0].height())) return + elif isinstance(args[0], float) or isinstance(args[0], int): + QtCore.QPointF.__init__(self, float(args[0]), float(args[0])) + return elif hasattr(args[0], '__getitem__'): QtCore.QPointF.__init__(self, float(args[0][0]), float(args[0][1])) return - elif type(args[0]) in [float, int]: - QtCore.QPointF.__init__(self, float(args[0]), float(args[0])) - return elif len(args) == 2: QtCore.QPointF.__init__(self, args[0], args[1]) return @@ -101,6 +101,10 @@ class Point(QtCore.QPointF): """Returns the vector length of this Point.""" return (self[0]**2 + self[1]**2) ** 0.5 + def norm(self): + """Returns a vector in the same direction with unit length.""" + return self / self.length() + def angle(self, a): """Returns the angle in degrees between this vector and the vector a.""" n1 = self.length() @@ -139,4 +143,7 @@ class Point(QtCore.QPointF): return max(self[0], self[1]) def copy(self): - return Point(self) \ No newline at end of file + return Point(self) + + def toQPoint(self): + return QtCore.QPoint(*self) \ No newline at end of file diff --git a/Point.pyc b/Point.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47869a3a385e284b53b7514a215d11f5d5c94b26 GIT binary patch literal 6601 zcmb_ge{b8y89q|-FU3xr*srArH2maRL!Jl-9j_vgJYE8i~E z{+C|3Fv!B#jfZ-aqty;&~q&m?+8PH1X1w_eh>R@b)`#v*U$Y z=U(WwqJ6Iy=Xsp8y!n%b#Yrpeg$9_`Yw>^a zS9gHgOrv{bHZfzXNeQvJXq#yHb#zbhbJdfo$`(m9c<-nuN@X4i-Zx~ctvY6_IyW%f z`oM6XUScl?_9;eGehpo80JgeOetUhLzy9HGO*;$wo!g}tO%=16-MD{-OKRwpGw)QT znJL`K#3sl2$2Y!*mOG##C=SX(?^4H5*>;7RCIQV>O3hjoPgBKw1eJ<@w`Y}}k&!v2 zXN9OKJtxE&rE5aWD}6?Y1*PYOIIHx65a*OWE5xGG=Y%+~^r8?Kls+%SlF}E1SXO#T zh>J=u3$dc~MIly|UJ>Gw(yKzODSb(Z%Sx{aaYgCNLR?k)iV!a;zfr$RQQSi}o+UOZ zPO6XQ{UKWZ5Fg~&F=Kazj)T=B zL$k%uQ3cKiLmJs{nw#Zc(U!_r%*p=${GR=v+Zi{5J!|C#P&@(Nb6yOPY&^FK*yKRD zQS!!!1gau|=eH_w^1_uRPb1R6qpuyyYMnN`6;seLnliNWAZSM>Hc>AKVv4`cK|`17 zMv0*l#E2NV36o~z@K_K4J>ZYL)YCqPdBYxkB7;9vE zgf3a%33E)#>U_C)U>gAdi>%$N4_B=o7fayu(wVqjrNJi}YSI3IZ}$0whj0zHpP{_t9A^qZ_UWQaP3y z)&x0vYly!Adx!1g*$T}0ONoq)It9EXrYA<2Q%@38Js5$Pq&P8jbl~0464Hf7e;ISc z3_s$t53($^rXuu}$$$xoSyH?K4*pIco;1`{cv7tX6>RlO0xTToB*3ii4RQH&vLPb>CElXh z?syLEzl`~Bqm2V-7VF?{`QA<;sfi%^5q5odOhi^h69(h~%==>kDn!)V>rPTc*MR23 zF%el2O&HKC0Q2iH0Tm+B@fVX6(W^l7a7;v2L=y&d9bi5g6HpF|k7PbJuM^O3XL z#!T&CY*M!c;JVBzlQtMXixs)m63BkX%=? zM?W%qS(1CE6M0`mO@rzb`B>X@SSXuLoO`zZvv~<&OLEc%fxrK}Q|ILrNfu8-`$7%M zo*V8U`Q-aVS7$+h3;b}%9wCRdIbZ&BjF&8gB?ECH?BroD@^qYmD;ECyF__vz0$wn< zZc)F69ZrFtNn4U;y$-G=dZH#@Y=ZvpAO_H3X)qzn#xo>=Vq!=T^E>J(ieg*Iy}`J6C%3>51tYWX6x38kW_!Pql9MM^u~VlKXupd`Xqg0Iw5l-RC%6O$6) zz|E9e=GHS5BYiJ_4_zrnT0)q#yZl1o>8PDWQSR+TW z@+}aIC!(ciqmhI$WEnby78pKwSzc{Ox|(y2VkKK0b9YyAX+e^ft>Lxq6K|U zibosl(JvPPgr8)r_{EkX`Q0L3l<}{y_B1i+sJZW0q0@$a%5PYFh|%qcCGKpT*!E~n zMDZoAg+3OC5q@B7tao5mrQB_RM=iWk)RFlDCP?V6-)PJXdvP*;H1>aiZT^flQAFXvDG;$d`^yUu z4Q#cBhgL=i+~t>yp|gyyC7MbT->?7rLvN>*;O+RV0_}V~{BlfqBj2;EA&rlP7GqNT zE$nYYJayKKMTwl?w8p0~iX#Ymh<{z)&d9A!n?WGouKXno__QneOyOT9#WGk*PoLGE z@3Fha?iF^gvb)ZXDXM>y-Botty04S^89O?$&*0)qQt>7!+LKQe`fssgX*+aip^jM? z^F-k}A_>d1GwseT&Q|PP>cFygN7ZKV}Ps1poj5 literal 0 HcmV?d00001 diff --git a/Qt.py b/Qt.py new file mode 100644 index 00000000..e1d4b28a --- /dev/null +++ b/Qt.py @@ -0,0 +1,5 @@ +## Do all Qt imports from here to allow easier PyQt / PySide compatibility + +from PyQt4 import QtGui, QtCore, QtOpenGL, QtSvg +if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal diff --git a/Qt.pyc b/Qt.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0a46429d8504f92f1ef55df29f6c149de83342e4 GIT binary patch literal 367 zcmYjM!A`?440Y0OLz58yz^SL^vLiy1K-vWj+KNjrQ`SapSkiWl3-ruya^w$?xPntA zfn`5G%g>Gze4a1AKHj$loGhTcrf|0eB{>5FU>W2LEQg$f4ImG|hLDHA)C?4dlEWJZ zxAy#L?s2z(hK2*kqL3EpZ?cKujLHNzgU0#9ZrWbq*+lO<)okugitM?X6q&9XX||D< zOs$78!Bt8Xq|5{N;LpYdK`os`>~TrF8;|%qp86vqihtl9sT6fuXoq$*opH*ISfch8_DrTgj0kF!~_#5Y~nE}V1!2Fncm%A&nweC zUK{yF{s0GV{41^;a^!@B_yO=$&8!oUkT}GQ)3yDmuCDs((VgZWtG%C&e;b8Ve{KAJ zgl1lXMEK`WOq4jZ?~ude2E`3ZnzY}lq zVS|caL|R*S6+91`WS&P(?*w$tgp)as|(O}b~ zrl<_H1S=R1kl30PirUiYleahMu;fi>_T*ruLhH__I-I&;7RMMFw+M0_-5%>gxkhv| zZjeS;Pa~BYWn4Q|NzcWnQ=7Q~xY9|yVBT`SG%DJ3$64X3;~>dnwdsN_FVvArE!g*R zoj%BpMZ&nq5_h6fRRk7N+@y@H#+I{_=TnvPfo_tOMnP4jVpl^+);1Cu*^w$-ZMS3~ zmzztPl1D(3LDs&dP+1O<-Re~pX z<@@=5$ll}DK-*?0e+02aGel~GP9^S~Qw*9sfV()rFvh@NqElh<#dkc@k*!^hJs$RC zPme>k-rtw~c2$-y(5XoE*SG`sA9ktuLH4Z6hNf_H96E?YR?++p@+j3-2eJMRFlVV7 z6q7Q6H_Sc3i+x$S+Pw`J4zd#duHw=KF!{-ITaR>VY>3M8fUO#$^QJWyrvg(!?p???tRguf7hmo+(RR?x0}#$hqGDAtq*h&$ z6UmE|LmoQJfPC!GYqumsdHGPw8llFM6#rJu|R0E2EU+rtt zY*{=2m)6y56Cxj>5Kk50L*Wk=mFjt^N`P19o2WSe7^No7=3Apd7dtB4q$e=0@f-)Y zz{qt+K=rOHroJf3>T;G)Jr@+Y!rVbK>`rQ2cUp~&f5Gty7;vL7Fbfez(rRu7k28lw|<{BfVf=r^da?5^>yl)db52kJE2J7u6Qs|E4V` z!cmbb>Lym-i*yM+3h7#JXKu#S{AjFOWoejA$KiK^!CA zSfs~DcyBRrK`dwY?#Fo^>rftBsCz^~p37r@_j5SR}n$5kpvFg18BF7yCajCYaK{`>9$Lh)*k@J5k&Ml}#?GLIf h{E#6qH$m`dXmlGl9{2HhSabULTXlMv>9*Dze*t4SB(4Ae literal 0 HcmV?d00001 diff --git a/Transform.py b/Transform.py index 9938a190..91614f1d 100644 --- a/Transform.py +++ b/Transform.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from PyQt4 import QtCore, QtGui +from Qt import QtCore, QtGui from Point import Point import numpy as np @@ -108,8 +108,8 @@ class Transform(QtGui.QTransform): def saveState(self): p = self._state['pos'] s = self._state['scale'] - if s[0] == 0: - raise Exception('Invalid scale') + #if s[0] == 0: + #raise Exception('Invalid scale: %s' % str(s)) return {'pos': (p[0], p[1]), 'scale': (s[0], s[1]), 'angle': self._state['angle']} def restoreState(self, state): diff --git a/Transform.pyc b/Transform.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bb1ce46bb5ab0d7334093a3c975e466b7cab671 GIT binary patch literal 9368 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 F{U0*kgaiNp literal 0 HcmV?d00001 diff --git a/WidgetGroup.py b/WidgetGroup.py new file mode 100644 index 00000000..32b952e6 --- /dev/null +++ b/WidgetGroup.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +""" +WidgetGroup.py - WidgetGroup class for easily managing lots of Qt widgets +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +This class addresses the problem of having to save and restore the state +of a large group of widgets. +""" + +from Qt import QtCore, QtGui +import weakref, inspect + + +__all__ = ['WidgetGroup'] + +def splitterState(w): + s = str(w.saveState().toPercentEncoding()) + return s + +def restoreSplitter(w, s): + if type(s) is list: + w.setSizes(s) + elif type(s) is str: + w.restoreState(QtCore.QByteArray.fromPercentEncoding(s)) + else: + print "Can't configure QSplitter using object of type", type(s) + if w.count() > 0: ## make sure at least one item is not collapsed + for i in w.sizes(): + if i > 0: + return + w.setSizes([50] * w.count()) + +def comboState(w): + ind = w.currentIndex() + data = w.itemData(ind) + #if not data.isValid(): + if data is not None: + try: + if not data.isValid(): + data = None + else: + data = data.toInt()[0] + except AttributeError: + pass + if data is None: + return unicode(w.itemText(ind)) + else: + return data + +def setComboState(w, v): + if type(v) is int: + #ind = w.findData(QtCore.QVariant(v)) + ind = w.findData(v) + if ind > -1: + w.setCurrentIndex(ind) + return + w.setCurrentIndex(w.findText(str(v))) + + +class WidgetGroup(QtCore.QObject): + """This class takes a list of widgets and keeps an internal record of their state which is always up to date. Allows reading and writing from groups of widgets simultaneously.""" + + ## List of widget types which can be handled by WidgetGroup. + ## The value for each type is a tuple (change signal function, get function, set function, [auto-add children]) + ## The change signal function that takes an object and returns a signal that is emitted any time the state of the widget changes, not just + ## when it is changed by user interaction. (for example, 'clicked' is not a valid signal here) + ## If the change signal is None, the value of the widget is not cached. + ## Custom widgets not in this list can be made to work with WidgetGroup by giving them a 'widgetGroupInterface' method + ## which returns the tuple. + classes = { + QtGui.QSpinBox: + (lambda w: w.valueChanged, + QtGui.QSpinBox.value, + QtGui.QSpinBox.setValue), + QtGui.QDoubleSpinBox: + (lambda w: w.valueChanged, + QtGui.QDoubleSpinBox.value, + QtGui.QDoubleSpinBox.setValue), + QtGui.QSplitter: + (None, + splitterState, + restoreSplitter, + True), + QtGui.QCheckBox: + (lambda w: w.stateChanged, + QtGui.QCheckBox.isChecked, + QtGui.QCheckBox.setChecked), + QtGui.QComboBox: + (lambda w: w.currentIndexChanged, + comboState, + setComboState), + QtGui.QGroupBox: + (lambda w: w.toggled, + QtGui.QGroupBox.isChecked, + QtGui.QGroupBox.setChecked, + True), + QtGui.QLineEdit: + (lambda w: w.editingFinished, + lambda w: str(w.text()), + QtGui.QLineEdit.setText), + QtGui.QRadioButton: + (lambda w: w.toggled, + QtGui.QRadioButton.isChecked, + QtGui.QRadioButton.setChecked), + QtGui.QSlider: + (lambda w: w.valueChanged, + QtGui.QSlider.value, + QtGui.QSlider.setValue), + } + + sigChanged = QtCore.Signal(str, object) + + + def __init__(self, widgetList=None): + """Initialize WidgetGroup, adding specified widgets into this group. + widgetList can be: + - a list of widget specifications (widget, [name], [scale]) + - a dict of name: widget pairs + - any QObject, and all compatible child widgets will be added recursively. + + The 'scale' parameter for each widget allows QSpinBox to display a different value than the value recorded + in the group state (for example, the program may set a spin box value to 100e-6 and have it displayed as 100 to the user) + """ + QtCore.QObject.__init__(self) + self.widgetList = weakref.WeakKeyDictionary() # Make sure widgets don't stick around just because they are listed here + self.scales = weakref.WeakKeyDictionary() + self.cache = {} ## name:value pairs + self.uncachedWidgets = weakref.WeakKeyDictionary() + if isinstance(widgetList, QtCore.QObject): + self.autoAdd(widgetList) + elif isinstance(widgetList, list): + for w in widgetList: + self.addWidget(*w) + elif isinstance(widgetList, dict): + for name, w in widgetList.iteritems(): + self.addWidget(w, name) + elif widgetList is None: + return + else: + raise Exception("Wrong argument type %s" % type(widgetList)) + + def addWidget(self, w, name=None, scale=None): + if not self.acceptsType(w): + raise Exception("Widget type %s not supported by WidgetGroup" % type(w)) + if name is None: + name = str(w.objectName()) + if name == '': + raise Exception("Cannot add widget '%s' without a name." % str(w)) + self.widgetList[w] = name + self.scales[w] = scale + self.readWidget(w) + + if type(w) in WidgetGroup.classes: + signal = WidgetGroup.classes[type(w)][0] + else: + signal = w.widgetGroupInterface()[0] + + if signal is not None: + if inspect.isfunction(signal) or inspect.ismethod(signal): + signal = signal(w) + signal.connect(self.mkChangeCallback(w)) + else: + self.uncachedWidgets[w] = None + + def findWidget(self, name): + for w in self.widgetList: + if self.widgetList[w] == name: + return w + return None + + def interface(self, obj): + t = type(obj) + if t in WidgetGroup.classes: + return WidgetGroup.classes[t] + else: + return obj.widgetGroupInterface() + + def checkForChildren(self, obj): + """Return true if we should automatically search the children of this object for more.""" + iface = self.interface(obj) + return (len(iface) > 3 and iface[3]) + + def autoAdd(self, obj): + ## Find all children of this object and add them if possible. + accepted = self.acceptsType(obj) + if accepted: + #print "%s auto add %s" % (self.objectName(), obj.objectName()) + self.addWidget(obj) + + if not accepted or self.checkForChildren(obj): + for c in obj.children(): + self.autoAdd(c) + + def acceptsType(self, obj): + for c in WidgetGroup.classes: + if isinstance(obj, c): + return True + if hasattr(obj, 'widgetGroupInterface'): + return True + return False + #return (type(obj) in WidgetGroup.classes) + + def setScale(self, widget, scale): + val = self.readWidget(widget) + self.scales[widget] = scale + self.setWidget(widget, val) + #print "scaling %f to %f" % (val, self.readWidget(widget)) + + + def mkChangeCallback(self, w): + return lambda *args: self.widgetChanged(w, *args) + + def widgetChanged(self, w, *args): + #print "widget changed" + n = self.widgetList[w] + v1 = self.cache[n] + v2 = self.readWidget(w) + if v1 != v2: + #print "widget", n, " = ", v2 + self.emit(QtCore.SIGNAL('changed'), self.widgetList[w], v2) + self.sigChanged.emit(self.widgetList[w], v2) + + def state(self): + for w in self.uncachedWidgets: + self.readWidget(w) + + #cc = self.cache.copy() + #if 'averageGroup' in cc: + #val = cc['averageGroup'] + #w = self.findWidget('averageGroup') + #self.readWidget(w) + #if val != self.cache['averageGroup']: + #print " AverageGroup did not match cached value!" + #else: + #print " AverageGroup OK" + return self.cache.copy() + + def setState(self, s): + #print "SET STATE", self, s + for w in self.widgetList: + n = self.widgetList[w] + #print " restore %s?" % n + if n not in s: + continue + #print " restore state", w, n, s[n] + self.setWidget(w, s[n]) + + def readWidget(self, w): + if type(w) in WidgetGroup.classes: + getFunc = WidgetGroup.classes[type(w)][1] + else: + getFunc = w.widgetGroupInterface()[1] + + if getFunc is None: + return None + + val = getFunc(w) + if self.scales[w] is not None: + val /= self.scales[w] + #if isinstance(val, QtCore.QString): + #val = str(val) + n = self.widgetList[w] + self.cache[n] = val + return val + + def setWidget(self, w, v): + v1 = v + if self.scales[w] is not None: + v *= self.scales[w] + + if type(w) in WidgetGroup.classes: + setFunc = WidgetGroup.classes[type(w)][2] + else: + setFunc = w.widgetGroupInterface()[2] + setFunc(w, v) + #name = self.widgetList[w] + #if name in self.cache and (self.cache[name] != v1): + #print "%s: Cached value %s != set value %s" % (name, str(self.cache[name]), str(v1)) + + + \ No newline at end of file diff --git a/WidgetGroup.pyc b/WidgetGroup.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1743d3041eb7cf343cd14cd0a04c7dc72b48e741 GIT binary patch literal 9737 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 wvKh$Akkuu#JQgbC%uRGgyt4?}Wh`) + """ mkQApp() if 'title' in kargs: w = PlotWindow(title=kargs['title']) del kargs['title'] else: w = PlotWindow() - w.plot(*args, **kargs) + if len(args)+len(kargs) > 0: + w.plot(*args, **kargs) plots.append(w) w.show() return w -def show(*args, **kargs): +def image(*args, **kargs): + """ + | Create and return an ImageWindow (this is just a window with ImageView widget inside), show image data inside. + | Will show 2D or 3D image data. + | Accepts a *title* argument to set the title of the window. + | All other arguments are used to show data. (see :func:`ImageView.setImage() `) + """ mkQApp() w = ImageWindow(*args, **kargs) images.append(w) w.show() return w +show = image ## for backward compatibility + def mkQApp(): if QtGui.QApplication.instance() is None: diff --git a/__init__.pyc b/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aaddcfeb79965db6e4aae3ca71070c9ab6555efd GIT binary patch literal 3585 zcmcguUvC>l5TCUjJ9hpwYTAa13J#=#Q(C*Us7NJLghZ6QIx0|D5MoBeXDBMtQIW!;FiWIM6qe{5 ztC^Y=iuIN$nx%s=RnDjiMHT8-=%_-9XXYrHqy8Kn&1GY(nV+s%m{N;$(1s;fi2Ef9 zXDGNz;k+2IOyPnM*GONZqw5qd%J2q@?N_hBOY=AWstS~wLAU3g4vkv?73r4ycIaFZ z?*?%*2M#xm&<2`aM0cvlp(k2{z5AkMJ5$B88#~+IZ9Q%5>>h0GZ0}cjwd3W$+)P@2+dj=Ck`0OrSEF@+@g5E;_T zQJRel)XNJ!k%feZvUmH!9pCuaHh~>pZW{ zX2{LaDH0E9g^m7Xx;sy2MT+a}5l{?{IC6{5U?DOPP9yi&mV(0g#~d9O6jq!(qSGSw zuhH4;SnX-1_MUQ!tS1|0#(-WszNhdzJpP3a1^g8RecPZT^A|G8GB$uZJBRPvC}fD$ z;2}{fP5P0s&N1tb<5O!if+%V?H!f$Ip-O-w8)WCxLPqO8$qnMh+P zt6{C7Nkzn1ri$POgMp4i1X2v@50Vs#A@c1gc^O1jq@twBB&+EJHgGQWd4*&yYvKuj zkSD4PHlJ&S7#zxeY=)I_OJw^=xE=JhtOqSJF&LRlERwxTOd__~i1X`F^gagm19YTr ztD?H0K2l5SmU=I@pjOotHJ_`i1$?C*N~JMPyXAC0i_ge}h=k2*0qf0bAD7WxKA~*L z69;J!+g6hHn;1w9<@4M@vp_Xx7Cs8-L~8Yll-}ovWtSQ z_JTO{QtgIm>;>L#l(<6^hsnsRx{k3Pe%+yUm>Om5k#U`|7{|slu{EJyz3UCI)C&U_ zfaMui8}G8-Z0dotP;kc?7wJ1*khX_?9Xrn@9;&tHI@*&dFKNjj+i9vGio66e>2x8K zrP>=>9kPn)zBR9Ewe}vjhH>-p3-*K$vc~h(Rc~#2Wz;TAJ$bPzmI`de41+jPm9s$7 zrQh3I9}Fa^cwrVD0eb`|Bx}Kz<-C>kxnc|7c9M}qT}E)*id0W1E(xQ{%L@N9?6-+# zuc4!2NzJQeRmqi+49iG?ifS@HKW+lt&oLMS7kHfFEJI}%$a@1bTsQ!rz>FQU`~L~E z*xTv{ZT&xhCUVb=9szzA;l{!B46B)dj&V2v&7lEmMC8Gym!#gq&9`JHLhl@!VgRSa zMQBc)ssa8oxQNex&rd*F<;vqTYH%?UiQpKJT>lM+BLkp+4`TtJUjyaWxx3Fiv`c!9 zv9V&(GZ2>V7%G)eY^P{r&h#emZ8hr3wZGLY2eR>VVLfgh z9cOA}ngPH3fJRaj^0%p4@F2 zTCA+yH|;oxcGKkKwSSk_KH%;VcVBYHC*nWkj=>}zTyoUp4A=0e7)JUD$HKCmOU1mZ i', self) + self.hideBtn.setFixedWidth(20) + self.hideBtn.setFixedHeight(20) + self.ctrlSize = 200 + self.sizeApplied = False + self.hideBtn.clicked.connect(self.hideBtnClicked) + self.ui.splitter.splitterMoved.connect(self.splitterMoved) + + self.ui.itemList.itemChanged.connect(self.treeItemChanged) + self.ui.itemList.sigItemMoved.connect(self.treeItemMoved) + self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected) + self.ui.autoRangeBtn.clicked.connect(self.autoRange) + self.ui.storeSvgBtn.clicked.connect(self.storeSvg) + self.ui.storePngBtn.clicked.connect(self.storePng) + self.ui.redirectCheck.toggled.connect(self.updateRedirect) + self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect) + self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged) + self.multiSelectBox.sigRegionChangeFinished.connect(self.multiSelectBoxChangeFinished) + self.ui.mirrorSelectionBtn.clicked.connect(self.mirrorSelectionClicked) + self.ui.resetTransformsBtn.clicked.connect(self.resetTransformsClicked) + + self.resizeEvent() + if hideCtrl: + self.hideBtnClicked() + + if name is not None: + self.registeredName = CanvasManager.instance().registerCanvas(self, name) + self.ui.redirectCombo.setHostName(self.registeredName) + + def storeSvg(self): + self.ui.view.writeSvg() + + def storePng(self): + self.ui.view.writeImage() + + def splitterMoved(self): + self.resizeEvent() + + def hideBtnClicked(self): + ctrlSize = self.ui.splitter.sizes()[1] + if ctrlSize == 0: + cs = self.ctrlSize + w = self.ui.splitter.size().width() + if cs > w: + cs = w - 20 + self.ui.splitter.setSizes([w-cs, cs]) + self.hideBtn.setText('>') + else: + self.ctrlSize = ctrlSize + self.ui.splitter.setSizes([100, 0]) + self.hideBtn.setText('<') + self.resizeEvent() + + def autoRange(self): + self.view.autoRange() + + def resizeEvent(self, ev=None): + if ev is not None: + QtGui.QWidget.resizeEvent(self, ev) + self.hideBtn.move(self.ui.view.size().width() - self.hideBtn.width(), 0) + + if not self.sizeApplied: + self.sizeApplied = True + s = min(self.width(), max(100, min(200, self.width()*0.25))) + s2 = self.width()-s + self.ui.splitter.setSizes([s2, s]) + + + def updateRedirect(self, *args): + ### Decide whether/where to redirect items and make it so + cname = str(self.ui.redirectCombo.currentText()) + man = CanvasManager.instance() + if self.ui.redirectCheck.isChecked() and cname != '': + redirect = man.getCanvas(cname) + else: + redirect = None + + if self.redirect is redirect: + return + + self.redirect = redirect + if redirect is None: + self.reclaimItems() + else: + self.redirectItems(redirect) + + + def redirectItems(self, canvas): + for i in self.items: + if i is self.grid: + continue + li = i.listItem + parent = li.parent() + if parent is None: + tree = li.treeWidget() + if tree is None: + print "Skipping item", i, i.name + continue + tree.removeTopLevelItem(li) + else: + parent.removeChild(li) + canvas.addItem(i) + + + def reclaimItems(self): + items = self.items + #self.items = {'Grid': items['Grid']} + #del items['Grid'] + self.items = [self.grid] + items.remove(self.grid) + + for i in items: + i.canvas.removeItem(i) + self.addItem(i) + + def treeItemChanged(self, item, col): + #gi = self.items.get(item.name, None) + #if gi is None: + #return + try: + citem = item.canvasItem + except AttributeError: + return + if item.checkState(0) == QtCore.Qt.Checked: + for i in range(item.childCount()): + item.child(i).setCheckState(0, QtCore.Qt.Checked) + citem.show() + else: + for i in range(item.childCount()): + item.child(i).setCheckState(0, QtCore.Qt.Unchecked) + citem.hide() + + def treeItemSelected(self): + sel = self.selectedItems() + #sel = [] + #for listItem in self.itemList.selectedItems(): + #if hasattr(listItem, 'canvasItem') and listItem.canvasItem is not None: + #sel.append(listItem.canvasItem) + #sel = [self.items[item.name] for item in sel] + + if len(sel) == 0: + #self.selectWidget.hide() + return + + multi = len(sel) > 1 + for i in self.items: + #i.ctrlWidget().hide() + ## updated the selected state of every item + i.selectionChanged(i in sel, multi) + + if len(sel)==1: + #item = sel[0] + #item.ctrlWidget().show() + self.multiSelectBox.hide() + self.ui.mirrorSelectionBtn.hide() + self.ui.resetTransformsBtn.hide() + elif len(sel) > 1: + self.showMultiSelectBox() + + #if item.isMovable(): + #self.selectBox.setPos(item.item.pos()) + #self.selectBox.setSize(item.item.sceneBoundingRect().size()) + #self.selectBox.show() + #else: + #self.selectBox.hide() + + #self.emit(QtCore.SIGNAL('itemSelected'), self, item) + self.sigSelectionChanged.emit(self, sel) + + def selectedItems(self): + """ + Return list of all selected canvasItems + """ + return [item.canvasItem for item in self.itemList.selectedItems() if item.canvasItem is not None] + + #def selectedItem(self): + #sel = self.itemList.selectedItems() + #if sel is None or len(sel) < 1: + #return + #return self.items.get(sel[0].name, None) + + def selectItem(self, item): + li = item.listItem + #li = self.getListItem(item.name()) + #print "select", li + self.itemList.setCurrentItem(li) + + + + def showMultiSelectBox(self): + ## Get list of selected canvas items + items = self.selectedItems() + + rect = self.view.itemBoundingRect(items[0].graphicsItem()) + for i in items: + if not i.isMovable(): ## all items in selection must be movable + return + br = self.view.itemBoundingRect(i.graphicsItem()) + rect = rect|br + + self.multiSelectBox.blockSignals(True) + self.multiSelectBox.setPos([rect.x(), rect.y()]) + self.multiSelectBox.setSize(rect.size()) + self.multiSelectBox.setAngle(0) + self.multiSelectBox.blockSignals(False) + + self.multiSelectBox.show() + + self.ui.mirrorSelectionBtn.show() + self.ui.resetTransformsBtn.show() + #self.multiSelectBoxBase = self.multiSelectBox.getState().copy() + + def mirrorSelectionClicked(self): + for ci in self.selectedItems(): + ci.mirrorY() + self.showMultiSelectBox() + + def resetTransformsClicked(self): + for i in self.selectedItems(): + i.resetTransformClicked() + self.showMultiSelectBox() + + def multiSelectBoxChanged(self): + self.multiSelectBoxMoved() + + def multiSelectBoxChangeFinished(self): + for ci in self.selectedItems(): + ci.applyTemporaryTransform() + ci.sigTransformChangeFinished.emit(ci) + + def multiSelectBoxMoved(self): + transform = self.multiSelectBox.getGlobalTransform() + for ci in self.selectedItems(): + ci.setTemporaryTransform(transform) + ci.sigTransformChanged.emit(ci) + + + def addGraphicsItem(self, item, **opts): + """Add a new GraphicsItem to the scene at pos. + Common options are name, pos, scale, and z + """ + citem = CanvasItem(item, **opts) + self.addItem(citem) + return citem + + + def addGroup(self, name, **kargs): + group = GroupCanvasItem(name=name) + self.addItem(group, **kargs) + return group + + + def addItem(self, citem): + """ + Add an item to the canvas. + """ + + ## Check for redirections + if self.redirect is not None: + name = self.redirect.addItem(citem) + self.items.append(citem) + return name + + if not self.allowTransforms: + citem.setMovable(False) + + citem.sigTransformChanged.connect(self.itemTransformChanged) + citem.sigTransformChangeFinished.connect(self.itemTransformChangeFinished) + citem.sigVisibilityChanged.connect(self.itemVisibilityChanged) + + + ## Determine name to use in the item list + name = citem.opts['name'] + if name is None: + name = 'item' + newname = name + + ## If name already exists, append a number to the end + ## NAH. Let items have the same name if they really want. + #c=0 + #while newname in self.items: + #c += 1 + #newname = name + '_%03d' %c + #name = newname + + ## find parent and add item to tree + #currentNode = self.itemList.invisibleRootItem() + insertLocation = 0 + #print "Inserting node:", name + + + ## determine parent list item where this item should be inserted + parent = citem.parentItem() + if parent in (None, self.view.childGroup): + parent = self.itemList.invisibleRootItem() + else: + parent = parent.listItem + + ## set Z value above all other siblings if none was specified + siblings = [parent.child(i).canvasItem for i in xrange(parent.childCount())] + z = citem.zValue() + if z is None: + zvals = [i.zValue() for i in siblings] + if len(zvals) == 0: + z = 0 + else: + z = max(zvals)+10 + citem.setZValue(z) + + ## determine location to insert item relative to its siblings + for i in range(parent.childCount()): + ch = parent.child(i) + zval = ch.canvasItem.graphicsItem().zValue() ## should we use CanvasItem.zValue here? + if zval < z: + insertLocation = i + break + else: + insertLocation = i+1 + + node = QtGui.QTreeWidgetItem([name]) + flags = node.flags() | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsDragEnabled + if not isinstance(citem, GroupCanvasItem): + flags = flags & ~QtCore.Qt.ItemIsDropEnabled + node.setFlags(flags) + if citem.opts['visible']: + node.setCheckState(0, QtCore.Qt.Checked) + else: + node.setCheckState(0, QtCore.Qt.Unchecked) + + node.name = name + #if citem.opts['parent'] != None: + ## insertLocation is incorrect in this case + parent.insertChild(insertLocation, node) + #else: + #root.insertChild(insertLocation, node) + + citem.name = name + citem.listItem = node + node.canvasItem = citem + self.items.append(citem) + + ctrl = citem.ctrlWidget() + ctrl.hide() + self.ui.ctrlLayout.addWidget(ctrl) + + ## inform the canvasItem that its parent canvas has changed + citem.setCanvas(self) + + ## Autoscale to fit the first item added (not including the grid). + if len(self.items) == 2: + self.autoRange() + + + #for n in name: + #nextnode = None + #for x in range(currentNode.childCount()): + #ch = currentNode.child(x) + #if hasattr(ch, 'name'): ## check Z-value of current item to determine insert location + #zval = ch.canvasItem.zValue() + #if zval > z: + ###print " ->", x + #insertLocation = x+1 + #if n == ch.text(0): + #nextnode = ch + #break + #if nextnode is None: ## If name doesn't exist, create it + #nextnode = QtGui.QTreeWidgetItem([n]) + #nextnode.setFlags((nextnode.flags() | QtCore.Qt.ItemIsUserCheckable) & ~QtCore.Qt.ItemIsDropEnabled) + #nextnode.setCheckState(0, QtCore.Qt.Checked) + ### Add node to correct position in list by Z-value + ###print " ==>", insertLocation + #currentNode.insertChild(insertLocation, nextnode) + + #if n == name[-1]: ## This is the leaf; add some extra properties. + #nextnode.name = name + + #if n == name[0]: ## This is the root; make the item movable + #nextnode.setFlags(nextnode.flags() | QtCore.Qt.ItemIsDragEnabled) + #else: + #nextnode.setFlags(nextnode.flags() & ~QtCore.Qt.ItemIsDragEnabled) + + #currentNode = nextnode + return citem + + def treeItemMoved(self, item, parent, index): + ##Item moved in tree; update Z values + if parent is self.itemList.invisibleRootItem(): + item.canvasItem.setParentItem(self.view.childGroup) + else: + item.canvasItem.setParentItem(parent.canvasItem) + siblings = [parent.child(i).canvasItem for i in xrange(parent.childCount())] + + zvals = [i.zValue() for i in siblings] + zvals.sort(reverse=True) + + for i in range(len(siblings)): + item = siblings[i] + item.setZValue(zvals[i]) + #item = self.itemList.topLevelItem(i) + + ##ci = self.items[item.name] + #ci = item.canvasItem + #if ci is None: + #continue + #if ci.zValue() != zvals[i]: + #ci.setZValue(zvals[i]) + + #if self.itemList.topLevelItemCount() < 2: + #return + #name = item.name + #gi = self.items[name] + #if index == 0: + #next = self.itemList.topLevelItem(1) + #z = self.items[next.name].zValue()+1 + #else: + #prev = self.itemList.topLevelItem(index-1) + #z = self.items[prev.name].zValue()-1 + #gi.setZValue(z) + + + + + + + def itemVisibilityChanged(self, item): + listItem = item.listItem + checked = listItem.checkState(0) == QtCore.Qt.Checked + vis = item.isVisible() + if vis != checked: + if vis: + listItem.setCheckState(0, QtCore.Qt.Checked) + else: + listItem.setCheckState(0, QtCore.Qt.Unchecked) + + def removeItem(self, item): + if isinstance(item, CanvasItem): + item.setCanvas(None) + #self.view.scene().removeItem(item.item) + self.itemList.removeTopLevelItem(item.listItem) + #del self.items[item.name] + self.items.remove(item) + else: + self.view.removeItem(item) + + ## disconnect signals, remove from list, etc.. + + + def addToScene(self, item): + self.view.addItem(item) + + def removeFromScene(self, item): + self.view.removeItem(item) + + + def listItems(self): + """Return a dictionary of name:item pairs""" + return self.items + + def getListItem(self, name): + return self.items[name] + + #def scene(self): + #return self.view.scene() + + def itemTransformChanged(self, item): + #self.emit(QtCore.SIGNAL('itemTransformChanged'), self, item) + self.sigItemTransformChanged.emit(self, item) + + def itemTransformChangeFinished(self, item): + #self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item) + self.sigItemTransformChangeFinished.emit(self, item) + + + +class SelectBox(ROI): + def __init__(self, scalable=False): + #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + ROI.__init__(self, [0,0], [1,1]) + center = [0.5, 0.5] + + if scalable: + self.addScaleHandle([1, 1], center, lockAspect=True) + self.addScaleHandle([0, 0], center, lockAspect=True) + self.addRotateHandle([0, 1], center) + self.addRotateHandle([1, 0], center) + + + + + + + + + + + \ No newline at end of file diff --git a/canvas/CanvasItem.py b/canvas/CanvasItem.py new file mode 100644 index 00000000..3900af2d --- /dev/null +++ b/canvas/CanvasItem.py @@ -0,0 +1,490 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +from pyqtgraph.graphicsItems.ROI import ROI +import pyqtgraph as pg +import TransformGuiTemplate +import debug + +class SelectBox(ROI): + def __init__(self, scalable=False): + #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + ROI.__init__(self, [0,0], [1,1], invertible=True) + center = [0.5, 0.5] + + if scalable: + self.addScaleHandle([1, 1], center, lockAspect=True) + self.addScaleHandle([0, 0], center, lockAspect=True) + self.addRotateHandle([0, 1], center) + self.addRotateHandle([1, 0], center) + +class CanvasItem(QtCore.QObject): + + sigResetUserTransform = QtCore.Signal(object) + sigTransformChangeFinished = QtCore.Signal(object) + sigTransformChanged = QtCore.Signal(object) + + """CanvasItem takes care of managing an item's state--alpha, visibility, z-value, transformations, etc. and + provides a control widget""" + + sigVisibilityChanged = QtCore.Signal(object) + transformCopyBuffer = None + + def __init__(self, item, **opts): + defOpts = {'name': None, 'z': None, 'movable': True, 'scalable': False, 'visible': True, 'parent':None} #'pos': [0,0], 'scale': [1,1], 'angle':0, + defOpts.update(opts) + self.opts = defOpts + self.selectedAlone = False ## whether this item is the only one selected + + QtCore.QObject.__init__(self) + self.canvas = None + self._graphicsItem = item + + parent = self.opts['parent'] + if parent is not None: + self._graphicsItem.setParentItem(parent.graphicsItem()) + self._parentItem = parent + else: + self._parentItem = None + + z = self.opts['z'] + if z is not None: + item.setZValue(z) + + self.ctrl = QtGui.QWidget() + self.layout = QtGui.QGridLayout() + self.layout.setSpacing(0) + self.layout.setContentsMargins(0,0,0,0) + self.ctrl.setLayout(self.layout) + + self.alphaLabel = QtGui.QLabel("Alpha") + self.alphaSlider = QtGui.QSlider() + self.alphaSlider.setMaximum(1023) + self.alphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.alphaSlider.setValue(1023) + self.layout.addWidget(self.alphaLabel, 0, 0) + self.layout.addWidget(self.alphaSlider, 0, 1) + self.resetTransformBtn = QtGui.QPushButton('Reset Transform') + self.copyBtn = QtGui.QPushButton('Copy') + self.pasteBtn = QtGui.QPushButton('Paste') + + self.transformWidget = QtGui.QWidget() + self.transformGui = TransformGuiTemplate.Ui_Form() + self.transformGui.setupUi(self.transformWidget) + self.layout.addWidget(self.transformWidget, 3, 0, 1, 2) + self.transformGui.mirrorImageBtn.clicked.connect(self.mirrorY) + + self.layout.addWidget(self.resetTransformBtn, 1, 0, 1, 2) + self.layout.addWidget(self.copyBtn, 2, 0, 1, 1) + self.layout.addWidget(self.pasteBtn, 2, 1, 1, 1) + self.alphaSlider.valueChanged.connect(self.alphaChanged) + self.alphaSlider.sliderPressed.connect(self.alphaPressed) + self.alphaSlider.sliderReleased.connect(self.alphaReleased) + #self.canvas.sigSelectionChanged.connect(self.selectionChanged) + self.resetTransformBtn.clicked.connect(self.resetTransformClicked) + self.copyBtn.clicked.connect(self.copyClicked) + self.pasteBtn.clicked.connect(self.pasteClicked) + + self.setMovable(self.opts['movable']) ## update gui to reflect this option + + + if 'transform' in self.opts: + self.baseTransform = self.opts['transform'] + else: + self.baseTransform = pg.Transform() + if 'pos' in self.opts and self.opts['pos'] is not None: + self.baseTransform.translate(self.opts['pos']) + if 'angle' in self.opts and self.opts['angle'] is not None: + self.baseTransform.rotate(self.opts['angle']) + if 'scale' in self.opts and self.opts['scale'] is not None: + self.baseTransform.scale(self.opts['scale']) + + ## create selection box (only visible when selected) + tr = self.baseTransform.saveState() + if 'scalable' not in opts and tr['scale'] == (1,1): + self.opts['scalable'] = True + + ## every CanvasItem implements its own individual selection box + ## so that subclasses are free to make their own. + self.selectBox = SelectBox(scalable=self.opts['scalable']) + #self.canvas.scene().addItem(self.selectBox) + self.selectBox.hide() + self.selectBox.setZValue(1e6) + self.selectBox.sigRegionChanged.connect(self.selectBoxChanged) ## calls selectBoxMoved + self.selectBox.sigRegionChangeFinished.connect(self.selectBoxChangeFinished) + + ## set up the transformations that will be applied to the item + ## (It is not safe to use item.setTransform, since the item might count on that not changing) + self.itemRotation = QtGui.QGraphicsRotation() + self.itemScale = QtGui.QGraphicsScale() + self._graphicsItem.setTransformations([self.itemRotation, self.itemScale]) + + self.tempTransform = pg.Transform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. + self.userTransform = pg.Transform() ## stores the total transform of the object + self.resetUserTransform() + + ## now happens inside resetUserTransform -> selectBoxToItem + # self.selectBoxBase = self.selectBox.getState().copy() + + + #print "Created canvas item", self + #print " base:", self.baseTransform + #print " user:", self.userTransform + #print " temp:", self.tempTransform + #print " bounds:", self.item.sceneBoundingRect() + + def setMovable(self, m): + self.opts['movable'] = m + + if m: + self.resetTransformBtn.show() + self.copyBtn.show() + self.pasteBtn.show() + else: + self.resetTransformBtn.hide() + self.copyBtn.hide() + self.pasteBtn.hide() + + def setCanvas(self, canvas): + ## Called by canvas whenever the item is added. + ## It is our responsibility to add all graphicsItems to the canvas's scene + ## The canvas will automatically add our graphicsitem, + ## so we just need to take care of the selectbox. + if canvas is self.canvas: + return + + if canvas is None: + self.canvas.removeFromScene(self._graphicsItem) + self.canvas.removeFromScene(self.selectBox) + else: + canvas.addToScene(self._graphicsItem) + canvas.addToScene(self.selectBox) + self.canvas = canvas + + def graphicsItem(self): + """Return the graphicsItem for this canvasItem.""" + return self._graphicsItem + + def parentItem(self): + return self._parentItem + + def setParentItem(self, parent): + self._parentItem = parent + if parent is not None: + if isinstance(parent, CanvasItem): + parent = parent.graphicsItem() + self.graphicsItem().setParentItem(parent) + + #def name(self): + #return self.opts['name'] + + def copyClicked(self): + CanvasItem.transformCopyBuffer = self.saveTransform() + + def pasteClicked(self): + t = CanvasItem.transformCopyBuffer + if t is None: + return + else: + self.restoreTransform(t) + + def mirrorY(self): + if not self.isMovable(): + return + + #flip = self.transformGui.mirrorImageCheck.isChecked() + #tr = self.userTransform.saveState() + + inv = pg.Transform() + inv.scale(-1, 1) + self.userTransform = self.userTransform * inv + self.updateTransform() + self.selectBoxFromUser() + #if flip: + #if tr['scale'][0] < 0 xor tr['scale'][1] < 0: + #return + #else: + #self.userTransform.setScale([-tr['scale'][0], tr['scale'][1]]) + #self.userTransform.setTranslate([-tr['pos'][0], tr['pos'][1]]) + #self.userTransform.setRotate(-tr['angle']) + #self.updateTransform() + #self.selectBoxFromUser() + #return + #elif not flip: + #if tr['scale'][0] > 0 and tr['scale'][1] > 0: + #return + #else: + #self.userTransform.setScale([-tr['scale'][0], tr['scale'][1]]) + #self.userTransform.setTranslate([-tr['pos'][0], tr['pos'][1]]) + #self.userTransform.setRotate(-tr['angle']) + #self.updateTransform() + #self.selectBoxFromUser() + #return + + def hasUserTransform(self): + #print self.userRotate, self.userTranslate + return not self.userTransform.isIdentity() + + def ctrlWidget(self): + return self.ctrl + + def alphaChanged(self, val): + alpha = val / 1023. + self._graphicsItem.setOpacity(alpha) + + def isMovable(self): + return self.opts['movable'] + + + def selectBoxMoved(self): + """The selection box has moved; get its transformation information and pass to the graphics item""" + self.userTransform = self.selectBox.getGlobalTransform(relativeTo=self.selectBoxBase) + self.updateTransform() + + def scale(self, x, y): + self.userTransform.scale(x, y) + self.selectBoxFromUser() + self.updateTransform() + + def rotate(self, ang): + self.userTransform.rotate(ang) + self.selectBoxFromUser() + self.updateTransform() + + def translate(self, x, y): + self.userTransform.translate(x, y) + self.selectBoxFromUser() + self.updateTransform() + + def setTranslate(self, x, y): + self.userTransform.setTranslate(x, y) + self.selectBoxFromUser() + self.updateTransform() + + def setRotate(self, angle): + self.userTransform.setRotate(angle) + self.selectBoxFromUser() + self.updateTransform() + + def setScale(self, x, y): + self.userTransform.setScale(x, y) + self.selectBoxFromUser() + self.updateTransform() + + + def setTemporaryTransform(self, transform): + self.tempTransform = transform + self.updateTransform() + + def applyTemporaryTransform(self): + """Collapses tempTransform into UserTransform, resets tempTransform""" + self.userTransform = self.userTransform * self.tempTransform ## order is important! + self.resetTemporaryTransform() + self.selectBoxFromUser() ## update the selection box to match the new userTransform + + #st = self.userTransform.saveState() + + #self.userTransform = self.userTransform * self.tempTransform ## order is important! + + #### matrix multiplication affects the scale factors, need to reset + #if st['scale'][0] < 0 or st['scale'][1] < 0: + #nst = self.userTransform.saveState() + #self.userTransform.setScale([-nst['scale'][0], -nst['scale'][1]]) + + #self.resetTemporaryTransform() + #self.selectBoxFromUser() + #self.selectBoxChangeFinished() + + + + def resetTemporaryTransform(self): + self.tempTransform = pg.Transform() ## don't use Transform.reset()--this transform might be used elsewhere. + self.updateTransform() + + def transform(self): + return self._graphicsItem.transform() + + def updateTransform(self): + """Regenerate the item position from the base, user, and temp transforms""" + transform = self.baseTransform * self.userTransform * self.tempTransform ## order is important + + s = transform.saveState() + self._graphicsItem.setPos(*s['pos']) + + self.itemRotation.setAngle(s['angle']) + self.itemScale.setXScale(s['scale'][0]) + self.itemScale.setYScale(s['scale'][1]) + + self.displayTransform(transform) + + def displayTransform(self, transform): + """Updates transform numbers in the ctrl widget.""" + + tr = transform.saveState() + + self.transformGui.translateLabel.setText("Translate: (%f, %f)" %(tr['pos'][0], tr['pos'][1])) + self.transformGui.rotateLabel.setText("Rotate: %f degrees" %tr['angle']) + self.transformGui.scaleLabel.setText("Scale: (%f, %f)" %(tr['scale'][0], tr['scale'][1])) + #self.transformGui.mirrorImageCheck.setChecked(False) + #if tr['scale'][0] < 0: + # self.transformGui.mirrorImageCheck.setChecked(True) + + + def resetUserTransform(self): + #self.userRotate = 0 + #self.userTranslate = pg.Point(0,0) + self.userTransform.reset() + self.updateTransform() + + self.selectBox.blockSignals(True) + self.selectBoxToItem() + self.selectBox.blockSignals(False) + self.sigTransformChanged.emit(self) + self.sigTransformChangeFinished.emit(self) + + def resetTransformClicked(self): + self.resetUserTransform() + self.sigResetUserTransform.emit(self) + + def restoreTransform(self, tr): + try: + #self.userTranslate = pg.Point(tr['trans']) + #self.userRotate = tr['rot'] + self.userTransform = pg.Transform(tr) + self.updateTransform() + + self.selectBoxFromUser() ## move select box to match + self.sigTransformChanged.emit(self) + self.sigTransformChangeFinished.emit(self) + except: + #self.userTranslate = pg.Point([0,0]) + #self.userRotate = 0 + self.userTransform = pg.Transform() + debug.printExc("Failed to load transform:") + #print "set transform", self, self.userTranslate + + def saveTransform(self): + """Return a dict containing the current user transform""" + #print "save transform", self, self.userTranslate + #return {'trans': list(self.userTranslate), 'rot': self.userRotate} + return self.userTransform.saveState() + + def selectBoxFromUser(self): + """Move the selection box to match the current userTransform""" + ## user transform + #trans = QtGui.QTransform() + #trans.translate(*self.userTranslate) + #trans.rotate(-self.userRotate) + + #x2, y2 = trans.map(*self.selectBoxBase['pos']) + + self.selectBox.blockSignals(True) + self.selectBox.setState(self.selectBoxBase) + self.selectBox.applyGlobalTransform(self.userTransform) + #self.selectBox.setAngle(self.userRotate) + #self.selectBox.setPos([x2, y2]) + self.selectBox.blockSignals(False) + + + def selectBoxToItem(self): + """Move/scale the selection box so it fits the item's bounding rect. (assumes item is not rotated)""" + self.itemRect = self._graphicsItem.boundingRect() + rect = self._graphicsItem.mapRectToParent(self.itemRect) + self.selectBox.blockSignals(True) + self.selectBox.setPos([rect.x(), rect.y()]) + self.selectBox.setSize(rect.size()) + self.selectBox.setAngle(0) + self.selectBoxBase = self.selectBox.getState().copy() + self.selectBox.blockSignals(False) + + def zValue(self): + return self.opts['z'] + + def setZValue(self, z): + self.opts['z'] = z + if z is not None: + self._graphicsItem.setZValue(z) + + #def selectionChanged(self, canvas, items): + #self.selected = len(items) == 1 and (items[0] is self) + #self.showSelectBox() + + + def selectionChanged(self, sel, multi): + """ + Inform the item that its selection state has changed. + Arguments: + sel: bool, whether the item is currently selected + multi: bool, whether there are multiple items currently selected + """ + self.selectedAlone = sel and not multi + self.showSelectBox() + if self.selectedAlone: + self.ctrlWidget().show() + else: + self.ctrlWidget().hide() + + def showSelectBox(self): + """Display the selection box around this item if it is selected and movable""" + if self.selectedAlone and self.isMovable() and self.isVisible(): #and len(self.canvas.itemList.selectedItems())==1: + self.selectBox.show() + else: + self.selectBox.hide() + + def hideSelectBox(self): + self.selectBox.hide() + + + def selectBoxChanged(self): + self.selectBoxMoved() + #self.updateTransform(self.selectBox) + #self.emit(QtCore.SIGNAL('transformChanged'), self) + self.sigTransformChanged.emit(self) + + def selectBoxChangeFinished(self): + #self.emit(QtCore.SIGNAL('transformChangeFinished'), self) + self.sigTransformChangeFinished.emit(self) + + def alphaPressed(self): + """Hide selection box while slider is moving""" + self.hideSelectBox() + + def alphaReleased(self): + self.showSelectBox() + + def show(self): + if self.opts['visible']: + return + self.opts['visible'] = True + self._graphicsItem.show() + self.showSelectBox() + self.sigVisibilityChanged.emit(self) + + def hide(self): + if not self.opts['visible']: + return + self.opts['visible'] = False + self._graphicsItem.hide() + self.hideSelectBox() + self.sigVisibilityChanged.emit(self) + + def setVisible(self, vis): + if vis: + self.show() + else: + self.hide() + + def isVisible(self): + return self.opts['visible'] + + +class GroupCanvasItem(CanvasItem): + """ + Canvas item used for grouping others + """ + + def __init__(self, **opts): + defOpts = {'movable': False, 'scalable': False} + defOpts.update(opts) + item = pg.ItemGroup() + CanvasItem.__init__(self, item, **defOpts) + diff --git a/canvas/CanvasManager.py b/canvas/CanvasManager.py new file mode 100644 index 00000000..0c64e274 --- /dev/null +++ b/canvas/CanvasManager.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal +import weakref + +class CanvasManager(QtCore.QObject): + SINGLETON = None + + sigCanvasListChanged = QtCore.Signal() + + def __init__(self): + if CanvasManager.SINGLETON is not None: + raise Exception("Can only create one canvas manager.") + CanvasManager.SINGLETON = self + QtCore.QObject.__init__(self) + self.canvases = weakref.WeakValueDictionary() + + @classmethod + def instance(cls): + return CanvasManager.SINGLETON + + def registerCanvas(self, canvas, name): + n2 = name + i = 0 + while n2 in self.canvases: + n2 = "%s_%03d" % (name, i) + i += 1 + self.canvases[n2] = canvas + self.sigCanvasListChanged.emit() + return n2 + + def unregisterCanvas(self, name): + c = self.canvases[name] + del self.canvases[name] + self.sigCanvasListChanged.emit() + + def listCanvases(self): + return self.canvases.keys() + + def getCanvas(self, name): + return self.canvases[name] + + +manager = CanvasManager() + + +class CanvasCombo(QtGui.QComboBox): + def __init__(self, parent=None): + QtGui.QComboBox.__init__(self, parent) + man = CanvasManager.instance() + man.sigCanvasListChanged.connect(self.updateCanvasList) + self.hostName = None + self.updateCanvasList() + + def updateCanvasList(self): + canvases = CanvasManager.instance().listCanvases() + canvases.insert(0, "") + if self.hostName in canvases: + canvases.remove(self.hostName) + + sel = self.currentText() + if sel in canvases: + self.blockSignals(True) ## change does not affect current selection; block signals during update + self.clear() + for i in canvases: + self.addItem(i) + if i == sel: + self.setCurrentIndex(self.count()) + + self.blockSignals(False) + + def setHostName(self, name): + self.hostName = name + self.updateCanvasList() + diff --git a/canvas/CanvasTemplate.py b/canvas/CanvasTemplate.py new file mode 100644 index 00000000..c525b705 --- /dev/null +++ b/canvas/CanvasTemplate.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CanvasTemplate.ui' +# +# Created: Sun Dec 18 20:04:41 2011 +# by: PyQt4 UI code generator 4.8.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(466, 422) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setMargin(0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName(_fromUtf8("splitter")) + self.view = GraphicsView(self.splitter) + self.view.setObjectName(_fromUtf8("view")) + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) + self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) + self.gridLayout_2.setMargin(0) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) + self.autoRangeBtn.setSizePolicy(sizePolicy) + self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn")) + self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.itemList = TreeWidget(self.layoutWidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) + self.itemList.setSizePolicy(sizePolicy) + self.itemList.setHeaderHidden(True) + self.itemList.setObjectName(_fromUtf8("itemList")) + self.itemList.headerItem().setText(0, _fromUtf8("1")) + self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) + self.ctrlLayout = QtGui.QGridLayout() + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout")) + self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) + self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget) + self.storeSvgBtn.setObjectName(_fromUtf8("storeSvgBtn")) + self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) + self.storePngBtn = QtGui.QPushButton(self.layoutWidget) + self.storePngBtn.setObjectName(_fromUtf8("storePngBtn")) + self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) + self.redirectCheck.setObjectName(_fromUtf8("redirectCheck")) + self.horizontalLayout.addWidget(self.redirectCheck) + self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo.setObjectName(_fromUtf8("redirectCombo")) + self.horizontalLayout.addWidget(self.redirectCombo) + self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) + self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 8, 0, 1, 1) + self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) + self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) + self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 1, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8)) + self.storeSvgBtn.setText(QtGui.QApplication.translate("Form", "Store SVG", None, QtGui.QApplication.UnicodeUTF8)) + self.storePngBtn.setText(QtGui.QApplication.translate("Form", "Store PNG", None, QtGui.QApplication.UnicodeUTF8)) + self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8)) + self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8)) + self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) + self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.GraphicsView import GraphicsView +from CanvasManager import CanvasCombo +from pyqtgraph.widgets.TreeWidget import TreeWidget diff --git a/canvas/CanvasTemplate.ui b/canvas/CanvasTemplate.ui new file mode 100644 index 00000000..b104c84c --- /dev/null +++ b/canvas/CanvasTemplate.ui @@ -0,0 +1,142 @@ + + + Form + + + + 0 + 0 + 466 + 422 + + + + Form + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + + + + + 0 + 1 + + + + Auto Range + + + + + + + + 0 + 100 + + + + true + + + + 1 + + + + + + + + 0 + + + + + + + Store SVG + + + + + + + Store PNG + + + + + + + 0 + + + + + Check to display all local items in a remote canvas. + + + Redirect + + + + + + + + + + + + Mirror Selection + + + + + + + Reset Transforms + + + + + + + + + + + + TreeWidget + QTreeWidget +
pyqtgraph.widgets.TreeWidget
+
+ + GraphicsView + QGraphicsView +
pyqtgraph.widgets.GraphicsView
+
+ + CanvasCombo + QComboBox +
CanvasManager
+
+
+ + +
diff --git a/canvas/TransformGuiTemplate.py b/canvas/TransformGuiTemplate.py new file mode 100644 index 00000000..5ffc3f08 --- /dev/null +++ b/canvas/TransformGuiTemplate.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'TransformGuiTemplate.ui' +# +# Created: Sun Dec 18 20:04:40 2011 +# by: PyQt4 UI code generator 4.8.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(169, 82) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + self.verticalLayout = QtGui.QVBoxLayout(Form) + self.verticalLayout.setSpacing(1) + self.verticalLayout.setMargin(0) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.translateLabel = QtGui.QLabel(Form) + self.translateLabel.setObjectName(_fromUtf8("translateLabel")) + self.verticalLayout.addWidget(self.translateLabel) + self.rotateLabel = QtGui.QLabel(Form) + self.rotateLabel.setObjectName(_fromUtf8("rotateLabel")) + self.verticalLayout.addWidget(self.rotateLabel) + self.scaleLabel = QtGui.QLabel(Form) + self.scaleLabel.setObjectName(_fromUtf8("scaleLabel")) + self.verticalLayout.addWidget(self.scaleLabel) + self.mirrorImageBtn = QtGui.QPushButton(Form) + self.mirrorImageBtn.setToolTip(_fromUtf8("")) + self.mirrorImageBtn.setObjectName(_fromUtf8("mirrorImageBtn")) + self.verticalLayout.addWidget(self.mirrorImageBtn) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.translateLabel.setText(QtGui.QApplication.translate("Form", "Translate:", None, QtGui.QApplication.UnicodeUTF8)) + self.rotateLabel.setText(QtGui.QApplication.translate("Form", "Rotate:", None, QtGui.QApplication.UnicodeUTF8)) + self.scaleLabel.setText(QtGui.QApplication.translate("Form", "Scale:", None, QtGui.QApplication.UnicodeUTF8)) + self.mirrorImageBtn.setText(QtGui.QApplication.translate("Form", "Mirror", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/canvas/TransformGuiTemplate.ui b/canvas/TransformGuiTemplate.ui new file mode 100644 index 00000000..c8c24a95 --- /dev/null +++ b/canvas/TransformGuiTemplate.ui @@ -0,0 +1,64 @@ + + + Form + + + + 0 + 0 + 169 + 82 + + + + + 0 + 0 + + + + Form + + + + 1 + + + 0 + + + + + Translate: + + + + + + + Rotate: + + + + + + + Scale: + + + + + + + + + + Mirror + + + + + + + + diff --git a/canvas/__init__.py b/canvas/__init__.py new file mode 100644 index 00000000..d7d3058e --- /dev/null +++ b/canvas/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from Canvas import * +from CanvasItem import * \ No newline at end of file diff --git a/debug.py b/debug.py index 2a2157db..13fdbee4 100644 --- a/debug.py +++ b/debug.py @@ -8,7 +8,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation. import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile import ptime from numpy import ndarray -from PyQt4 import QtCore, QtGui +from Qt import QtCore, QtGui __ftraceDepth = 0 def ftrace(func): diff --git a/debug.pyc b/debug.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b44753ef934601e96d6d22a76eff91270cefea97 GIT binary patch literal 29902 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 Ow|Ag-aQ~66;Qs(Npy)yX literal 0 HcmV?d00001 diff --git a/dockarea/Container.py b/dockarea/Container.py new file mode 100644 index 00000000..a254d474 --- /dev/null +++ b/dockarea/Container.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +import weakref + +class Container(object): + #sigStretchChanged = QtCore.Signal() ## can't do this here; not a QObject. + + def __init__(self, area): + object.__init__(self) + self.area = area + self._container = None + self._stretch = (10, 10) + self.stretches = weakref.WeakKeyDictionary() + + def container(self): + return self._container + + def containerChanged(self, c): + self._container = c + + def type(self): + return None + + def insert(self, new, pos=None, neighbor=None): + if not isinstance(new, list): + new = [new] + if neighbor is None: + if pos == 'before': + index = 0 + else: + index = self.count() + else: + index = self.indexOf(neighbor) + if index == -1: + index = 0 + if pos == 'after': + index += 1 + + for n in new: + #print "change container", n, " -> ", self + n.containerChanged(self) + #print "insert", n, " -> ", self, index + self._insertItem(n, index) + index += 1 + n.sigStretchChanged.connect(self.childStretchChanged) + #print "child added", self + self.updateStretch() + + def apoptose(self, propagate=True): + ##if there is only one (or zero) item in this container, disappear. + cont = self._container + c = self.count() + if c > 1: + return + if self.count() == 1: ## if there is one item, give it to the parent container (unless this is the top) + if self is self.area.topContainer: + return + self.container().insert(self.widget(0), 'before', self) + #print "apoptose:", self + self.close() + if propagate and cont is not None: + cont.apoptose() + + def close(self): + self.area = None + self._container = None + self.setParent(None) + + def childEvent(self, ev): + ch = ev.child() + if ev.removed() and hasattr(ch, 'sigStretchChanged'): + #print "Child", ev.child(), "removed, updating", self + try: + ch.sigStretchChanged.disconnect(self.childStretchChanged) + except: + pass + self.updateStretch() + + def childStretchChanged(self): + #print "child", QtCore.QObject.sender(self), "changed shape, updating", self + self.updateStretch() + + def setStretch(self, x=None, y=None): + #print "setStretch", self, x, y + self._stretch = (x, y) + self.sigStretchChanged.emit() + + def updateStretch(self): + ###Set the stretch values for this container to reflect its contents + pass + + + def stretch(self): + """Return the stretch factors for this container""" + return self._stretch + + +class SplitContainer(Container, QtGui.QSplitter): + """Horizontal or vertical splitter with some changes: + - save/restore works correctly + """ + sigStretchChanged = QtCore.Signal() + + def __init__(self, area, orientation): + QtGui.QSplitter.__init__(self) + self.setOrientation(orientation) + Container.__init__(self, area) + #self.splitterMoved.connect(self.restretchChildren) + + def _insertItem(self, item, index): + self.insertWidget(index, item) + item.show() ## need to show since it may have been previously hidden by tab + + def saveState(self): + sizes = self.sizes() + if all([x == 0 for x in sizes]): + sizes = [10] * len(sizes) + return {'sizes': sizes} + + def restoreState(self, state): + sizes = state['sizes'] + self.setSizes(sizes) + for i in range(len(sizes)): + self.setStretchFactor(i, sizes[i]) + + def childEvent(self, ev): + QtGui.QSplitter.childEvent(self, ev) + Container.childEvent(self, ev) + + #def restretchChildren(self): + #sizes = self.sizes() + #tot = sum(sizes) + + + + +class HContainer(SplitContainer): + def __init__(self, area): + SplitContainer.__init__(self, area, QtCore.Qt.Horizontal) + + def type(self): + return 'horizontal' + + def updateStretch(self): + ##Set the stretch values for this container to reflect its contents + #print "updateStretch", self + x = 0 + y = 0 + sizes = [] + for i in range(self.count()): + wx, wy = self.widget(i).stretch() + x += wx + y = max(y, wy) + sizes.append(wx) + #print " child", self.widget(i), wx, wy + self.setStretch(x, y) + #print sizes + + tot = float(sum(sizes)) + if tot == 0: + scale = 1.0 + else: + scale = self.width() / tot + self.setSizes([int(s*scale) for s in sizes]) + + + +class VContainer(SplitContainer): + def __init__(self, area): + SplitContainer.__init__(self, area, QtCore.Qt.Vertical) + + def type(self): + return 'vertical' + + def updateStretch(self): + ##Set the stretch values for this container to reflect its contents + #print "updateStretch", self + x = 0 + y = 0 + sizes = [] + for i in range(self.count()): + wx, wy = self.widget(i).stretch() + y += wy + x = max(x, wx) + sizes.append(wy) + #print " child", self.widget(i), wx, wy + self.setStretch(x, y) + + #print sizes + tot = float(sum(sizes)) + if tot == 0: + scale = 1.0 + else: + scale = self.height() / tot + self.setSizes([int(s*scale) for s in sizes]) + + +class TContainer(Container, QtGui.QWidget): + sigStretchChanged = QtCore.Signal() + def __init__(self, area): + QtGui.QWidget.__init__(self) + Container.__init__(self, area) + self.layout = QtGui.QGridLayout() + self.layout.setSpacing(0) + self.layout.setContentsMargins(0,0,0,0) + self.setLayout(self.layout) + + self.hTabLayout = QtGui.QHBoxLayout() + self.hTabBox = QtGui.QWidget() + self.hTabBox.setLayout(self.hTabLayout) + self.hTabLayout.setSpacing(2) + self.hTabLayout.setContentsMargins(0,0,0,0) + self.layout.addWidget(self.hTabBox, 0, 1) + + self.stack = QtGui.QStackedWidget() + self.layout.addWidget(self.stack, 1, 1) + self.stack.childEvent = self.stackChildEvent + + + self.setLayout(self.layout) + for n in ['count', 'widget', 'indexOf']: + setattr(self, n, getattr(self.stack, n)) + + + def _insertItem(self, item, index): + if not isinstance(item, Dock.Dock): + raise Exception("Tab containers may hold only docks, not other containers.") + self.stack.insertWidget(index, item) + self.hTabLayout.insertWidget(index, item.label) + #QtCore.QObject.connect(item.label, QtCore.SIGNAL('clicked'), self.tabClicked) + item.label.sigClicked.connect(self.tabClicked) + self.tabClicked(item.label) + + def tabClicked(self, tab, ev=None): + if ev is None or ev.button() == QtCore.Qt.LeftButton: + for i in range(self.count()): + w = self.widget(i) + if w is tab.dock: + w.label.setDim(False) + self.stack.setCurrentIndex(i) + else: + w.label.setDim(True) + + def type(self): + return 'tab' + + def saveState(self): + return {'index': self.stack.currentIndex()} + + def restoreState(self, state): + self.stack.setCurrentIndex(state['index']) + + def updateStretch(self): + ##Set the stretch values for this container to reflect its contents + x = 0 + y = 0 + for i in range(self.count()): + wx, wy = self.widget(i).stretch() + x = max(x, wx) + y = max(y, wy) + self.setStretch(x, y) + + def stackChildEvent(self, ev): + QtGui.QStackedWidget.childEvent(self.stack, ev) + Container.childEvent(self, ev) + +import Dock diff --git a/dockarea/Dock.py b/dockarea/Dock.py new file mode 100644 index 00000000..3b058ba4 --- /dev/null +++ b/dockarea/Dock.py @@ -0,0 +1,350 @@ +from pyqtgraph.Qt import QtCore, QtGui + +from DockDrop import * +from pyqtgraph.widgets.VerticalLabel import VerticalLabel + +class Dock(QtGui.QWidget, DockDrop): + + sigStretchChanged = QtCore.Signal() + + def __init__(self, name, area=None, size=(10, 10)): + QtGui.QWidget.__init__(self) + DockDrop.__init__(self) + self.area = area + self.label = DockLabel(name, self) + self.labelHidden = False + self.moveLabel = True ## If false, the dock is no longer allowed to move the label. + self.autoOrient = True + self.orientation = 'horizontal' + #self.label.setAlignment(QtCore.Qt.AlignHCenter) + self.topLayout = QtGui.QGridLayout() + self.topLayout.setContentsMargins(0, 0, 0, 0) + self.topLayout.setSpacing(0) + self.setLayout(self.topLayout) + self.topLayout.addWidget(self.label, 0, 1) + self.widgetArea = QtGui.QWidget() + self.topLayout.addWidget(self.widgetArea, 1, 1) + self.layout = QtGui.QGridLayout() + self.layout.setContentsMargins(0, 0, 0, 0) + self.layout.setSpacing(0) + self.widgetArea.setLayout(self.layout) + self.widgetArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + self.widgets = [] + self.currentRow = 0 + #self.titlePos = 'top' + self.raiseOverlay() + self.hStyle = """ + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-top-width: 0px; + }""" + self.vStyle = """ + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-left-width: 0px; + }""" + self.nStyle = """ + Dock > QWidget { + border: 1px solid #000; + border-radius: 5px; + }""" + self.dragStyle = """ + Dock > QWidget { + border: 4px solid #00F; + border-radius: 5px; + }""" + self.setAutoFillBackground(False) + self.widgetArea.setStyleSheet(self.hStyle) + + self.setStretch(*size) + + def setStretch(self, x=None, y=None): + #print "setStretch", self, x, y + #self._stretch = (x, y) + if x is None: + x = 0 + if y is None: + y = 0 + #policy = self.sizePolicy() + #policy.setHorizontalStretch(x) + #policy.setVerticalStretch(y) + #self.setSizePolicy(policy) + self._stretch = (x, y) + self.sigStretchChanged.emit() + #print "setStretch", self, x, y, self.stretch() + + def stretch(self): + #policy = self.sizePolicy() + #return policy.horizontalStretch(), policy.verticalStretch() + return self._stretch + + #def stretch(self): + #return self._stretch + + def hideTitleBar(self): + self.label.hide() + self.labelHidden = True + if 'center' in self.allowedAreas: + self.allowedAreas.remove('center') + self.updateStyle() + + def showTitleBar(self): + self.label.show() + self.labelHidden = False + self.allowedAreas.add('center') + self.updateStyle() + + def setOrientation(self, o='auto', force=False): + #print self.name(), "setOrientation", o, force + if o == 'auto': + if self.container().type() == 'tab': + o = 'horizontal' + elif self.width() > self.height()*1.5: + o = 'vertical' + else: + o = 'horizontal' + if force or self.orientation != o: + self.orientation = o + self.label.setOrientation(o) + self.updateStyle() + + def updateStyle(self): + #print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible() + if self.labelHidden: + self.widgetArea.setStyleSheet(self.nStyle) + elif self.orientation == 'vertical': + self.label.setOrientation('vertical') + if self.moveLabel: + #print self.name(), "reclaim label" + self.topLayout.addWidget(self.label, 1, 0) + self.widgetArea.setStyleSheet(self.vStyle) + else: + self.label.setOrientation('horizontal') + if self.moveLabel: + #print self.name(), "reclaim label" + self.topLayout.addWidget(self.label, 0, 1) + self.widgetArea.setStyleSheet(self.hStyle) + + def resizeEvent(self, ev): + self.setOrientation() + self.resizeOverlay(self.size()) + + def name(self): + return str(self.label.text()) + + def container(self): + return self._container + + def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1): + if row is None: + row = self.currentRow + self.currentRow = max(row+1, self.currentRow) + self.widgets.append(widget) + self.layout.addWidget(widget, row, col, rowspan, colspan) + self.raiseOverlay() + + + def startDrag(self): + self.drag = QtGui.QDrag(self) + mime = QtCore.QMimeData() + #mime.setPlainText("asd") + self.drag.setMimeData(mime) + self.widgetArea.setStyleSheet(self.dragStyle) + self.update() + action = self.drag.exec_() + self.updateStyle() + + def float(self): + self.area.floatDock(self) + + def containerChanged(self, c): + #print self.name(), "container changed" + self._container = c + if c.type() != 'tab': + self.moveLabel = True + self.label.setDim(False) + else: + self.moveLabel = False + + self.setOrientation(force=True) + + def __repr__(self): + return "" % (self.name(), self.stretch()) + +class DockLabel(VerticalLabel): + + sigClicked = QtCore.Signal(object, object) + + def __init__(self, text, dock): + self.dim = False + self.fixedWidth = False + VerticalLabel.__init__(self, text, orientation='horizontal', forceWidth=False) + self.setAlignment(QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter) + self.dock = dock + self.updateStyle() + self.setAutoFillBackground(False) + + #def minimumSizeHint(self): + ##sh = QtGui.QWidget.minimumSizeHint(self) + #return QtCore.QSize(20, 20) + + def updateStyle(self): + r = '3px' + if self.dim: + fg = '#aaa' + bg = '#44a' + border = '#339' + else: + fg = '#fff' + bg = '#66c' + border = '#55B' + + if self.orientation == 'vertical': + self.vStyle = """DockLabel { + background-color : %s; + color : %s; + border-top-right-radius: 0px; + border-top-left-radius: %s; + border-bottom-right-radius: 0px; + border-bottom-left-radius: %s; + border-width: 0px; + border-right: 2px solid %s; + padding-top: 3px; + padding-bottom: 3px; + }""" % (bg, fg, r, r, border) + self.setStyleSheet(self.vStyle) + else: + self.hStyle = """DockLabel { + background-color : %s; + color : %s; + border-top-right-radius: %s; + border-top-left-radius: %s; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + border-width: 0px; + border-bottom: 2px solid %s; + padding-left: 3px; + padding-right: 3px; + }""" % (bg, fg, r, r, border) + self.setStyleSheet(self.hStyle) + + def setDim(self, d): + if self.dim != d: + self.dim = d + self.updateStyle() + + def setOrientation(self, o): + VerticalLabel.setOrientation(self, o) + self.updateStyle() + + def mousePressEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + self.pressPos = ev.pos() + self.startedDrag = False + ev.accept() + + def mouseMoveEvent(self, ev): + if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance(): + self.dock.startDrag() + ev.accept() + #print ev.pos() + + def mouseReleaseEvent(self, ev): + if not self.startedDrag: + #self.emit(QtCore.SIGNAL('clicked'), self, ev) + self.sigClicked.emit(self, ev) + ev.accept() + + def mouseDoubleClickEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + self.dock.float() + + #def paintEvent(self, ev): + #p = QtGui.QPainter(self) + ##p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200))) + #p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100))) + #p.drawRect(self.rect().adjusted(0, 0, -1, -1)) + + #VerticalLabel.paintEvent(self, ev) + + + +#class DockLabel(QtGui.QWidget): + #def __init__(self, text, dock): + #QtGui.QWidget.__init__(self) + #self._text = text + #self.dock = dock + #self.orientation = None + #self.setOrientation('horizontal') + + #def text(self): + #return self._text + + #def mousePressEvent(self, ev): + #if ev.button() == QtCore.Qt.LeftButton: + #self.pressPos = ev.pos() + #self.startedDrag = False + #ev.accept() + + #def mouseMoveEvent(self, ev): + #if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance(): + #self.dock.startDrag() + #ev.accept() + ##print ev.pos() + + #def mouseReleaseEvent(self, ev): + #ev.accept() + + #def mouseDoubleClickEvent(self, ev): + #if ev.button() == QtCore.Qt.LeftButton: + #self.dock.float() + + #def setOrientation(self, o): + #if self.orientation == o: + #return + #self.orientation = o + #self.update() + #self.updateGeometry() + + #def paintEvent(self, ev): + #p = QtGui.QPainter(self) + #p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200))) + #p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100))) + #p.drawRect(self.rect().adjusted(0, 0, -1, -1)) + + #p.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255))) + + #if self.orientation == 'vertical': + #p.rotate(-90) + #rgn = QtCore.QRect(-self.height(), 0, self.height(), self.width()) + #else: + #rgn = self.rect() + #align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter + + #self.hint = p.drawText(rgn, align, self.text()) + #p.end() + + #if self.orientation == 'vertical': + #self.setMaximumWidth(self.hint.height()) + #self.setMaximumHeight(16777215) + #else: + #self.setMaximumHeight(self.hint.height()) + #self.setMaximumWidth(16777215) + + #def sizeHint(self): + #if self.orientation == 'vertical': + #if hasattr(self, 'hint'): + #return QtCore.QSize(self.hint.height(), self.hint.width()) + #else: + #return QtCore.QSize(19, 50) + #else: + #if hasattr(self, 'hint'): + #return QtCore.QSize(self.hint.width(), self.hint.height()) + #else: + #return QtCore.QSize(50, 19) diff --git a/dockarea/DockArea.py b/dockarea/DockArea.py new file mode 100644 index 00000000..88e699d5 --- /dev/null +++ b/dockarea/DockArea.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +from Container import * +from DockDrop import * +import pyqtgraph.debug as debug +import weakref + +## TODO: +# - containers should be drop areas, not docks. (but every slot within a container must have its own drop areas?) +# - drop between tabs +# - nest splitters inside tab boxes, etc. + + + + +class DockArea(Container, QtGui.QWidget, DockDrop): + def __init__(self, temporary=False, home=None): + Container.__init__(self, self) + QtGui.QWidget.__init__(self) + DockDrop.__init__(self, allowedAreas=['left', 'right', 'top', 'bottom']) + self.layout = QtGui.QVBoxLayout() + self.layout.setContentsMargins(0,0,0,0) + self.layout.setSpacing(0) + self.setLayout(self.layout) + self.docks = weakref.WeakValueDictionary() + self.topContainer = None + self.raiseOverlay() + self.temporary = temporary + self.tempAreas = [] + self.home = home + + def type(self): + return "top" + + def addDock(self, dock, position='bottom', relativeTo=None): + """Adds a dock to this area. + position may be: bottom, top, left, right, over, under + If relativeTo specifies an existing dock, the new dock is added adjacent to it""" + + ## Determine the container to insert this dock into. + ## If there is no neighbor, then the container is the top. + if relativeTo is None or relativeTo is self: + if self.topContainer is None: + container = self + neighbor = None + else: + container = self.topContainer + neighbor = None + else: + if isinstance(relativeTo, basestring): + relativeTo = self.docks[relativeTo] + container = self.getContainer(relativeTo) + neighbor = relativeTo + + ## what container type do we need? + neededContainer = { + 'bottom': 'vertical', + 'top': 'vertical', + 'left': 'horizontal', + 'right': 'horizontal', + 'above': 'tab', + 'below': 'tab' + }[position] + + ## Can't insert new containers into a tab container; insert outside instead. + if neededContainer != container.type() and container.type() == 'tab': + neighbor = container + container = container.container() + + ## Decide if the container we have is suitable. + ## If not, insert a new container inside. + if neededContainer != container.type(): + if neighbor is None: + container = self.addContainer(neededContainer, self.topContainer) + else: + container = self.addContainer(neededContainer, neighbor) + + ## Insert the new dock before/after its neighbor + insertPos = { + 'bottom': 'after', + 'top': 'before', + 'left': 'before', + 'right': 'after', + 'above': 'before', + 'below': 'after' + }[position] + #print "request insert", dock, insertPos, neighbor + container.insert(dock, insertPos, neighbor) + dock.area = self + self.docks[dock.name()] = dock + + def getContainer(self, obj): + if obj is None: + return self + return obj.container() + + def makeContainer(self, typ): + if typ == 'vertical': + new = VContainer(self) + elif typ == 'horizontal': + new = HContainer(self) + elif typ == 'tab': + new = TContainer(self) + return new + + def addContainer(self, typ, obj): + """Add a new container around obj""" + new = self.makeContainer(typ) + + container = self.getContainer(obj) + container.insert(new, 'before', obj) + #print "Add container:", new, " -> ", container + if obj is not None: + new.insert(obj) + self.raiseOverlay() + return new + + def insert(self, new, pos=None, neighbor=None): + if self.topContainer is not None: + self.topContainer.containerChanged(None) + self.layout.addWidget(new) + self.topContainer = new + #print self, "set top:", new + new._container = self + self.raiseOverlay() + #print "Insert top:", new + + def count(self): + if self.topContainer is None: + return 0 + return 1 + + def moveDock(self, dock, position, neighbor): + old = dock.container() + ## Moving to the edge of a tabbed dock causes a drop outside the tab box + if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab': + neighbor = neighbor.container() + self.addDock(dock, position, neighbor) + old.apoptose() + + #def paintEvent(self, ev): + #self.drawDockOverlay() + + def resizeEvent(self, ev): + self.resizeOverlay(self.size()) + + def addTempArea(self): + if self.home is None: + area = DockArea(temporary=True, home=self) + self.tempAreas.append(area) + win = QtGui.QMainWindow() + win.setCentralWidget(area) + area.win = win + win.show() + else: + area = self.home.addTempArea() + #print "added temp area", area, area.window() + return area + + def floatDock(self, dock): + area = self.addTempArea() + area.win.resize(dock.size()) + area.moveDock(dock, 'top', None) + + + def removeTempArea(self, area): + self.tempAreas.remove(area) + #print "close window", area.window() + area.window().close() + + def saveState(self): + state = {'main': self.childState(self.topContainer), 'float': []} + for a in self.tempAreas: + geo = a.win.geometry() + geo = (geo.x(), geo.y(), geo.width(), geo.height()) + state['float'].append((a.saveState(), geo)) + return state + + def childState(self, obj): + if isinstance(obj, Dock): + return ('dock', obj.name(), {}) + else: + childs = [] + for i in range(obj.count()): + childs.append(self.childState(obj.widget(i))) + return (obj.type(), childs, obj.saveState()) + + + def restoreState(self, state): + ## 1) make dict of all docks and list of existing containers + containers, docks = self.findAll() + oldTemps = self.tempAreas[:] + #print "found docks:", docks + + ## 2) create container structure, move docks into new containers + self.buildFromState(state['main'], docks, self) + + ## 3) create floating areas, populate + for s in state['float']: + a = self.addTempArea() + a.buildFromState(s[0]['main'], docks, a) + a.win.setGeometry(*s[1]) + + ## 4) Add any remaining docks to the bottom + for d in docks.itervalues(): + self.moveDock(d, 'below', None) + + #print "\nKill old containers:" + ## 5) kill old containers + for c in containers: + c.close() + for a in oldTemps: + a.apoptose() + + + def buildFromState(self, state, docks, root, depth=0): + typ, contents, state = state + pfx = " " * depth + if typ == 'dock': + obj = docks[contents] + del docks[contents] + else: + obj = self.makeContainer(typ) + + root.insert(obj, 'after') + #print pfx+"Add:", obj, " -> ", root + + if typ != 'dock': + for o in contents: + self.buildFromState(o, docks, obj, depth+1) + obj.apoptose(propagate=False) + obj.restoreState(state) ## this has to be done later? + + + def findAll(self, obj=None, c=None, d=None): + if obj is None: + obj = self.topContainer + + ## check all temp areas first + if c is None: + c = [] + d = {} + for a in self.tempAreas: + c1, d1 = a.findAll() + c.extend(c1) + d.update(d1) + + if isinstance(obj, Dock): + d[obj.name()] = obj + else: + c.append(obj) + for i in range(obj.count()): + o2 = obj.widget(i) + c2, d2 = self.findAll(o2) + c.extend(c2) + d.update(d2) + return (c, d) + + def apoptose(self): + #print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count() + if self.temporary and self.topContainer.count() == 0: + self.topContainer = None + self.home.removeTempArea(self) + #self.close() + + + \ No newline at end of file diff --git a/dockarea/DockDrop.py b/dockarea/DockDrop.py new file mode 100644 index 00000000..339b28fd --- /dev/null +++ b/dockarea/DockDrop.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui + +class DockDrop(object): + """Provides dock-dropping methods""" + def __init__(self, allowedAreas=None): + object.__init__(self) + if allowedAreas is None: + allowedAreas = ['center', 'right', 'left', 'top', 'bottom'] + self.allowedAreas = set(allowedAreas) + self.setAcceptDrops(True) + self.dropArea = None + self.overlay = DropAreaOverlay(self) + self.overlay.raise_() + + def resizeOverlay(self, size): + self.overlay.resize(size) + + def raiseOverlay(self): + self.overlay.raise_() + + def dragEnterEvent(self, ev): + if isinstance(ev.source(), Dock.Dock): + #print "drag enter accept" + ev.accept() + else: + #print "drag enter ignore" + ev.ignore() + + def dragMoveEvent(self, ev): + #print "drag move" + ld = ev.pos().x() + rd = self.width() - ld + td = ev.pos().y() + bd = self.height() - td + + mn = min(ld, rd, td, bd) + if mn > 30: + self.dropArea = "center" + elif (ld == mn or td == mn) and mn > self.height()/3.: + self.dropArea = "center" + elif (rd == mn or ld == mn) and mn > self.width()/3.: + self.dropArea = "center" + + elif rd == mn: + self.dropArea = "right" + elif ld == mn: + self.dropArea = "left" + elif td == mn: + self.dropArea = "top" + elif bd == mn: + self.dropArea = "bottom" + + if ev.source() is self and self.dropArea == 'center': + #print " no self-center" + self.dropArea = None + ev.ignore() + elif self.dropArea not in self.allowedAreas: + #print " not allowed" + self.dropArea = None + ev.ignore() + else: + #print " ok" + ev.accept() + self.overlay.setDropArea(self.dropArea) + + def dragLeaveEvent(self, ev): + self.dropArea = None + self.overlay.setDropArea(self.dropArea) + + def dropEvent(self, ev): + area = self.dropArea + if area is None: + return + if area == 'center': + area = 'above' + self.area.moveDock(ev.source(), area, self) + self.dropArea = None + self.overlay.setDropArea(self.dropArea) + + + +class DropAreaOverlay(QtGui.QWidget): + """Overlay widget that draws drop areas during a drag-drop operation""" + + def __init__(self, parent): + QtGui.QWidget.__init__(self, parent) + self.dropArea = None + self.hide() + self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) + + def setDropArea(self, area): + self.dropArea = area + if area is None: + self.hide() + else: + ## Resize overlay to just the region where drop area should be displayed. + ## This works around a Qt bug--can't display transparent widgets over QGLWidget + prgn = self.parent().rect() + rgn = QtCore.QRect(prgn) + w = min(30, prgn.width()/3.) + h = min(30, prgn.height()/3.) + + if self.dropArea == 'left': + rgn.setWidth(w) + elif self.dropArea == 'right': + rgn.setLeft(rgn.left() + prgn.width() - w) + elif self.dropArea == 'top': + rgn.setHeight(h) + elif self.dropArea == 'bottom': + rgn.setTop(rgn.top() + prgn.height() - h) + elif self.dropArea == 'center': + rgn.adjust(w, h, -w, -h) + self.setGeometry(rgn) + self.show() + + self.update() + + def paintEvent(self, ev): + if self.dropArea is None: + return + p = QtGui.QPainter(self) + rgn = self.rect() + + p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 255, 50))) + p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 150), 3)) + p.drawRect(rgn) + +import Dock \ No newline at end of file diff --git a/dockarea/__init__.py b/dockarea/__init__.py new file mode 100644 index 00000000..88d12393 --- /dev/null +++ b/dockarea/__init__.py @@ -0,0 +1,2 @@ +from DockArea import DockArea +from Dock import Dock \ No newline at end of file diff --git a/dockarea/__main__.py b/dockarea/__main__.py new file mode 100644 index 00000000..86e8c9d1 --- /dev/null +++ b/dockarea/__main__.py @@ -0,0 +1,83 @@ +import sys + +## Make sure pyqtgraph is importable +p = os.path.dirname(os.path.abspath(__file__)) +p = os.path.join(p, '..', '..') +sys.path.insert(0, p) + +from pyqtgraph.Qt import QtCore, QtGui + +from DockArea import * +from Dock import * + +app = QtGui.QApplication([]) +win = QtGui.QMainWindow() +area = DockArea() +win.setCentralWidget(area) +win.resize(800,800) +from Dock import Dock +d1 = Dock("Dock1", size=(200,200)) +d2 = Dock("Dock2", size=(100,100)) +d3 = Dock("Dock3", size=(1,1)) +d4 = Dock("Dock4", size=(50,50)) +d5 = Dock("Dock5", size=(100,100)) +d6 = Dock("Dock6", size=(300,300)) +area.addDock(d1, 'left') +area.addDock(d2, 'right') +area.addDock(d3, 'bottom') +area.addDock(d4, 'right') +area.addDock(d5, 'left', d1) +area.addDock(d6, 'top', d4) + +area.moveDock(d6, 'above', d4) +d3.hideTitleBar() + +print "===build complete====" + +for d in [d1, d2, d3, d4, d5]: + w = QtGui.QWidget() + l = QtGui.QVBoxLayout() + w.setLayout(l) + btns = [] + for i in range(4): + btns.append(QtGui.QPushButton("%s Button %d"%(d.name(), i))) + l.addWidget(btns[-1]) + d.w = (w, l, btns) + d.addWidget(w) + + + +import pyqtgraph as pg +p = pg.PlotWidget() +d6.addWidget(p) + +print "===widgets added===" + + +#s = area.saveState() + + +#print "\n\n-------restore----------\n\n" +#area.restoreState(s) +s = None +def save(): + global s + s = area.saveState() + +def load(): + global s + area.restoreState(s) + + +#d6.container().setCurrentIndex(0) +#d2.label.setTabPos(40) + +#win2 = QtGui.QMainWindow() +#area2 = DockArea() +#win2.setCentralWidget(area2) +#win2.resize(800,800) + + +win.show() +#win2.show() + diff --git a/documentation/Makefile b/documentation/Makefile new file mode 100644 index 00000000..15b77d38 --- /dev/null +++ b/documentation/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyqtgraph.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyqtgraph.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/pyqtgraph" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyqtgraph" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/documentation/build/doctrees/apireference.doctree b/documentation/build/doctrees/apireference.doctree new file mode 100644 index 0000000000000000000000000000000000000000..1620ca32384d7d38e0620151f1b3adcdd4c5aa82 GIT binary patch literal 3040 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
fJJz;3~kI!EEpYMeh~>_755berr=8m zWMXI$t+a!VFLy^G$S_3A=_>XY=a!c(WjLkK@#UUK%63M|@(5Uq0_ga1ZzRB)LH%>f zYRu4uIiGyc@#UA1l(8_Q+EwT+?w5LKrQ1n2;dFetFM>}Xyy#D=4xn+&D0eL^b`NdE zeH25-mtRF<*ytfb^^x@gL&p_;P=QXD{Rz1U=%i8|fIbNynF8M-phkB2c+( z{86-kjxP@YUo6eSUU~|$_x%`1N`5jn`}V(sa_IQ-U}Ogg&A$CpGF>n5G{w;I>c=PedY+AP=Cv<#yA~L0MC8uOMbDp3%bbR?kBxYwA(Oq1GQ{Ef&^$nzb zL}_$Ha|a=r8{{oH-(f!sjg4=Iq2FMo^#a?k~V_6|wRQ4k$po{9wV zVPplFYF{o{_fQrcU!IO+O@u5R^2{jqFYrstCFuc5qT|b-B1tZid3dPXz<=IFKd7_vVYYkhmiPG`qImjxO0=!zKPXLei9 zQ4k$po{x;;(?7clNegw2{Sy|BjxR5$LA7>yF=o&hNKAgoGTWZ}DTj_PFGcn=zV}ix zojI3N3>{xyj>NEW_m%OzD$FScQqQagf?lT}I=;LT3EBmMq*&;6_g(!1sDZfaDUOaW zuSVk7D)A(pSzN3FQ?0UrsCOxfjxVo8qG%bL)8rB#%iS46t_I?MM{#s~dA(koKIE;; zxZ_Ropt0ER@g1t-1rmAeOfV_zV| zo<|x%$CrObMz9)txO&kLH-|VnzI+nlI0EZis4tltB5x;>jxV1^NNVg`XqR6yExm#` zI==iX!WoVF9fyV*HzZGVeED~T%t2$Rx@c%)`>2$TFaJqG@uRt+p>GjN$Cu9{=xAu| zD)*LU?jVjPZfNpv2&Uu9=Mk7DD|n7+U70*RNhlp(zKEb>04>bNk2H$>3`1kZGl{0- z%YP#@6<2$vV8Ni!xGCLAC>>wEjG(NbYLBiXH!ST3MAGr)e-V;*akb}wMTOpmz*`8U z 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 IpKq=EABBxf{Qv*} literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/functions.doctree b/documentation/build/doctrees/functions.doctree new file mode 100644 index 0000000000000000000000000000000000000000..9d40ecc8d9b3fe9c0d672be881267dd431003c40 GIT binary patch literal 54676 zcmd^o2Y4LC`Mv>Hxf>e@^)SVf?<5zxK?H-b00mR*gQFPvB%QRkmUP;=J6i&lPH3Tq z0HOC95+I$BkP4}!hjh{lse}}gkmUbBq5cYAAO0?9AW|B-#OJ70U}o7w3*d(M_U zy@j4~DL+tb8!GhXicNAmm>ViJd0Vuc>uu?KTQ#Sr3}pv%rP1MB(Hrl36Ph!#$Bi2| zJnEJDO?KF68(e=_VW42ViJedYGI;QC{8L*8V>&!~wX z9w?N&DTtm^6P+I_shuW}b^)nJtfmsfxuH}dJJg%#Hszv|=qnHPl=6k4qBnI_X2-CG z1;vDu8yL=+5YMz#b)fQt+5Vh29VX_6W!q?MO?h}A=go*fbBY@j+RO-va;r z--`LwJz40R4azha>&=$3i6WepAL{q!tjg?!xMF?~CKDv}=8MAv+0hXCtyg7c)u1fq z58sd-@a9Hf6!R+!W-wdw=7A($DdrR9p?oRPQyAKiGetNoq<5QDbx;S_A1coEw(Z=a zoSt1QcNa_f5{gY>sH->ESM;{?y*H$13};O-*EN_c7U9F9xBXgghxCk|vN0%6UByD# zpcw49D&5qZ>nWJ5NP9c2O3%rbONG8dp)^z|<+_TctSNarcW#lMB&9CjTl9AEy+pG! z5r32LHyM9Z@YjUDsp*~Q-&T}?Ruq9&7J=4k5!lt4mfo>aQd^6yS^z>E^PTDVn}NTX z_?v~l+4!52o+d7D4RT{QXY2G%nis7k*t`fS+TEFpzj^rECcR@Z-zwg1t$H`axrei@ zvu!y&xyQ*5^ujmZo@>1Y_0M53$5PznHRCaC@x6t33{7^U^XQc2WVcJTN2Irp~PUJ zw~X4gQpj7RW+{2=#A&On&&u5qU}z_Mn!GGq7lAhRG z7|iB}y#CCVth6JT3*D}?vYwNf42Iy@^71Iz&`FZ(W?PRM>5ayy|X2mO<^!?NZcSwu%u`jQ)Felb0j_IHYm?Q z)_donIM*uk&PUQN@VyIzTtBOE*`*;x_GTcv$oDRWgfkifGo`}gT_Q*>-4v3`fTZHC zw*bTCzIR1{;i64oKqCSFlyYVuUqt=-W4C^hUU%qRG1nzPQ=r4>Q3+FM9Z07t&p87;%(_exv*?)vsHKO_xzllL9~ zf3NSouLAu2Hcz|{q^AeCTfw{D`(QTI)~1|?MepOtB&jV=(H_ZK??cF?Cw%Y2f!E)Q zT!Pmpsy*)`dOI!bJ@3iboaAGHUR;SoqK^XW$9(VOfo@cqp?bx#d7qH1{p6-f)~D*7 zA+G4^9w_vzZ}Ogkk*9s{(}9tXIjxz~kiu8lZ|myHqbJ(c)iyl34EbI}vpCeBDD+iZ zy*AdtpMjH_syMvQN-93rAQhck1igvRNBUQqrC&e_p7FgeA_bW)AV19HvZWlyXeNhN zX(*9R9E*DYL^O?sjfv)xlP@Ol&n*{AAZ^s>jro#e*?LjZ68WKGzBku`0UtzlM1&?h zUuyHdB)Quwec>;6O4sOVWDAnxv`Z4cvdJWT6~*U20r6`nm0$P0Z@?wamjLk@mN-W| z^{nrGQ#`c{o?711lN&A-;e>_kX)a9QDdTuNQ7R;g@IuMSB}kGilpFO{o6{ia-)fZf zN)Pqh%}BenZdIe+cL4LdzV|)BQ%eHoXHnBh7dtdtkQ*F!vc-I{$@@N1@|^GeKqOgd zDt^g1FlM)SKZnv^ z_}(uC*Jep-f>FCOlb-jzUx|ut@z=mWq5#*M3JPSH$tB9gT(4wPrF^y}nv1zyVwu#m z%hpMWKeCh?9O!?dJY<724y@Nm6eAR{tCf* z4m5dxhZ|n@y??+B&WnB!)6TnnToQEIYEX{_cV zN<02xv?;|N<1RCWoK4C}{W|(`WY}e0qxNcsUqi znHh-o%}najBw7eE#4@uO-*<@ThCC)A{Pm+Avnyf&d_Dr- z>_!#L3s`wX~N_Po86-FTA9J43M@dy@x4g}IJs0*4A0pl$Qe6x_!YFx~z zaSUrrlJr!F9!k56WlSq+@s`_2$I!tnqOQfNt2)|nQm$CB{*)1fSwfz8olAv3G+;4% zAp)t{8-Z{3p|qw(Y-(x*!M>#Lr}XeJqydKg$rX>`0MLOUG?p>T5Cs$mBJj;YlvfmD zw^9XQ1Y_FCa&U-6-HM@%BZC<8Mj`1q#@WOO#vB4ZX(r5aYCN=}5jqZ|j$pjvR7NSN zj;lix=QSFYuwY_f;!4t_o?Rg!#v$f#T=-_CfMInCWF5SrZ_N?HS33xCB{8DoNC|6) zBIYQ>!O5KneDfy!+Dh6LJ8&{QsKXJbG5~Qf#4txwU{y#URM*Jp!W=_de2yOrx`Tm) zIgYB152+I67*R;4_w^f5=!3gjOf7X}Bw^MNLIiPTFk#YUK!@0zfGgje$ophw2tXSQ zAchi0%1X@`h7jaC0Rv!km1_qQMF*n~LJfu!GQwyx(j;mZ%oYDRt*%Xp3@FS=1h$Ek zoGdaO{|(@$z%mNlsR(?tmN=6xQ%bcClPncQeG&{dOt*-4jKxq7IFW{41ir~pZ_=gplq;yKY3L(Mzp}h0!>2~s z?NEO-X+hW9&ouCj@?Lo2h9x<;(70Tjho z8ep-M$}(K-0ITw#w_%{no7z!UlwKKXHOlx=BC2>P6r^jQ6NU}nx0)s>mW%Gk{L zldi1<3u$dpd!d%xj>^n=qMzf3nwiGxz|6sb%z0^*qhqhhQqs>C84R<`1-S6dg@VUP zx`c|_`OUm%u9=JE+8u{qS1i>Gxh_UT&5+9#*NK5kB<$c?Ni-1{dR+=Sa_%w&zIh9N zgH~Mt@yT*0>J{>&I_SEb{8uQy)XC$p>|(PnJnoXm0&9KdO7dT&{2Y4OMM$prnX5?% zl}>XFVw_!N-Os#LE~j$oZ$?mBucZK1|IFKP<(unxpX5#oZeB080?iFn5yD`X11mP)E&?56GI1kV;nkZE_~vFRPP*2v0A{scKkSxe z1MWd-GPi&SX}gv3x2b%l9Sd#8m1JV3$hk7Rz??YFCiu8YijUJIWA>(37E1;(ADfWg zfka^0Js}gDnB~DPlRj-Hv)B#OmRRHDin&CfRwjX*e7`d=n&`<6XS=bqh{f(i5$o+6 zoh+s{H?H59HJl>?q_#w5*%@MV!dX|pcXkcPy7A{gy6Un9u` zuH!VKtoY{pMrm%?6nd~>LuM0uWSUkchcUNRO;uasNLeVw%xq5 zXR{S(o$LlotYSX3h~=_G4*AhjTDHvGj*?VU@-ZV66gqQ<5K-sc$>J7$4rT?<-d$wq z$UKgx{(rYTn!AwUJODN; z9yE>*ie$sav3BQ|eU2Nnh!2UcoG-B`KUlZHV_@M;bXklZ1}ibYc`0bnr!$4B5JciTp`);NeOithg&C+37XeLZ#CP)imKCP0Uq2y;(GE=43X2$2p^?Az`_grWTW4-{ky6kvH zBpYT&{c}Mb4Kn15qI5Gexih$ z5OIfO$TuVmo(m#JmOP6{WXU%X_~u*q4YEW~M6=`pWXbWl0 zz>0G92LgZ50zX~KNWZT8j>x=4Dlw`7N>2L%v!JCZo(ziC_QJO*Mj6aB>&T{g=|20U z5{vP#rA0=WR|1F*+|tu)0GmGw1%>EO0yRBlWN=_vaX8zPGk-<|mO>3i0tUVPzX%`e z-^!w?tcK1BdblqMy(9DAO{w`SP@rW04S{d|j^Ci}mHa@-MmJAZHD4zEA0fIrh^zar za#Fl9`q;!i8{2s@|Ab|98M%h~FQTaRY<2gl8HavTv(v>PbfxbVvv(Cc(P77G_s$CEtv#ncJl$6~4k;lGbJ{!@3kvA))=>>4(6g+H>6IuEgcz6}E3Y)ciI8)97xsH~%INBSF- z?h<;+4I$t<>h>hZBiI3SAduD~aKes=g87{gV9A*BYF-Sgd09!_h4e&-9=+pN0u>nqpICiRn0=G7_koJLh2smiC4L&@P`*t7a#(OX-43i7D{Vk#3mDCH!{eQ z+Cp+ALtLSP$0fBCY4P@3h3?*h#}(?@sBV#}%k;s;hsk?1L;2iLPjx0DArleU6^K1| zWxUOSSqH726Md#Ih&^5#ve=?t8kH%Gpp{MVHLJFS?#aS_qJY3HM$61%n8Z^f$-%Oe zKD=%)53z)JB1x#Ou`Ly`j{hXyyngTsNKM-29xLV38!90VFH%CweUO|aIUAxEC&_(4@H1AZ($9VnOE?pPXA;GV$HJT zbk5>(HF#hH`!pw6jplGfmAhT5cO~wyVog;R_Yu5_)w{SxudC|n;g`A#y`xR$NT>q% zqiCcvY~)S6sdqWn*AXvHIa=uTT1{bNlUW4{bRL7iH^&NF;E>~Z6V)l^Md$HCuh%Ir zeE{id(4ch<0^g*CG0=JfZ=zZSfM`7tVepL0v;mALWSoY;H(eA=x>mcG)*1|%J%On*>lo+Sv}GyQtzue4dw5fC zJmm&y>jgbXTaMy=D$Z5e0w%l*eajM<*X>Uwx>JdsRALbQ)Kp@9DzVY@gB8gZv(aQb z)KhQSCOa=8y7K!9He7JRBkK`h#gsaet~D%x1CO}siX42i6-tpAB+a#DX^7$l6<5y; z^QInO$_>hrC-lIxhVsR*d?`j=Oi9j`5eA;!fNS4uq(qXhbsnEebfgl8rV^(tUu#B0 zB;|^cs2@iuRgY6%--40TMWiFD@eIVnYiAOok z=Izvxbgdx)ROhrHsW+1UCgoRG-OQVM04X;}>MfuLNxhZgx2ZVy>Xzi)Y3b_J6_ne- zgEWi1XqxY!f_jrS&3B4O$0n8E1r{X!ZUnx$hsu(!wI~ql5+5xw_fqmcl~gy}&zpKo z)e`f7&;vIl8{#4&UAzLz!2ye<2?NO&aO7?ny zx^>w+CL-F6R#opt1YGnU1enLCXwtR%#eAKMf`q+~{O?zOb<78NQ*Srr1_}Eh=t06B zr}&3doG%geo7`wo-iODmMcd-mC2fn%6Cfj%Vk(--4^y_@noZ?LM5H55?~|Y+$sa|4 zd3owdy4IKgt4ngURD7J0pHN9n{3m%+52soxJ|*9G8#W8}q@l#0(F44n5_ zT>Iv8l+d0Do+67GZ95!#4X(OB?P|tG6j>a)h?#% z+!G|~E9C#G@~cz6#+!PpDK|*e*Fg^w^$m(YtKyj?hE$<KhQj?sL6)J29M!Qt_J@S0t@+<&cyXYQ( zii2vNgN)S`tQ-D7)HGZlX?z4mg9VZwidOdX$F!AZx|FFOiHt0e{1_L$`H7&w0to?P z%=1&;(|6`)a;*y_VyR|<iA7X2T=c(H}yGQeGq>q^q|cCp7Jk*<^K>PFQz0-FCq-e z>>qKBxl>9cGgDCr@K$*VOPS?dvBmsZBvP)}h!)JhP^cbed*>9f@sfyiL=^rN(Qw?~ z5MX+f3X-n1B2Z!;%$Ldk59QYf^Pjw_x1Mr?ME(o(Ad%y+#NnGQ@QZwU6%VGEkEVZ1 z>Z?a!)4vsop$Bt3B9QV42z)b<)S3rVFxm%m5_u+Dp3QkMr$7ezCAxwKvq{wadk^MR zQ8)GnbB5&qG?9@9b2=`3Gec0|!6ZNw@tM4*@60T@)(2B8)jXK95mEbKigU!k90?;2 z=GKUXm**nD>n!*U9!!C;c@O3`)Uj&M2t6o2`%-?tu>Afp@?uJI_yB}~o0s9*HwRK8 z$!!sm>C5MvgM>Ndiu$M*+sR*#u)TAts6SXlI>Nf%h!{BO5CpziPT8bu4G4fbCk076 zl>CP&zq)1xZ|V)FT${8_G@yr5*~+l8ju>S}Nj;8)93k`o%#oBoDlFd_BQK^z{!Iu2 zKOc>2->jlUlJD`)KmPcRjzjUN9wY22R}4fweJmyF6Wrc8O$;0-BKj&$MceU+gRfU3 zz`H1vPP*2FfQa#Rn*1jyzxw(_-qf2;xi)#5@U_e#VnQ*j?Bp0_fv-;ydf@9*DZe%> ze_D*Zm=afXAq;%I4%fcPQbHHFtap~9Tz88=$`un)Z}(89KFRH!>_qm8h&Cl5eL2L! z-+c&t(@*)NYmEqy7=JtD&nv(B+vQEY@sw+mxCwu+r?P>tvcVW-fxm}@9{9UJ`Qfm< z7b7pG#NP&C;O`=?eN&=@)*-t3W6Hvuaz%aA*Bi)RpWOCN_6s(Oh}I!i*9c7HGX} zn8xzg@MxPk7pzFKn2jd+JnE^pY?FMxi0Jh3E7-UIoN&d32=E#JbtYYFSOC|#qT1%* z3X4dx$YCxbpF~W=;f7cfG8YGux<{Y;2QERJZ!X1e&_S~M2iBoIeqHwuV2g*j40eOv z18*UQy5*h9*;nRrlJyO2yJawD?*LBKE14_6-eB**m5BDuRn(;`tak5!MZ&!USCc&U zy#v>PAA1KR2=5(uE7i!}fopN)o44^^U&PiFL|?=feBmtw*HH#r2(A~xMUZuN)o^nG7xPjn4lH(EF4>}OY#xZrl z1Big}2NC$@Axf)pF{j35`@lO$e>g-BS?2bEcaj!w`CX*T{((oR>rvHJvwuLWg!d0T zMxJ<`?-u^>{(<)(0;zc~0^huk(wZ8vsi_eJ?3b9KRw&M9HuzZAEPlmWcWuLnkK1y0Vf{zJ3sK!4|b)Qgm z9NCX?7XzxHHuFiyp-vY}sMEDBsBA^}6qQDBRJS5LC1M?$ZS-k~!Cjw5;G56TNYb@P zBsuh0kiO57>vPJ*^K1^+mqCilmc=6fJlUhjEAM-L0SrJs1({e;XP!YMaD5ShZ@xqo z$yyc|6g&#hZWH`6*}r1hF9{yBW-RX)OiPnic)Y z8|)PPs-Sgcd!QS$DR4@#De!Axg{S^crOSLBppaAFK;WBaB{AF_ka8t6YKqS{$?`2_ z$rO+~coRzBYO*gC$q|Zl?Ls@^PT;)N0c@Y_)p57H=oH$(!&jR?>mCi>&K>GAHAhQ4+)xm|l||)6Y@l z2UdhNnR-BW$^4Mf^|(}Cy2n2P9R)&K6bhLiBLX;nf&lZ)l-Al$%pfz+&(U4eax&58 z`q=zTwf~&jf1%oW#?j_#s=7eVulObPJ#Y2x0c3XSf77{Xegz4uDVSgUwJ2#gzgYib z#9>k;G?@4QjVK-a^x{m(hTn>eOfUWp7ryzufWY)3@gWyq;5~EE{6VgDdQmLZOfSBO zh}!8z@r)SwqlCeW5k!s`BmRU)z-3El$t&5m?}x5O2;?ead@K&$vG8)yYoxc57r%;^12z=gAIqu%dCP>>Uo5MXyM zertQbQ>x=ZNTYK&Wpfc@WjB*!DzM)-pbwCaRy5e4(xA;1An zlvnd&P|ZsZcze=!2+>2f*#q8@w0PS)fzA!IJ5$vzs;Z^~EJnf|-~@T%RqiVM;STV8 zL?AJ{A@I%al-9(EO(rI^fp!ma?HS?<6+Cu57myZjzZrC%Nz+1A3qz_zInJc%MpgQ{ z4xcp>lVZ&tOOqms2(KfRfg7yFvaR9o^76%WN zSwgj&z_C*fmzP(*}kvi1-DUPwz!-Fi{20q%K(4s!795QnN89Fk@ zB5z)6>}DC1!;uFfz=>J3lzd$*A!8&f>~dy1?Hz3GjlHw+Mo3sqL1*I-QSxtfHkOOh zv3EAOGwo23kv3KRxf*)Nu3Bp~u)2K$ea$UIc%{tyE zYZKw562*3GcuOTVXpogahTsA<+_}qA9G$yvA)KMu?B+1(p3+@HPq`rk?A;lX;}H}=2Ld@eyyArt;$Xjwz&9JHK<$e~wJ+Vfjiir+ z=pp0m-i?wLZ~S!9WqR=p>N-<(WiYEU&OyG5g>c`_Cp})@S;DVp4LL}_*|_!1ITX?a zh#5_Q7(JKt^OVj`CCgs}!{?JH-tYyW!?29#slr@{2q1Vf0^eLjX+>`2tsqZ*OBY`5Lf7-XUF;m(&FvE9rR$z^G0gBNwsBmL$-9} zhHx}-3CFM3ynS}W>J#Jzv;pi?Ycn^49|;#I=$C3WSeeSbg~}r4t6k(U zymLDO-`qjvN!KC}_;gUHjk=SJcPZn4u#F0KESkGvI%uQrA&l5I>RytyjSA#r+o&d| zsbubhM1wZ!enk7`0jjfQ2XzB7$AV!S^&rV(Z=)UpKiVh>!fn($s7BhThjE1iWO$#< z45Rc^62Y(7=+4IW1JF>h`ZFu+94@$?*t2209Q(n^*C|#}NnnpFrT7Pf~%}7mI3N+PqJZ z{#1w_GR`*dY0~12f0}e@em+B8pH*F~dRe+q1BsPzyZ1Tr#OwUL@N4r!73K?wKx&>r z;F~W}T2mu7H8q0ZOQe5U>D-egTXx$IXaM0WWQvFIRnP%Jj?PgX=4*%nhOZ;=%{M5n z7{qE7!?UD+Q|Zi;;Gt@O;#*{kNAYdYZO#T-%y$q69N$IYo9|J9;tUc zvhX9_0Lu@^8jt0NpaYA1(lD&a{0Q;D^J4_qY)&PLM-Wu;{FL;cDP6UMDwPI!eooeS zJih=vz!TDBeu;SCc^(1wp;L+C5d>8{zb5@RO6MwKwOVU{=C@>wNAo+-fkwWrQPpF9 zk62)N0fBG+KoyEbtXHwTNcta@&N35Js|}F+iEQym{tUV;Gl3TK7sLU_O9*`PS1M2( zV!4XrZ>0ZS=^UrqO1%M$m&p|m;~$^{hAbsnCFY-q0*Zej@Xa`ME`!m#*sY@20uIJP2RJdW|81Ba~ZR@9gYhy;>}2z)b%8q^q}T?n0gfhe z#p9R?y7h-uVx}PqD5fL8Zg9#g3b9KRp$_Fta?J{Hg<4d0C})!vk6@0_gC6D9R5w@E z@k2y<8n;aU*&SK-I0OC49_B>w*}k?!x|qYK-gpE;v6LGgwudej%KZ)`aj*Dp%$jl(97qy5zyfeHPMb3#*5Z;q+3+3Lc~L7E z9Ej@_in$@#=g>3S!x_|Uk161kS?M83$sWkb;U}Y7C&)Dh5Q*k)gTr%6IB2)n(w1mC zbQHPKmn{$Ag(`j_te6|Xp{!Vu(9=_bdgB0UZ8L4a5?h295omp}seT#NEEegG%>RXi zECCdh*rf=3vloFUU7I>_d$_`v@<;Tfr3`!7e8+MzDtpVMe17>|vlF&sQM8nh1VtN3bic z;0XJTRePpG2+=X@5s20??2)+g%~8B>Fou=HgvYR*WQ;r~l`-s_gg-KdJsPn<`Pa-Ye{K=qmVg3}VI#pG1;g-M_`7h6 zMFI*belbJ*p|$udxw=DKp%TX)UJq&UW_v*oYVsVl^{KYZo_H*e4?fbUXExwKiH_8v zspTy}&1>I1=!XcBENIYC(`T;IQFEv-f}q+_%ZpgY=Gb!~0UxbLfb(IfALqk}nv^Sv z2)jw@#X&L-DPy=^?D-GWi>ph<6a<^yqBKkxv9+Q{vQ8YbSVKOxPQ+L0p&b$p>ck?V z@zx2|)zpa=4C}-)$z!h*H-H~?q6FbOaU<18oj8Ik-;DBJUxcnCf;FO`3fG9IlM^-K z8A6!Vs75>!6y&y#z&B^%x3)$++Y0KOUSQ3fBhk_N@LWV|eRv+Od~-hU8`OuAkZ^r? z0U0C3xKbZpDEyK7@Xd$?_KOhs=3=VQyb$Zv`tTCcFIBor=qWdZfc4>JB*!Co3+O-~ zb9xmoT#h){zXE}8uA~CBFBYq{;Z>wx9ioSfvo^ejw0Prh1)a6wwN&*sRaL1C#Xz_= zypHsEb=M1jwKlu~w>YzgLW*C^gdcO+6n`VRZVGXQN*im#n@Ni|dkg46ZFnoS-KN?y z&G2ey0R`{c+IMU*FzdxZf7%nZ_2KOhLRBPqP#@~^R;dr~pvDM>YJGU8h;?k9#k(Mb ze7+k2j<=x&9B(6va85CTkgL#j=VwLkCGUO8E0v;C^I4JmNeIC-4f%ACDgDH}4c-90nsnYxNr-C55A|^LR@g>q!{dVfF`~ zFBy?VXy$35 zL|czfBU)RJ&)~{8pXGgn)u-q{z&Wb1;hgWGYGJTK^2;JVqJ^1 ztO$LH^e-#jCG?aVLcrGJD0ffJjB_6`x zK(}Q5R9`;{G;G2I?SnZ3&s{MbG{x7B1P3PAa$8m6SgHLl;p7AX}w_dQdn%NRj zK(Q49-;Afcq7b`85t`1QK(2`)u27B0?#(39;+-&A=t2Kx3e`2Ky39Sun>F$!C2ZxD zc8W(4`nA_66e~K4KM2@#jWLCaY&07g3c% zj-Qko#Oz(ZwKqSQ8!93WN0d-SF;OgIaywg09DV$e`k6^g>*28E4fsr_ObUXsgkPat zx`LnPY%^1V7zIGQfzFjSUzN_)G$tqFfokV!x`=iB4<&a70HcV^M1YT6GkHnZ`arUf zr6kDH+2op|T$vXnPqDZl8A@NG;B!_Mr%iVwm(1va@)jFrR||=uLMbtn%kh(B-N=1| z4{`M~pYRngSu@bu1NkOZDee;Z^cRXmA(6`>1G(sc4}_JZGz?>cms?>$Maj@P+sf{7 z5z#UNEZuqH;ODopIB`}!43-`0FAvBKjL0Xp1_m(QJDeW|I99}V-dE1E!Lc^r#9^aE zg^xXA)o5#^KDKbtv{t`)s(e2kZXJ8 z${dBVCLdYLCioe?JkD>#`X4@^*N2xNrI>KyJ}3m(`jg8AZvh;C%#m&Vxzci}QjW(_ zMFo7xX$Po{&BPsPGn$DLf}zMxWYF(%ZZ0UZGlb!UT@d&tK~4HS&dt>X{Nf|KeScT# zn{V}nzMRx3`MZJ5ies$sB$vBHvf-Y6IT(zUlp zYhOL2b05S4=e`JhvmbRNU290d@Y$(9ro4Ng(6heFD;G*Q<$ix~!O;g$!!p&tL26A? zm!29DG6#Z9O2Gjm6BtQR&A@O?HJX@ zo&2vPiBcyB^}P%6=&@9BoK@k7N7=|NR7dh!`eIzRXU9zHQt7DS9h8!Os8ZMe?b@jv z_{L9I4i>QfTIq1fCq>b38^D)=s1siU($}lPd2&2qG{}?HBuJjDA>Opb%O!cmE17aa zL-~{|iIhw^QNr+eNX8||m6H(o=4AW^xguu5xsrurUU!d}QxwjrgtJ!RWd0+jQ7*(u z)pBtfF?3lBr%Acc9+)i{s5rD!jxJE7dxj@Yvb(ZFqzB)f4RN7i<{dlwz1ppAP#V_J zc!ScAB|%C>=mHAJv>6py5Glc$N6n4w{km!B&Uz5Wcx$ZIhb&g;XO*RMg!%|+sNT*=sa_$n@5xe9dKE#WlO)wl1a&qM3X%nd;THck)b=-MC2cZes z@el&vyn{BAt_3CuuFDQNdvR*0!ZZ()Cved_speg(hOgi2Q>eW}dANeMux92;x<V=Ar5Ir$w!;@GuK%_R%m>xn<23gnHOD96-&T!6C~34+ z=@T^YVQV1LtM~{B@m_ioba*MeuJBPr!BHPW;G2(AUMo~FRdJNgss}~s6J-9RGG|VK z2kmig%dp0uHBvoF6PVb0MWRopsW}6Rf`q4DXUu!^SfR|tO;h&%iPT_C2Jsy zhIKD4X_`;L4!kfyf=JPNiXiG!+}=4in0$Cz#5!Jgke`MbxbZUxu*ykLN!KD5ckx+u z_s10q`sc{?dF2YtSm0D9_dvu-?e_&zBMGZaP&@;=YkNdr#5K;&qlBHJ__Ex%o9qb* z3VC$CLRQ-{eib*q`I*b@gdUxq@2|Jc}?WTi?VrzWz;#q)QDcS5QUs{@Y}#SKHpXjp+Q2 zh;;nBDflk5AX~qO0H?jtUImmOt*e5A{>5|T|AF#nI^e}MDAZEeIkh{CUTva)(k^rV zIfFw3d-FXqg|G9z*wxK7{Y3LfYBXhin_7}u(!zG}hg4gi?5a0^1bX1j9}~z=ERdD2 z84&YRsKi4l35{mV&uFh6VSDGck}*FQk&evD*AB}spb?q$O9Z}o9=|BMt_3I#h{>d1 zk^k4qpV`mi(Z;WK6d1fWf(e39mPkpLe=X)W&>`jgx71ai?rP5b4)h>revfM$7)Obu zOARSkAVuBz2Qt;GZSUMp-1(x2bo^WJ{|FWE-=7d*X_>|e+gwki(x_{82%76jBHfW$XrIQh88wq3g;Y;L;F~7eNN%PM zK$djVKdwDwb}IEvv-)ISGqlsESsF&H%SoASZ$@!NX$^K|wKt=3z#k&)h_b!8xfQSc zPSPp-saFQa%L0ZW^u_JV*pOURi%T z_~5035ny$cT9d9dDo{1kWfSosWLd5(d|DLL>wQvMF^>%hgZSj+2~aNWgmB;zcwHX#&KICP0i1l0Kw#Zm|xP-3I0h1Cz!l#9y4(*Vl`^2KA>2s*IH++9VD89^kF zj3U6th^Rr4i0vwpGe|#E=`5VpXSV?qpN#QP&H^1!q>xwjn6nWJEaxEb&AC*eSj2i2 z%Xy@quXO6D)*TJ7TtLQnEEj@qu~hY#HzO8UE<%9U!Kgy9i1jL#OGv*|>7j#fwC-wv z<}$Lzqj?MHK*NK0c<_yCGM6JBc&t2|4M1H({&-Mt z1s$Losx#Mu2xM*0X z58L-(Wh!^5JlKu>rX#XT8Le>%yHc^80;{BW!zX8C_>YA)-I~E^WAtk9jG}F7Gq*q^ z{3K{G%GQ>%vUB-Xnu{Q;?p(f2#5!I>IJZL~Tzm%t-`q*_N!MZ&7tqUrPwpbu-O9zM zJ(wex52!Rw(mmvkCaJR7_Fga`Nzx_++RS~31FHKG;MhBA(C+~T955$f6rCN}K1l9| zEcX(r0Bmom8J`eeycpxupmI840X}cC8#Wr3$_880=VO*P@yrt^%#WlZ7^%KPv~F^w zTKUY$!(c^vUX^|GP6#96??T|4N5rTdc}n8KC0;tAkCN-L5Z6*%zy2qu7xSwzhmkYy z7HhT-`W~W)?SsCTWc}uX6_4$L&UR*(%=;kFpbPqbL}TrX+O#UrB)b+1yPzK=dF)-# z$H9*-s086I=!d9Ax}ZQR0a z6cmJyA;1zAertP_pRj`ZQHCHxJ}I%$4&|p1tsTmzaOIn)dEcNzDd`M%C_hcc$g^AN zP<}@EBOS`mA{Nj;hrlmho`Hv5y`AT8eZv!qMc?wi#0E!9=&Pl|Z6eQPt0fn#OU`)e@^L{bx#V+@LoA^>gyagZc&N0M)pE@JkSZ?0Ez@vX7b+nP4HZ zQ2*f9Lnz(isR|9!Do4fu6x1pbH)noah;7g;wbqyhuY4Y}KB@A4RO=H39M`sDfMn zi~yhJq2Z)!(MhuD*}xAkk?XI@73mW+PR!rP9ZgK7Pw;mzATc$4f|n5oRR2JLukKJo zq)#AV6rJr8{EOVwSm9wB9(7YtKZz{3inI{&sH0@^JYm;S6oR`e4 z)3Twq()rUqg)Db==diGkPkmu030{*!mg9XjOI?_NN~|Mm2SaAI z8HR%{%M^l$?XonH+{N$Ns&s6RWv(-~WTt|@L62n`qVXLhD$~kI6YN?j?6J%sdF(xw znczo{MS^gTWfs*)k7YKl@I@rv>z4;@5{fNmYs8`u3b1geWG z)|(MvF@fT0UF@lK>5?oYJsF~hOtVXpA}!u@E9g9crH!ff zVHAm`to=LwjV&(QtUMR3i@Ge@kKhj$UZsW+r@2>W*zQ3w@6P9gDsjYMY~0G zdVoU__8<;#1qo#qj%3Zt%idksV2PK1izY|0J|RxV-pUeY(u$@Zx1qNhu;~V!tfAf} zjJk1tXp?b3$8sxv@kJWpP0y)#tvlPZ9;sYkH0wbD&aI_uZhE_VbFxD%H`J3yLI*%w zm7Zq$db+x^#avg}446U0rFU>jrQv0Z7L^Kxfnr-eSL$mknEpjhX>eeX84`{u@LNw# z%$R~&m@%9imZaw3;vOg;rt%rZlF1Ku>97DP^%y(7R1lK^MuSLq-Ih>09oy>q%g6;t z^yW+vg!Gp9+IvYZreK!_x}4d;VN<4>X_`A-19%;Oz-&NVCvq8SEW(4FhBO<=*yLmf zknLm}5w;2G=@uAvrxt;BRQQ|~tI|7i#<~k>DD)y5y7U&W5B6f_$D9tf^ejPMgrH-; zb8F6!=n1{K?sC65Q#7S#E`@5&8k^8;Pk-d+qB#N(HMcyW3sBz3S- z4^+!f7Z)+irC_=wy;W{#gSm{#G~vU=T)7vi92_ujVf3ur2o7Gzb@8oxY(FcS%NajM zyjLuDm+~bfR9$!lbxti7%BCkLb-{XbC7EZ)gAgjgS21>GU5=Tn5uKh;F7>tUZ?2K+ zCaX#wpSP-}{7^WP&9%Zdg(9s*xa)0lKeMYhU(DjGX(;P-;&qgrf+|KQ&CT_~CeK>7 zyNh48Ha8%qb8>n{Z6P&p7sd(cH7fz>M!BBUrI^f3a(B|J9AKa{l`)#v>U5E6TofE* zfYT;Wo^BTXQ~LGmI>_={aK9=&gT=F&8MiWanv}RoMRS`-?qVyNjT<+%4Ugj3jUiSv zy}1pGz+7BpZWpc{Yu#D*Ox^(&m(R)!sf@d-{<~9zx6JpNyX0a9k`pcmcO&wW^bT4N z;FJwXX%T)dIbA)rR=5W&c&cm$@o6cVdl9)dy$v6o{7^AxWRnISXvxZ6b04U9K&P7Q z#!B4%8aJI@snqWei1KoGdR~2trVY%4A~Y_&Rlc`q9>O(V8^Le0Gtr%dn$nr1hg?l| zuiXlF^RYE$Tm7`_ba(455HmxMl-=FwbY|*T!Di{9ihJljk}1xhvzwkn*Q8%i+12H_ z!E?3aKTj9AXLD+@oSuV+2(@ad%*1Mg{xDI!6TcW*9xR(X-$jJzU+F;$6Sc_Tn7jSf k%HEXpl%Rnsnn%Ej7e?@#o``2oQu!EeaEKj#%iV4N57KomEC2ui literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/arrowitem.doctree b/documentation/build/doctrees/graphicsItems/arrowitem.doctree new file mode 100644 index 0000000000000000000000000000000000000000..8a90ddb68c5aa439d7ce3e5422a617ade3cab882 GIT binary patch literal 5249 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 AO8@`> literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/axisitem.doctree b/documentation/build/doctrees/graphicsItems/axisitem.doctree new file mode 100644 index 0000000000000000000000000000000000000000..b0deac26328c4cda3a687dc224f471c3dc599c0a GIT binary patch literal 11126 zcmd^Fd6*nkwNFB3>trU$gaAU23Igds(j9gT3ULEPaeCdc4 zD^|4TbudL%OBK$UbNmYfyHTiAoTd|0D!PAsWodLt8a;rj!yQBfSCYlQrvmA=PX>YOXN*!3_q|8*6?`InB9(Rj((uv;A77 z<~0Mc-@#z8Dbk1J69Q!ZWI<=Zt@Hm4s9 zx}zd(r0rIj{fW$5x-})cMPbvap}%wb=+Y!iT~uDfF1q22U7Yo3rr5BWj$d)DYLTgm zR!yH$be(FEJ*C+0`C{rU7RykK@rvRUPEB0GFQ%PXAV1I#x#lW;Or(#kngf9~#a^Y4 zGY1kK^j0%x1pztLFhD2eBfTwe4%WjK)A{=NvN=$5E#JrY31xGzYSm_lyLfxqJjkj0 zdPi9o%)t~-7cH|dZn?9pPgFxzXs&jhrmasJUj?>_o9mMm{%Xo`>*!Q{N?D(}$TQ$k zE-ZB#EPQ&T&wz!+JyW;W4^ z2=pT@Xf=^26;{<|8xKKUjc`)a7c%Ih^5#I(i=`MpVD{I&hUGN%MWs~|2^LA@Ri`=J z=!;8zAP{UlfldQMDZfNbWKi{^^NS3^q!r(Rj|Cy2^Bx1s?1}WH+<6ygG|PKXy%~rD zZ#PBm>I{iL73jRq$} z*Oh@&Ug#*7n|-mJDhN@1lMWBJCmzUJ$y&xrR)#MVMp8ABIQezojT0uD{($lGr)|U3@7zdFRgFmNdAeS5I2FsPp+NzL| zZEd!VZn9urFTq+6Y#}qPAsj{8Pla=P2p3BhK(a8>`_V#Oo)Kn9gpr<_Wl83GNiq*f zIOC<`OEg;rcvSe5FlI&^A zI8B>RPCp%hS4a990G8H3iOh(CJ)V*{PCOdg&Ga)E`kB3;uZ2;%_~`?%%d;Z=>{Pj{ z#V(3PxGvJqVG-Dl={WA-E5UniFTCd^osc%x&j;8GBK^V??79xv^^txNgAD_0L8$0P zd~q+tmjH1Q2mMmOzAVx&Phqd`z}^t)S1>G_1*Sycl91F5`pRCQuS(3CX|G=m$k#;r zwJGEc9mpFa{W^wR56JGo*Nyx7Ubt@n?ov$j8v*{NNWVD+zfr)EMmUokF6l%6Ly9`_Ef@0lqD-I3K<}((g!FAFj5P=Ap>nINez|vq=YQp*6n?LElFbZB7~9WG6)~ zF*CkN9j7D@zq3akepg=YE*%Fs(+PEcn~`rP-ZobQqh5LSZK_}O2m0o&M9dS%TO$4L zjP8ojB_Y2juf`9kW%94OSmJfeUEiuMQMYFE$b0jOv*~S-ejhu=KFHZNMO)xZ5cqr2 z>Me2fP?9^}FS+vrs^3zJbLI!(MIVavhZp9|k1%n$v|%Y3RRsZiVT$}{UU59XJ<=az z)i{)-8o}3ZON5bl{_$R}_lYEwq>c3_A>F4U{pqwbZjT2T$Lc#G{h72lMUc4s*ZCaTQJPEVE>xK9G?sW|3c!`ub{ReQpKhl3>#qPym z+Rgg~$V>3!V@JY&)c46g~YdGM>-9EdGp88@w=4NuP-nCfH4zy;NOv!=*vI#<|+ z6Zl5m@msDn4{knhDa1nX8sJ=S4g_|?FXVXM?;Feu_*k#mSa%6}!^_V?gj;i{Z4UisPFer5ge}Kcri;RVMd$rLGSt7pCI3Cr{{jC{Y_$8uC9%Netk22& zKO_BLsY`P5o`VzppC|8hU+TZp-2Fd2a`#_2cOMHelU5Iqx(C%@Kr2>avn!Kgp**rT ztweD|tMJU2K(Q2@!|5QY_$$*mtp>HaWSUs%&N2+3bEax4P#`oI&Bt`P-S|ZvjROt>XwH;2K5RVfQ}Vb$HlBlr$M1h?Z5~W z0;bziRvh7tNspwAmECUO=K{<1ZI0{+%vRO43$&FvZk%S(U`2c0Wqi@d!Z!_H&+csnWO zLXSWhWIrFTh%OKY@yKW7P7vEI^^eTdXTT+hT_|bQqQq# zFTNr&<$ttvb0&7lyi}%&Jc*5?7pAT}e=Qify@+lsDc8l6CvGzup&K|Y7kljz>EfAf zX5MMJ9>^syf=iBPyeDllYRYQj)Jn={t$71SH<+zr_b1a58}r;Oo*x^z3lKi-Ijhlf z?On7F!o%M;sV(U!l&E=`=(NZNov2yjc4PmXIzJY&!!sU-S458&O-H9=Lrim*igR=? zT&k{Do2lr~3+x;{0aWP!vT(RO<{*K*V@GpMrUg|%!>;^9pD_ImXPx)r(IrS#fgO48U$}?AuCp zR%GKeTd2#mY|&|v3C8BI=!mubL}&Yc#jnV727 zh&xw8mjToKwe_dH$?#{K~UdjN)YTX;pJMakrdT&SmoP^bp4bzC ziU*)Cjd5Ju=i2*icl@CEcriT>#~{X$EY3aUGp%Ki$^csMfQe&vV1E(xK>jvuV$@a8Qp<5`@xxa^!g z6BkbGrmH}aopU{nTd*yjj*p10#xq`-!WP%a&mvn)v(#*kXYhNhbSBJkF2`9z;;exU zlz6+x3$gcOR9&k%!-uc-nFu{?t04-B-3s(fuz|_0#Veu%cqU8ZX%;DKGTR(UMoJ~V zmq5<~#e|uw+cl5myqBIWC8NA8H0gN(jzR5;A6OL7b*LF%i9>6gtl@3}JqQ00Jr~bB z@3!;Nx5Cn04XL8S%r|5WpNCp?l*P9_%ryZ0y@V?_ED%}vy;N#n#TuaMGL;>iwgu1#&mT|low@q{_r)+N0h02Hla?xvre9);UM!XUK zFye4lSf5@eO<&JVhf_IX3EzOSh~CIG>(Sp1VaQa_9aDDT)0?E(o4Ilj`7Xf3%BQ#B zH*;MBX_D{W)N}h}&oD=CMZHIF!!x3{Gp#wA+OxvYf}pjlSPVQ5cc&aXm@IfSRa8O4Ez-M~VhDDt z*>X+g<``k5Ww%&W8v(oq=C=s*5k5I;;oeY#?_K%yZYdoOJcdkVd=GQ%Lp{yYt^7TV zkunwHIujb!ZF(=4t+JbR8-EPdEnJ}qFaf0Z;hzs=(jxK&3f|x*f;vJf%*b;olAR94^4Y@2tzT`W)W9pKrK%M0ZN*rkd+nGZh<`W84aEl@Tti!A(Ao%4^Ldp=zTY4mxgB z!Auc`{Q`&%m$-Z3zJ_A6FAOGkoIzjb?_9!)N6|N8PEIr1 z$@EPo8Vcx~pk#c| z99ih3^j#+GGxuHqk-o>@18I8rKL1VrjhoBqbut-B4}1}he!yTuQ#OLE#r=N|{wHu_ zh<&xwGe4Blwd{WF;PWGHc~la7W@l${`iw*1rUak5y}t-zzeqo3s?7^++ciLc0+MMN zI3paOD;;D0lpC*d>hv@I7=dnCr??k|2h1blpkTYU1T7dnP?h9*9{n677-C6p$|~zo{Xy7Z sf+M?T>1-00{$w^8nnP)3@#%h$mg$dpn*Hpbm_YmqKN0;I&#+qfUzk=assI20 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/buttonitem.doctree b/documentation/build/doctrees/graphicsItems/buttonitem.doctree new file mode 100644 index 0000000000000000000000000000000000000000..ce962738e59669da725827d5eef5e4acde6a8794 GIT binary patch literal 5775 zcmbVQ2Y4LC6_#X6I-S)l7r-SJ#c%?wqnKvEl!O)$r7<9qTsC{RbDG6_yZ2^yEh)(o zNCH7gNGFZ-9uh)&NF@oWq>|oy@4dJG&E8&-<@*Ak^xd~R`)1z%{?}&an!bt?RN~0> z!f8KnWSHmIn)IVQ^-f(&eF?28h#}vu$!MuALt2|qe_<}))6-LqqbTs*NY-dzc5Wj^ z>Pr;OYrC$d&o|#L;ctU8y>r_S@^RX-T&!SOl$+^|#riNVhmjirPT*UPtcEm{P+pAI zZ5>LhCd1I42ZnVe8Wy9KSZm<4!XVZaNh7l&@5oA^ZJte|vtq)Iqo5iDksn0T3L{%b zG&a*K2H6_d328i`^#wH`@@=L}s6qVYR4x{|igG;%I%q?QHm>9;loeK#r%j+~b3%uJ zCT%Y?&>Sjy!~kFeARVUq#6V+w%gSZ6tn1i!E8sp)Tjvy*DypC+i(b4>78e4&uUNBv zH?%yvTx6-DU7_7Y&n*|(YsDt##msTW{$i@HXx!meLUu)1Y}q-j!-%$BcP(vC=ssmJ z7}@j83hfYs4Z~+xt1W7~+9CRlIGv~f_N0P3vZRhGshuTtbV(ITv4T$3&~)-jMVHA% zMEvfRtDgdfPEF`EC|0a#IxgFBxf0fu>n~2*j+5q$>pOBWPp1Rs842xywCG83%`z>;l5X_2)fFd*^l_5L#wV77W*uu0Ppy_!LKKMCaJB zY_?(8D%g$k1mE!v7i~hL|xnljP74JfHfAiLKlG-#Ts6o0me%cdH{R*qz>~7ZVicl zq3g-xCl_2NQfJQwKAzJAQ{enOwM7h_#hH)j+Dpv;Xs052Ae;Ljb_6|G4ccO$ z7C13Te~8*TH)M=$lrefJ+<#d@qTKE=x|}EFs8mtZ}~_KDtLXz5>pBWkRn4Ig0JQIgJ}`)vr$I zHJM{T18S-=J^Hm>di3iGYOev1?OY2}lZ93ZY!NVtDxd;qino#xy}q+aD{jZPB=m+A z>P;hlV?kZ8tPaw5)kUU&r;F3A>JoKpdt-T1L2(zoEulAaG@RLt2F{4)Zu=gz(_7Mj zRi+|Y&g?ks_}=qY4~u# zl)F0<`UoTKsE>epM+2&W#pSU-`_Co@oYwu;?d34Myy9Z+36^nJRU zzRy5k=XClk6n-wD&u0qnYAL)sp)at)RVw33E5F!H>zAN)l{M(gQ2mvJzM84N+o(>f ztK$0kwS>N&eaynqRLpO5QD=&Cx(I%4-jIuRk+%J?8t9tT^vzi@(mWfQgUh!P`nJg7 zk#`CqeJ7#sit%u%Rt`KXlMLy5CHlTt-*K`uvwu*cABv4D#4YpbMa9&3;OF1D&D zs_!WlqaeVQgDa!zbfD*pDyn%!`iB?-0~N_j(?7B5XkFIX28n}o1q=RbR*Z5_sautx z#C z4zvaMfdiVY^i|ou4l5M8KJis;ZY9#R`BZWW^`q@S!~Em*@XkNK7!xJENt@ZG91t#@kldw2-Ttv6z-xvip4N7 zvdfm)TI-z{naPRK&LGuCvtYlt>_R{~hMxx;w`{t=Z|Yud7PI@F|NDkYPqDh8dFe}B z>mdS;#XCxXiPn~Hjx(df9DrG+(Z}=59gSkMu&{s)DfVN&DK?JWUxaX2)Vo+}TPM3K zQgj+3SDDlqCRz2$HPf zFOxi>J{f~0vDsvj>xWXC%TVdFXC1v8Uy-ltaMOsKPsdJ4$3_e*Svl9I^5St>Y+Biq z%tfd^jVJY(t2%u;o)direhX>>w`qg8W5}u7VlmNiA#3`g-eZK%#4q;C$6?2=&oUEn up=EBiva3$q$l>-4_ikb+YZ;+F8=@t>7r$bFW6}xrIe1C*x%iFC)BgjeNXXv+ literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/curvearrow.doctree b/documentation/build/doctrees/graphicsItems/curvearrow.doctree new file mode 100644 index 0000000000000000000000000000000000000000..532f70172cbd9438946c890aa9c68e8299c26643 GIT binary patch literal 6107 zcmc&&d6*nU72j;KyR(z*CfOW7Ldb9gCL!4g5UxN1;SLLHR0oHF7s_@V!_n9Lwp>!EZ27<%c9)Gf>~$}!A#noX|g$7^oc>x20=PP^^3vg_?qrzv~1|ucdM{{p4Lt&SgNFgx-5C| zURjz6^xjh4_TA9(>`IBHN_LgDl{~jn;$17XY+lMX&gfrC@k(Yp+-kTxlJ&6E)^l8k z5gmEM^|UUbqbg!3vZvW9v|bD~6{CX4+fk(5N*uxFM<;YlQRJMs!EzxTTNXoA&kjR8 zA6FK+ie25SdB--C#X>gj@nt$ejoD%#l`ocQqZ+hhvB-0MNt-76u~^g9=tR}8VB^ij zZtK7qJlJImOgx@Ysi+P&uGisSTh$S2wOXUrsr6#Oh|@_5WKS-tW6SEevf5Bq$Cp*H z94la|CekV0Kv&3Fc>K2R)lY?mPD|)?04qisk1KXusfG>Z`m^J<H-^iXC(K$9Wn`|m}5N?R+YM#yowOt9F*F|j+f2H%8*#$)eMvzjyaK9LIg1YVc zbkS753El%}2bF6$BhbZD0}wFu=o0KH7Bu6#)g>Lo=+fc=R%1~sbm4eWtg*`z;CNX= zm-8-fo~M4%IPPYs4?FMJy1CE@!X1-=FER+5;Vqztsv%np)&nPoxgVz1P7ND1n+c2_j*Y(}AyMhvE4q>=$U~UH_K~Dan0JJ=Dyo1}4&N-x* znA?lkH6S*BG1?1qUPASZT&)MWl$j4^K|&48P*WXbh7B3BSHw(oFD7A=NjvH&0+TqQ zeHoLW2NNWFSSga)b3>$(8Bm%{XfB(Y?cJE6G*6fw%iJEr7Xs)-3B5Q2y2Sv=xPt$K=f-N_N2U^c z35;{DZ85<`+DM#U5rn-k95tb0Nf8>rWSgTKgRB zELLBG>^j`C3B8sr`nrQybYekPNxUAL{C~368{klHOz2J6FvXU;E%jWkLf)LvTQX;X zL2+m(Q+~X)NBQx#qS`rCgyplMytuJYZ0GoPS}Y*VsleIDRdPgcpI?6{t~72>=p9|~ zrp9<@QC+xS9ipUA7n^8F7pFVaZgofJ$@#9L;sW8$gx<{_dMb#vDYY&V52#YOJq;ga z%7OQoa^SrxXR8CtfcL?~?@#Ch^UHt_vhYd?0V_ML>jn}OZe2D?9@ z?=e_A`*%ZqzZcXG0M(vKKLpT^68dolbiV;g4-Umg!%q_WY4$PmZF3m>tOtrYchW`h zYx9*wERMA8hqXZ0rKX=xijmgs*<9^@kh4ib0el3=TbM;E#S(#)=zbVsi z#qxP~Z!`OMW%|8X*(Gk7Pk$)WAH{IhO)KD#{#2$vbAlC1s?AHjxitO-cls8?c01s& zW%?V_Y6KeP7yW%whT6r|xYk=}6->)0 zr2m$6--5nAkqc!M;oJ!I0=#w{qBU)fXeWQA?Zs%G=ze?;^g{e5dJ)TtvCJ|SUy-55 z-cS$V3y7@%SfCwrIvNr;8^4N^Q4iwVq*$VBRZKwFBi5cC!kAdAqNuUGREmNC%^Fum zwedhtmsC{uN;=09!!S-&nw$7=iZI%c4c1Vi5mCkBd9(N^pMnjm8q~Sb6Y9lgbR-HG zGUK>}R~+EW>6{+n=Mm5Lr(-l=Fwv2El*js|ub1+}u!9pEEtYyiFT*=4VD`iEo!Lh_;tCQMFS0%k7D$Q zjIodAr3b_y>cW~kt&cHFnmvbW?{h;p3ba1fjIOGBfxXv~xaxZr+8G)xHn_=g_;{08 z2UY8>z;3&d6*kzg8z8h_ER_B}eY{yLRlE_(*nyUHPoH3hm&sY=Oo>h(RPb#t)I~Es z#@0i}&ophCR@rFQT%0zqEDjdE$q0{fJajnniDqJ&uA_JI!zi@tbc=H^xL>SI(}MJ*Nn0>`q^xESQlASE1X**xOz?zy z7Y55>wTUFx5Bcta#1qifq5J3k#8F;;11{R?>UxFS|{pI)Y^tS{tA zeWvZHFT!)8FUD_CjiEC+gsx3a-5HCqdChHeg6d0*@NWE~KtBqZm*H0ovQIjpz8o)!p2Tll8UG(%uq2!S literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/curvepoint.doctree b/documentation/build/doctrees/graphicsItems/curvepoint.doctree new file mode 100644 index 0000000000000000000000000000000000000000..c98a291658a6fb2ed20c70781757701030c86fab GIT binary patch literal 6727 zcmdT}36vaF6`f2nGd-QmB$+Hg7E(Y09Z0$p2wNb5u!S%%B#?qZZOiJeKl6)JSM~c< zm6=fsi3$=GcieDS+;xJC1^} zAc*`R5>^=5Dx!tMbId-L#&tqkl+faW>^1Xkq72DC{Fjrt*vwU>>p9>-OUkr#rk+Ak zWkz{g1~n~D=m@Au*%J*kN19z`FJJ>89VNTX-p2Zhnb#;$SF!I_q5C|o9F@>iNd`4h z^5T7>G!dwMrJC)#q2<|?5>u7zDs3rwZl%OlD>Zdq$_%IFFQxoS+8k~*+!cvhSZc{x zRAEG`Za6@z6FRzL_C@wMYlYUBeGS3LF!Od4DYp_w(D^Y59a}JSPF!cYkd7;xeO1p6 zLp-l7o4JZz-KW@M>&oU_R_^g-IzbNEW=~3ADARh`Yscn1&-DeJINXiP8m>ks$!-Z9 zZz#;v4xGV*UN%C-#e_-)d8Br|Huu^jkCMyf3b|UYF?%#QZI%FgazP$fmTSv$U0EJq zmW6UG!BaLkoidZrUNH%e-!k*|Q=y^L5;`4>C37mq6+5m}!@6|+$)fEzY0S93BPR27 z24J3<&{j~JT`8^+hNZXZEQWPoeO9?5;|`Nw0XQs=QFYk3J45DEF_@|51PGB zP_td1E*kCD!FveopmHr|1ll>;0|E_?E{08UptH?vi!Z7P7O$SDg2woTByZp9U+V5dekhQ zp7{gWa!cD?RDpQagq+L`%Cq}($_L(!P|uS9uCav185bx1rUw?=uq|jmLz02Hoq$~n zVAC(7eE{brRLj7PbpV$l^MNc#sE!kIv<*zZ2BU37j6`)p5;l;uqKzUTi4)qNAqhGl zL6V1-BB4AtL<*Szq{)P)vZI;phBTyU#Pn#!_LxrC9(x$KY2K5k1PrfB=y70}Mk1G9 z=wh`uUwOz2J~~$7a$cJ5MhJ zlUoyd5lej+q~2ld# z=685uU?|QwB4pZ%Fe2L@hgAF)Nnz+rTg>mpBlPTYi(}Yc;E%U<4L8Kidm^K*g1S)A z)CR-$9qh-UQMG-(RM?^GO<~!sft6FF7sFV$$Q7dzBzcJa&Gc5L6|Kek)K+Bv5LFZ? zgRxeHY;Qwfg8bRvR4=`hjq|cuj59nZ>k(cKkNN*s{43z*uT1Dwa3je^xHXL__O9C! zdNq639@u0AE%H`luY_sSPQwY{A*fA;VRm%O9B*?Ct%axZshLhok( z+6bgAOifukHg&hBDOKvu?LE44d#}vd^3aa$eK7A`3B7-M$MyjxzIt@Y?5yQAXcwk8 zXdf&{?k4U|=tGQ|OGTz#_{^ORIJ%qo@GOqpY`8wss59AN`Y7;yETNBQTE07VKkgIm zN$3;Va>haT1fT50;Zp@&eD6)@(~PuPsR8qz225RgKhx=w&jM!iBz+DTKcCPSGBEe1 z7`evZm(UlPO>X+7=0Ox+RoBf!0M|BeJx{kUyIfK34NVewU_CcyuQ(i*Ebuw z&l&{!7MOiIq3>kO?$^xH8ZWtuKakLOv(3!qblv`5CqCbooIW2+=m!kff~)M#N@b(| zp)ec5MLk7oM6vYKFBbzJ$U^Bd)u^_i&XtBA*D4?lRGv$4v+iyxZZb9+n~R(1htR_V zFfNCuPNqMa1z)Rl{TPb)Z@~Hq^!w92D0H9w>D9^!E`n z=Q@p1(myat8feE7(m%_pYfe{}Igs(w7}OlBC90cu7NJje6-GxE#!=&F-_%?zm(@Jp z%)xCUl-N-9;I*we98cxJb`tX~m9MhUZnU*Y`#6YHfq3XvDKrR8l zKsgww)dfDF{VJvnDv!MpbAcMG;sAzgvGUY>teGoi6xFwuN>LDCCg_T2tQe^Al8kCz zNiATCekiIc^uT8zOd4Q%M9ZpzOjdA zAX92FKM#0lNMp?TaL^HIh}XJ>ua@vbzr&qtgs4$V@y-Hh+gP61FFdsjYZxpofT)-w zVy=MhRV~+qd1-r5gX{<<>oEr!_|hqsI+E$+<`HuhVl+?|L- z!~%pkD8b)@Hq;7U?r}sV9#<>*CUcQMP_wg4c_NfTL~0e*51OlkII73d;qG6}7xgl< zYNU?lmjRy0Aw^h|96o9d)?n3zPF$;7O`i>wfpx?cT4C283mU}7q{M?0w(?mP+$-#w zI#$yzW3!F9p0>V)4+Mw;H$M)WW)6EB*KOM3sI~mMSWIG03(ZF{Mv+?C>-g#(vlrhx zWA3;*USFwg4)5OY;@crm>IA*Itm*~!K1*PtMCq?Z?oNhD5zfigt!Z7)SX5CH4{|)TIdVy_E}UU8wF%2+PaKVHJVR~f=X~QTj-r#(n_R!$$m$d( z>(@s%V&@R^hEU9ytYSnb@&Ir+SL2|7Qok7meR!f~b z_;9!DsPp(?5Yn}~#raq~Xs%4tg7Ab+TTpu>E&Wjhm%RWa2(pH~bnt}gLM)cmMSSIQ z9Zs$v;=35h2SLu}b<|F5B53F1F9glCi_^6Mt(s=)--XqYxMD7wSR8^3K>Cn)ZhjT~gI$ns@}i=pjzR=dZe4AH*P9Po%RyQDBme ksX6B5W`9;vLbV&DWwi&tW-mLY6RIollBg^38&`_|0=v8c<^TWy literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/gradienteditoritem.doctree b/documentation/build/doctrees/graphicsItems/gradienteditoritem.doctree new file mode 100644 index 0000000000000000000000000000000000000000..b1029004df6603a1e48d00bac85ce8ddf469fd13 GIT binary patch literal 6649 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 Ll4U%@(&+yHTGYz) literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/gradientlegend.doctree b/documentation/build/doctrees/graphicsItems/gradientlegend.doctree new file mode 100644 index 0000000000000000000000000000000000000000..912cb974046a488635ffcaf71a0cbdae1df61a9b GIT binary patch literal 7223 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 Q(+-rvORNGsqsr)i0T7jdB>(^b literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/graphicslayout.doctree b/documentation/build/doctrees/graphicsItems/graphicslayout.doctree new file mode 100644 index 0000000000000000000000000000000000000000..c22d0900709741831dfabf99843a688c9f40c7a2 GIT binary patch literal 8137 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/graphicsobject.doctree b/documentation/build/doctrees/graphicsItems/graphicsobject.doctree new file mode 100644 index 0000000000000000000000000000000000000000..b892464b888bcf221db4ff86d90884e2b0cbdb1c GIT binary patch literal 17301 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 G%>Mu#HN literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/graphicswidget.doctree b/documentation/build/doctrees/graphicsItems/graphicswidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..c6218a6b168c675314eb61b6b70629f5e2d4e4c5 GIT binary patch literal 5637 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 Jhu?ZO^*^iH>%;&6 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/index.doctree b/documentation/build/doctrees/graphicsItems/index.doctree new file mode 100644 index 0000000000000000000000000000000000000000..ce964372e07ccdc124c94c7e289cb261eb6a2963 GIT binary patch literal 4809 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 bCi<;b(?9C;Pc~{Z25%<*)un%DjoJSIS$77U literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/infiniteline.doctree b/documentation/build/doctrees/graphicsItems/infiniteline.doctree new file mode 100644 index 0000000000000000000000000000000000000000..a39e2dbcb07baa1db254b8a582feb803e0b02dde GIT binary patch literal 11533 zcmeHNd6*nkwNFB3GSkUS781ajgcK^!3CVO693dKDPxrybWzTOWjeqaaC(sRo-bIMn%Beg~!ZW`p9<%a3|MjfgR zmDI34Tn&k!e%bdzs+wx;q@JspRgVnDR_iA94MrGvQ=S*NUSO8}z@R{_pIEN1Vk6mL zx*<|W=B$efWJZf<*Zz&4^%DNsezl?R8mKEJHj`s%rd8rh98cJ)Pvv$ zGG-IGM)YNRKNNtdYSdbx_b2U}yUCE*q|mjiaBfa*DNe&|1hBB8hF{2PJ4Sw>j=koP+8QaX zqOS^!I$KB`r>{y>QxDd2Mi7u)2?N;wA(47$ULUB1O@{N;@g;p#)iHb@?fr?R` zA@<&hCH)9CbiSmvSsM+#FNWV4#gR)-l#*YpZpf-X{p^WDDY+lDCd8StpdN6H8XUWNj~n7Pwjo zuSayV+HcNbSdA~F|43Nr)JUBM<`(nX6;_O}QuUjb?aqxEwOTwhZMSC5<eLpWqr)$(F-&33hx zVI;fpF(W>bx%DR&uX;G|RR(kD=nl!k&Lv@2`0c!*>hQg)of3K^DGrJOl37*h2#U)h_4rEGkE+N> z15=^n$Y8pBVK7~x_r*rK5>rIql+DBN35T;LDlK6m!^M;)qdTXb2pg3n^(3}Ye``<1~4h)R2_0zk+NB? zT-IJ=8QIokanv*mHq%Qm2ZD7HQw<2`M#@Wtvw8>@OV)&BDpCXt>xzspgCdN0%V$Y~ zUXp~6gtKRMOsWHrWj0cCsVu68EJz8kQ(%%~`HtSj1xH*GB4a$~+yxZRVCRdv-5o&qMgy*dh3E}fTL1Mnw)wYnA{wxx3lgRbU)Abn~pK> zYlg=4h2~8mOU8w-=9q3hu*P!xE!&TK8^*j=F|``V)y+k2%z^x>iuw##WuS+j#(~`J`kx7rojPIqCB#s zF8WZ9y6D4s>+Iro*gRcksb3FJ;ubnll`^}#&Fr2? z{hFCA$VT1j{iYYk--6=;3hH-Y`};`!A!U0{o9(@k`XjUL%17Ny|I~}=KY(c$zWPV7 z{HI9$bIS5wVHxK$i!;`rBlRz7XFALzNBwIrR{z$$io^y6Z-ek&MqK0lJ81tUQvZ?C z{xfpNAq=7iDtC;v2H=0j>x2$@)qim+`R^Vo)3W7Q=kdxtyDinJbs5`DjT|ip4p$l3-w&lD5Y*$>3GP<>YN> zmt2g6YOq)H2p{pHSvz1jp{Y%w>3~TAQn~{}lYJ1yiQxzpe=H}1jT!CK_V6;2Gp$q& zobk;%nMm~7E)CT}8+V6xnzW>3$8s2gEot$JA|munZPvm&3X56J;7l}^RD;ZI-;}nW zLM%;Fm9#{eOeJDyt~uK-LMrnD4APW~3p|ngBC#1%y0T%rJG6!|SBlrW%vOv572mNkNDs&+_2JnbzP>Ar32`J{|qnqOBGzStBUCqz&;?a`D7L6oY1CVPD(GVUH z4da(XuNT5vra3yz(qLwQ(kod?S|{J#3bgH!qxFD*8XNG4=t%s=108B?l#iv=*o1xy zbu1kvxJSph#pmMr_kn=X^fxjP=ot3X=rnsc9uOAS+w>suSGRk4hY=wjr7$6G`v$@fTcvm}Oi@qZyQaO(&w zFNq&}!P6{ZvLN8K6zveye2I2)$G{>hDjGu%LZ^U7M7!|Yv9zLxC5Vfxu2QQmTwT$_ zfh7wpI$7wRBH!^ctA#c?&H5$c@=A*yAz%yUo^-KA;|w3~%B583k!XYbr{WROX+l7J zGtHbOuq~{;Xt(sAp6Smp=S3KuA$?0TKNJ1D5~H(()T3fjay}sThxwUFq7Sdf=xl*m zn&cjaA8(VObI<@~&c!35^8`CqhIwWODoT5Lv_S36KxN#|H9nm$eM|FyjP!G^-Y2v! zh-nq~Aky|5GbXm3q>ePGF2}R7b(9p6xvoO;^zK5ECO{2FVKLjoL~_i9!e@b8t!*88 zEaQy-Kglr(Vle7OctmuuupXICgk!zqAYm(9A|Q{8L5gKqVG_r!C{&Fgtya0FmO2!3 ziQ(2X#NlOJo8z-;!CJ6cS0S3i`NmS!C|wF(i`!_wNU_jHE0e7+(iwI9uOj7TUBMBG|}uB0AeF%^ztBxm*CRhyhzE7NFw~mCXuQT;UrSSTSTUO zj}(uEDjgemZN1`U*fyF1C}NTY&E}H2P(UuRxFfe%F0s*rIGM&Hq8a>l<`O4Cyj-qw zEH1?&$)o|aIGMOY*pu%OIk{{}4}n?HY&K~M+=3C*$|j28$2-Xop%E(hctjKkg?I*L z-tBY}O8^o*^7-G0gaeTHlI9MfR?8D6o!wFJ7XFY zp#|zZ6_1FXCg`y`%(XkATqRJ4GEkWqkc4uz^ervG)6p+$zh?-kYhqHx-3Yo%&3F#Y zV_|PM!iFsF`33W+<9M@t!kw(}Tbm8_I+p!o^h~BYI?duCkt9P!63MfK!2)^OiR4pn;EUeY#E>U%-v){P{-3bIK`}PcKAsiC)B=>pQLm3B4Cf?@PFMV;5>! z0A4Bp*E7Ii)sBzeeR`R+z8t@tU-UKUjU?Xcp;w?8caU1Tw5~7FD+TRU_+{yv9$|Zd zUM=7Qb}hcBL^t3Yca?^+!us?YL47Tw4yAI$621;?5xt&!*6{%r;Zjl=SDxx-;L{rf z?Ty?yfYBcyLVUUrpPBubDUw6GT24~4pQD@5@6ns^8_}B?R^O1?v&=VqpyiBDZ$S^_ zJQDnHGKNcA*b1weZWT9J=&k6R)Ys5d6$rS%6_P`5Lz_No1wnInp%8c;uCv)@Fg4~; zyr1#;Q6%Z$t9rJ+c z(EHJb3rlODD({_#K7A;WFTNc`A7VJ`C^n!j$xNW}{+q$jm#YZ<%I5A4U6N zeTx?c%`jMs{>PY5KZ{lk=vMw1;+tM>*eK8OxTBAw4PISa3meUHD};Tz4ec0>*z&dA zDK7#3L<~GUYmm#jfUapY=#v7wiQP73JL3H^_JW}b;{8+T)CbVF^Y~q-)acXvx!#<^ zr9oIe2yw|xjQtrV-KY2CqqnUG0s4XvixqGBW>|xk4Truc&FjoLY{!|n z-3S$2+$^FmN&5!29xgA+jkClmUly9H1+I+OGxo3`g%;scC${VZ!p-PpzOpgD!P-u*OqIx&xr!DE=K&O;0|J@X+jEn zj=sfUJmic@dDgB47JVBn69f8iXOPl&7_d*@zZW8Xm)}<+2>HAsI9kZBn_!#qis+Xnp!Bd@S)bT_{YL%(cb{1lCc_0f1@Fdb7S zE*L(r%E={4`WZkl%o2~u@bT&AXe{YR$yl;ooB_zmlIgNP;;JUDW#U4nKA2_~pZ);Q65Wemy`N*W Q=F=bX5z(LU8&<~t2Lxxf82|tP literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/labelitem.doctree b/documentation/build/doctrees/graphicsItems/labelitem.doctree new file mode 100644 index 0000000000000000000000000000000000000000..c40e04fc5cb0e83b4c3a7d6563e6765622664f31 GIT binary patch literal 10554 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 QWyBxQi0O}bMwQ|J1z5RdssI20 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/linearregionitem.doctree b/documentation/build/doctrees/graphicsItems/linearregionitem.doctree new file mode 100644 index 0000000000000000000000000000000000000000..40deb4cc264d585f8cd14738e4348202cdc635ae GIT binary patch literal 8537 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%0D2e>yX^_c>wz!q`@mTT_WI}nVqD1C+nPxhf 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 W-#qWY0>91S-v0wVWdXSW literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/plotitem.doctree b/documentation/build/doctrees/graphicsItems/plotitem.doctree new file mode 100644 index 0000000000000000000000000000000000000000..c80871dc71a470b7015aef6171feb3bcf574ef48 GIT binary patch literal 31151 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 N!wu#g@W0&G`G2C*dQ1QS literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/roi.doctree b/documentation/build/doctrees/graphicsItems/roi.doctree new file mode 100644 index 0000000000000000000000000000000000000000..ad83ccd5272de86afceed00c3bdd418b5dd12ab4 GIT binary patch literal 21560 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 CvFO?W literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/scalebar.doctree b/documentation/build/doctrees/graphicsItems/scalebar.doctree new file mode 100644 index 0000000000000000000000000000000000000000..3a56a7bf36f16f0068e1beb3da1d156a6c45f0f4 GIT binary patch literal 6287 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 HkM;i#1E}~O literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicsItems/vtickgroup.doctree b/documentation/build/doctrees/graphicsItems/vtickgroup.doctree new file mode 100644 index 0000000000000000000000000000000000000000..9ecc872bd0f4ace1a28c677782eca5d308a041f7 GIT binary patch literal 5951 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/graphicswindow.doctree b/documentation/build/doctrees/graphicswindow.doctree new file mode 100644 index 0000000000000000000000000000000000000000..24ef9fc734c2fc6fc99a89f8c9dd9bb3de18b0de GIT binary patch literal 3501 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 LrcdE-+OB^Y2)B-A literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/images.doctree b/documentation/build/doctrees/images.doctree new file mode 100644 index 0000000000000000000000000000000000000000..8f127e33bd00e7463177e169e98899dbc4bcd1fd GIT binary patch literal 11808 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/index.doctree b/documentation/build/doctrees/index.doctree new file mode 100644 index 0000000000000000000000000000000000000000..e1879d0ce48a54022363186d745193dbdb778e42 GIT binary patch literal 6042 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 T|4B7dD;Tl=Rq(&_&dPrPIG^{q literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/plotting.doctree b/documentation/build/doctrees/plotting.doctree new file mode 100644 index 0000000000000000000000000000000000000000..5a3604feb0645eba883be551c3808b755a7e5d8c GIT binary patch literal 26167 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 s_v&^tpQ>@k!HDVZ)Wy% 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 F@j7T&e%1g0 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/checktable.doctree b/documentation/build/doctrees/widgets/checktable.doctree new file mode 100644 index 0000000000000000000000000000000000000000..d64858a9d00470e951d0f210c8d040cca12c2ca9 GIT binary patch literal 4775 zcmcIocYGX26_#a7x;smJW=@4pw(0lK__ufnBz1Ms*w|hFB?fZ|P^t<2P?wj|%_r0?3?KOk07r9C7 z2WlpaJg!RmXOo9y(-Jcm2=2}w zcIpAA;n_9S-KV%YW7~e{$F@x)kSamS)dyp9uuw_ZMwQf6?8kr;g|^2Vii#uWU1#@$3p;IvhgNSD)Cg_S(+M$*xzX}rQFog|JLQ51(!%xx7rGNui) zYuJeP#`hFWq_oiz!}uG)UxAgn@JuGG;0izRV1qW*Xmc-5io4oSiMD{Ittp)bnxwOk zq1na;*f3xNAZ-_eY&g5VqgRdOEt!PA3+_v_vswo$Wf3)bIY{Ptc_EVX<)#z*$_|`* zSxc22m-d$fzh2f+D|ez$&I4!sFSqTL7kqDy$Ev*I*^E>%?Yj0T?M~@dbv6<^bD9s@ z!$vZfXo{7bIF^1riNW@*Q@V}C3SQFEa*9r`u@N_Ll*0EjYOGLq+EwYSFBYFf{% z(QU<~!-m@OR*j}b!AaPLWFN?#NvUj!Z6;&7 zQtfQ9U2GLQ#BQ;N4H{EOB~GoKX{dYvRnBuxg2bO_5o;W3QpZd=a_6zgPR6 zz|fsjx(f^oZD}T{J4xMDE#ZfYGmhuAtHlpJzF49IfO*%H&V@7^XydAASmrj}O~X3R z(rAFG4bB3KNDk?9RinFGNDx1qLjfJE?m-HyxH)ARuBZHPsuSvIowG6B!+~d&%(0VT zLp8f4x+kEWpVGa0psmw)=>m=H-WEzCYD2wnnGJhU)A2*Ps5)p0`2^Te{VLrH=;G>- zu3?=DbO~Y#h35LD;*u_4bRX*k*0iXtd?a3+NJM!S7!Rd%UmfL5tK7HTC`g*2nx+b9 zJzwl##q)GoBY_Um{Xp^fYF%`H?a>1?*YrR!?6BcxBnV7ZRD1K{ayHcV_zG0`P{Y^N^pNFle;;}Zf1MD0$P{OZYH;|#N9``Fxp1^H8NN3b`i5kE) zmy*!9bc61s+)Nj?(e5+kYhYLP12zv}tCdjzaLts$9Nb(VaBXA}khM}GoDi3Hfhig= zCRS1-QT>p_8IqOqq6A2eq_mJDY4t&Zeh!vmE(2eoXDkBJQc4fckLIzPG>}#a(<3yt zNA|;(p1evrYD)Ad*tjjXGc9r9vN%O=6c?KcY%8bh#iioV6hpBXmXF zl+tT8(5@^7&KonFrUG8qPs{6jD&P%(`Novql;gRns{(}1@i(XRmi%TO9h22>?PJCi z5NfFX@8~gst&gP>szxN6T+-VrY^*b@nsMswDZPUg@KrZAMej`MU2H-vHS1Ae=aP!v zU8DD~jjJYIbNanCdLP@|BW{~p@2}AZSkd*{4h7|ivI zN}s9GXIVk4Hu$VClVRZSJ+Mw$E|bAJPJ3k7-3w}tIhYZ=?Eu? zGl3t>4zaNeL^hWHup-W>uw7irNZLRed0^CT{}fK>1qj@a*o20ta74gh05|E!Y{=vF zWR8APVH3Pr=Q^-U0at?an0~s>rM?-SDK@skD`-+p49x`r|TN%fln|r%L8mOYy{mAI*UNtm#{9MaVKAPe&O# zfug^lo-`*|a?Q4qdHmO9w!Xcr**bgaZ-A+Dv@6U04ii1Z(?8fy5;yi8pntMbc3da) zzi`?QyF*0(Wg05D_=CaozFk z4u!I$PY(K?T(7^3Bf-1kEk|*2neA*BEDyNp8elsXcD7Z^F^HftvH+R9S8^N|Yiz42 z0zXt-nwKOz)S>j`2HZvItj8M&6leR|MEly9p(Jmsa-&wB)Y+EaXM=e`A}95s0kg=< zP57S5&G^)BNe*FHIxZ%~)&zOn{i4P^pX3%JycM6A)Xu_`Cr>j6vF@6^H-80!eI2_w hwsKa?Tep(iAX<~#@yUjD9(qddz>ie!#Ai~UxdkBOoWcMA literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/colorbutton.doctree b/documentation/build/doctrees/widgets/colorbutton.doctree new file mode 100644 index 0000000000000000000000000000000000000000..e9352b2bf15d4fe1369cc6faec90133ecdd4df05 GIT binary patch literal 5607 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/datatreewidget.doctree b/documentation/build/doctrees/widgets/datatreewidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..e9673c847cb734c4b6d099d63bd38b54f296de0a GIT binary patch literal 6995 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 V{2Kinm3E+Z;w4rtexvf(zX8Q%IO6~S literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/dockarea.doctree b/documentation/build/doctrees/widgets/dockarea.doctree new file mode 100644 index 0000000000000000000000000000000000000000..9131d6a138603600dcf1490289f73706bcabc079 GIT binary patch literal 2862 zcmcImXM-F^5j~xJVOP2!S(0r|hvg)#fV&YOz!+n)2?tA3nIjCNo|)e5;U-mg&jnzx z1sHVXoO4b;lGih{cc(@0jc*&et6sf&^{QW*jp8DzY?7MIyojagyRVtdZJ$qFzr(XN zpB_*z7n!sNr8N9N&2!r;Gcz+dwG|BJ<*3pci2_qpI+A>5Nc~twg%<8MpB>V9QQ2Zt6gDrc42%`p^3v8R zT5xcZ*zj`AD{7urCRjE)EUShVBbB5v;`2G5&rcU=WaN_c`9aipq2`OIQH#9}$t9Yh zc|Zm@en`#IeE0jq(_yqMb(JR(i0kts+qXfBr;1E^X|*H0y+ZGJnaC3pq$2d(DNjUv z(@T@kbCi4K0o$<_rSc}MH?@0hK5s9H$I=@2XG8U-HkM!U@*Q5Q`Ep1LR*W4JyiN;U zLHtVU3u|=}Ru()zTJx0wEyh*p&KZ8pr-djL#^CegJ}rhK+R?5{Pxy4&5&ERhPYt#o zK^-lV8%GE?Cm{H$T9q`{2E69;K+TJa9!Qg1^7XA*Wa|XPuTrxLwaTExe;$?vZD zJ+x{Lvam>l$w|Y{`}|%ych876x8LXU`{}~e@u2zZ13qt2FG_49bHg9>`9lsJot=0= zBcmZk{|Mo+Ykb(}k2qLmp)GKKbV!Ry+}Xm%U<*qpu^9fi&$no4lB@+lHNQZs=ux8K z00A7u4H$pI=WSXv(pt2{@F!0wcBG5k$~^ABAkqr<$e$|s(=~r)K<6h^1a6D~e8;d4 z7!M+Jp<~!ziF-1S&~X0jkk0B+gro4mN~avqnzFWh#`A1ZzMuYaN5_z1WaT%>Thz3^_5~e7VbDE1~SdFisg#2*_^-MAU^KAn^M3HBIdUBT-c@(#ewd{pG4 zWXzxMdIiPpCML0k<}V;}lonzqkeNu*09K>19nsh?-lcU^k{wOjan}P=I3s$RCc@^4ylI; zE|DqOq)EmoO;$?!? zeGi2;aylmX`xqUklcVYW0X^DH_uk&#W_e&$kvG#F%UusAjmJNv%lFbXH6=e%nwFiT z1;-Tqm}Zlhe}eo4-Y;09T_=BfNNepJ%2YOH01~YVCcFM;PLfQ( zH_tVhyWYh73k<>;Jccl8?GL}i4@*tOM-KU`VHMKFX^(GTXusZ?X&)26srk19wTc~n zb$Tba#@pxLVWa-O=0BX+sLHL;`St++(e2ScHTxbbx(kH`5&*i z3Q9lu25mRkUuyoVTF0hAw7+%HT%hh@hZja0ih2`IH~e>>|3ULkns^uev*v$Q;pV>q DnwcKC literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/filedialog.doctree b/documentation/build/doctrees/widgets/filedialog.doctree new file mode 100644 index 0000000000000000000000000000000000000000..17e770c9cf535b209befd0b13fdf625fb7ad9cb5 GIT binary patch literal 4763 zcmcIocYGX26_#Y5ba$3yNiK24g2Z8!U|j+RObCWR;v~o%%@Zzx%d&SnceByn?!B2^ zO9Ga}K)|9C2%$r$0YdM+_uhLiq4(auncF>`Wc&W(C;jfXxBKS3?|rZAdw0!H-3{w; znv6XoEDFk+Fa54gmPA;I505K@O#z_Qn)^&%JMa*J}-5w_Ey)RcS^HJ26}DdjY4j=7yjwO)@%L3<;t*836(6d z%_QAQV$nHbyVxprh}~ij%NcPxR{-qoEpd8PoKY2ftK!V6u&S|uO_5r29cJbbdy3cL$!8 zQ^!t%4b@yP(mep}f`sn52HJZ4lYx|SZi-0ST_PT{Q+pbMGOEK(KjUYU@s z-5t&-oQ8*okU>of{L!Jh9RX8j0$MBubI26c@!nax|fZ3`x5W5_EI06mjW$3LRq+kd_j9Savjv-IRf}N|+w5 zu|1+6w&dhB(p6KWN5bCK30(twrtVB(fQqFwXF8?OJwV5_%}4dK`DpN=E7)kF$AF4! z6MAgs&(($s^gMow)?(MgAToo(;{fe=Ld)4v6sk$%PO7A0^mq;E3H<;)(Gt3mJ}IFm zXR3~i9cZM&bkL{t>7Y-w#KDRMXR>j_nwhp%@^>ZV(+Hsm-5LFwkLYQuTd2^@^16hc zzQ(+1lh3fkMa$w8T~SwCMxGcBQe+zkmm3#uzS0CWXXcSOpX-Lnza z>pC0?)6t$|I@)u^h$BwyWY0t3o}bVQR(G-&YT-&{)5$G~D^NzO8`q00p{wl1gkG#6 z=6kpVoEuU&rpjK@FNiNiR`njHmjU0)6M99)>5V3_gsz{P5_)BJJF{SFqR^|Td0r3q9y)nabb5HIIo!xIr=*`)~EHWma-_pm3 zDIHW#=ikwz0oxEsCs2)0wz#CXmf2W$J~boM+Y)*^%i~hFG)3=7=$&jrEwyT)Z)cK< z-c_Y{v&mJ{tvUUkD!rF&UL$UsNAIiB`&psxbzVV=K2W6(>dAqPW@EG&&OU^xxr=aR z;QnxxKB8f@Ly7)JA1$-I=cW_&$1qWk^zD{lzU_=J+ud6Z%|8pHJuu7Msebu=VT-;PZ;U2pF57 zI+QLphc?Gt7Su6Z(U;0>R5t1m#R?HipT5j?iYRKIS1Lteh_%P#QDZig&60>(eu=)q z3SgklwQBk*w2rrVTl>H<%-5mtYh^aBFu1kR09$%fQuK8MG77bg49+)LPEU7B^i4MA zJ3%wX;sSY>)3?|VHi~bvg6m+qj+|DTzM~-;a&4a<GzQQ?SyK_70?k66y- zwYW(?F0%>Vs&O6IC7&z7c|<>1X1l{UYRA#3bp8~hE7+(|7<^_!~75jxIdBKr^1`J)?Y0)psY>Vc-fkjeB&5n2rcq2}~VtG8+ zxA&a=F8!KK@~Ao!hIslr%f(S+-vRmuE2hVF zLjMz|y`VQl^e&0 zz`$Idjjv86If&aV$BrD@cR&v5>ydQzmh1Gl+W&uTmFu;kLX!s^TYe~I4&ULtNwF^J za@gD*)#;NJy&Tae_oQ{du&^-OUcwjyBr4 zDU?Nha>#S#27NJ(1n-Hr9L3FLwzE^P+~=lifbB@w=_W15AcD$B17z}E$#LAQvaO~F zyg+ejei-3_4y7wM;wegJ1AcWtadz%abncBAO0u>pC$;jp#1t3NLe$H?Q}A7{+_Np3O1Tk(k*?QG0<@-%Z0E3Vmfv!6Y%qhlY( hCe8|3>sE3bM5}T;KH0F&Ls!Wi_>#z-_>60_{{tt!mRbM+ literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/gradientwidget.doctree b/documentation/build/doctrees/widgets/gradientwidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..6d09b2c6e0f444eb359d0edfc92cdce636abb0e0 GIT binary patch literal 5742 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 MCD9k)H?EA|51$LLIRF3v literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/graphicslayoutwidget.doctree b/documentation/build/doctrees/widgets/graphicslayoutwidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..6fc16da9f247b1fc085089824fa13e51cfafbfb7 GIT binary patch literal 5215 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/graphicsview.doctree b/documentation/build/doctrees/widgets/graphicsview.doctree new file mode 100644 index 0000000000000000000000000000000000000000..179bef42902ae1ad6d85143a1dd876e643acfc86 GIT binary patch literal 12436 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/index.doctree b/documentation/build/doctrees/widgets/index.doctree new file mode 100644 index 0000000000000000000000000000000000000000..4f113d6def817a500583698574297206c413200d GIT binary patch literal 4236 zcmd^C=YJeW8CGPg+}X0_9%7qpnxjCZ3&D_JS|AVzf+Yq|a)fL)bGvglW9{wkyE7~4 zLN3@qj)>lS@4c7Md+)vXUcT`U@XX$xbf=&5kxzWl?{qiwmgjxm_kHHncn~H*Zd9Zf z;v^KhYX5DE*i`APOHR{xm$uBYO3d5BbW)*dYnLYKd&b7b(hiwc##4E*c2-o_5=PUc ztJyA{H?&a(Hj~&7MN`vMm!{c{lxJG_ZJ{-9!BEAcD%%m{Sq5jmPVy`ew9RGJPy|WF z?QPobvfVs4Ni#`IoEYJ2!!tuOi)XPZ8)tNyL#`QLDWloX#@HmnMu2ocjRvZY#^$4dJVZ`ACm8V~4bEmDn|g}$~Pdu@Pzk$sG zKA-Qg=>`v$GFzq#JhsImdZ9-b$yv@O`sa^$bg`V|IolekSkTzgsu=1!hOPbU%b5t;#$|DdGnu{&v&EGsmNqs6kwc;p=K=i-3-CA7|pRDpCr9w*nLnMY1D$(%+GGOD;` zzsnpI8<916AoPNBLJBPgjpPJ8c3L8~wxf>IC~Sl{@9?^ z`6ps7#d*6SG7-{=HFtXd2Noy1Xxu3uwYTT#-W8`kVVz*y+dha;G1z%f&p zXmyXCH0RcL1KxqDG)heWGu_yuC%g4sLqDoEw$ZW@D|AzjJh#4o=(ms?Zc{EV-`pdA z)N&w2uw-~65_C(CxLeGgf!pHy@@k_;!KiJ_m=Fy(2rTT87`+}OQIcVT8|=Ymk6Ld1 z&`@Z_h*E)$cqhpRMr4naTb~^|hB>{25ya_akCxo}zM-FNGh?ZX%w|YYkJ@hioT1;P z(k6kMcC>Ck?ol$D1qNX&!$uDkkCGOpJtDWhbI6KLRoqBcDeI9Q#jdF$QC>4W%H8_j zp=*Qmioz`SXl2yx$)p2`5iFJPtoEqm)(;HbZ|9LwLv_EkM^71bKUCM-dej}o216a@ zF?)GKM09(Po;vCu5~1KtS>01TIxX0wRAC66@w7FzUBy9^L;D~{O;7je8FTVRd6T?Z z-Xa?^luaq+Ng2slrZSU8F3VMUtGrF#E>8(I-R7&INR6aB7RPh+=C#Qp}4^C=&jz`aB zyGFEDar=25J)iB}blfj~dVxnTWR*anM^ypP=X5g$Jz^l9T8a4}2q%z1jAJVW8p|ADm zb!=J-W3bLNy?#iVG_zvjhSlqNltb&%8xneBm)3pw+mQzPChm*t4?t^69+~pM2Nx&UOc_L} z4nO3|n#&G}EK4#UktSBU`2Bw+WJiR;eVEPKv@nRYgu@7F(nr`tC>nW-KI*brf$?q& z+d(TxAq;(NjUC3!N^^5}Ha`xf4KxBncOfLy7CXXUwG5|E0KJ`I-cJ2>PHFliu)?HM zaWg58eaeEo!n4>03|oS?>C-OTXK}YpX{+WN(MG&R&}Y~*ewe@LvZEn=mhBR&3J(&{ zI)`8{!22AqnGBM+sao{;vQ}W+veL>V8GQkkgDBxkzG!n5`Ph7RD59`myptV9Chhf3 z+o-y)Q;XG?*4P#iFVmOZGOtvNJVYMth`wUsTTK(NtdNQ^k2HN1{bVtL)GStGneo@w z*tY(2!ME(CuOm#$(IJ+711>fJPv2w{xoOT{N#A1C^13DTw{cs=!zH5cuu5@qUIT>h zvYmdYwB3B5V1di`TvkB`r*NoN>H8Q=6>LBsBNCJB2a9+Bj`E0p$R^l{YvB4vY-;fS z`!VzWw-2YEuu5N7+q(S}Sr%N{K=)_p8%vV)*8Vv=w`}c|m6e6GV`LH+tsRQx8cymO z{em4D#cETF{8HY5gZ(BHktiks5H!*sJlK8< zs+WitMAq~>kL@ogtzx_>ibuDIZDB(CJ-T2Ub`|=p|HU8h#Y~a$`gr`Kn>X0L%>%yw z&i>QlSpP-%=Pvz4&dR+S)Ej=?_gBhayYx4C7^5GG?(cvhpe!$k literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/joystickbutton.doctree b/documentation/build/doctrees/widgets/joystickbutton.doctree new file mode 100644 index 0000000000000000000000000000000000000000..4e22f97e174bae06d02076c8f3627aefcc26bae8 GIT binary patch literal 4859 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/multiplotwidget.doctree b/documentation/build/doctrees/widgets/multiplotwidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..c548d00196cfbe220ccaba342825a83200f9ae2a GIT binary patch literal 5295 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/parametertree.doctree b/documentation/build/doctrees/widgets/parametertree.doctree new file mode 100644 index 0000000000000000000000000000000000000000..56b8107738508625a28d18ead11cbd1317e7ffc7 GIT binary patch literal 2912 zcmcgu`Fk8k5tZfW*tI3gj%){PCC+890ci{v2qAhd}kx9E>O2ZG-Jh!zvGc!{P?S7QnYALT}MO>wl=ZA*Qn<_Lmv2a@CK`cjx z7iwOlWxOyl$fPl13{O3u`?MTYS|eIuib_Y4&kU&_%c#)8z2>t+x*#fBjEchMg_VJ^ zLR((iJVgr*QxY3quKAprr_~dH);mBeh8820q%nf4PMP#eR zZU^TQ&Com?10i2lvozoR{?N1=ElXYHNdyY}{F*HZf;?4Z(o3st>FpMJ+sj0rm>?CQ z=d3&t@og_nLeG)!mHTYRT9nG0@ZS*Nweh^&Bpyp^yc0QYXk+=cFW==WHD3*B!HTit zgx6@H%ZeYSzOYs&VP(PiBQ;+e&|+Mb&d%_oJ}pG4Fb1C=^Jy^@(YAK=dfcbej@l=D zesZw&Fp_EMTt9@laRLcnS1Xd{TCX>J9;kUy(F19cOTM`|i;$hD_;qSlp$xAdOcMyf zq@eB%sKR>98v}Le{wnj-WwojvQdiU(%{34CmO^rG9H>Wq^_Z_7_tg`=y5Uy}(N!1o zP17Oo$UUh1&G(OU8{EF7=C=ZjaxvGdP*h=LN|oe$8zPR|j!E)Z?)CX?fc{j?Z-+I_ zvj3@hXeb}z+nW^7V-_$0xHvqG(Nv`0Hr1Q7#;QO)-~5`6M3BE z{o0_d%gz6xMq8Kf?}y)I`Q5^?b*f{l3MGdieEQUYn^Br261Zx3M z%`eglN|tC0i2#$w^&o%B=Pg<^(ppr=@TZSSd8CWn$~^ABDAEeEls{AOXKVi4fG$j^ z2wal^__kpmFdl^KLdO`BCEm$ALjC#kLprNR5lk^JE1hycSCqBo)1GIG0+TwCcC=CG zv8Qa7di(|Ifq_Um*DT<5xs;{LL1J)4a2O70*+5%m5EYpl?uH{2(iSep1WrtIPJsI? zX(<)?xWbf%eJpuIvzVS^>cs+M#EPtBX>v|;u?(v*7eiW+ zStwm$_fu(cmG`1)T`;~jNhD@@J$8yJmrSbU3boC@> zQ?2q7rD@rXwcv=SpVDj+^Un~!z@38?+m-X@2Xv)vMw!ZHB7jM&g2_4X3rN0Wz7e$H zU;1>Zk#>?B9DVM#lesHU%)df6oWap__`|R9!%~y+;h6l*unOto^q_Cgx!-QiwD-pE zYX1E|U7udm?d0wAAFyHnSo5EbZCK^j>U?X!e|CHIFU=8v%jK`?y1;(L(lywU_uO63 zUIa%wPOYfb3UWVwu(oULZ#Dm2tzq53<3Bo&&RKT=;`C_UQExKX4gb^Uf6=_tCr+n- K*ZiL<-1sj_1vL=> literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/plotwidget.doctree b/documentation/build/doctrees/widgets/plotwidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..29b5c88905e3db6f5b0e86ac7636e56aa0bc8adf GIT binary patch literal 5476 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 VFB{XL=qPzAUSfF~exusle*yIBbF=^e literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/progressdialog.doctree b/documentation/build/doctrees/widgets/progressdialog.doctree new file mode 100644 index 0000000000000000000000000000000000000000..0c6a7f2c6f99217919fef5e111cd73a7fa39f810 GIT binary patch literal 10374 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 RO6(RM!b3#A!8I&o{u^RKDUSdE literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/rawimagewidget.doctree b/documentation/build/doctrees/widgets/rawimagewidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..8bcfeb1b13bc4713c391f267bbcf7a3cc8793c4a GIT binary patch literal 7770 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/spinbox.doctree b/documentation/build/doctrees/widgets/spinbox.doctree new file mode 100644 index 0000000000000000000000000000000000000000..f3b88a281239619f3f08a306c07cf7a54ed2346d GIT binary patch literal 12210 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/tablewidget.doctree b/documentation/build/doctrees/widgets/tablewidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..21539df76c1e7391e6eb7963f1f8facf975c44aa GIT binary patch literal 11304 zcmd5?d7KPf{+jzBW5;ic1Q>T7es*&j>V0{G>nFE?3wA^>d9>P)a#z? zuFy(Ql!)SeAc{9CD&BY>DB`VnqT+okig=6X@B3a)&-CofX`CHFK3;EBJvEKn%|5h~6oZ_>_GY6tAhlt%|Lla}lF`HEG@FjU4W z>K&PiQ^>FbWf~5ViG9do9x@TVOk_Mild$A4`GGdBJfx2e^@9s$S71%Br|C^*S6u-8 z5Hn>30XcW8My?ozGFaK7H0GrNiv%lGk}&Y9f>t9XFeeuFu4sZ;XxmYhD` z>`jo=LzdYQP2QT*!>Y%snae7UYwOJD5@4=-zdiw)>Mkm$Qi9R+ww&JHW_fr_1)4e$ z8r~7=lb~Vo)MPa5R0C>*+Nh3Io6HV@*C#6oaY|Zk&Z%@x4d&FAoZ6bJDUeqnszB;t zvn{jFo<=;5w4py0syi*z4+m+*q;}K_R;|$HUyfU{r&IcLF#3p4KQeX&FgC_~$*?|y z`8qSr?0~6=yRA?!e3Bg7&*`($a2>~;sMva!#BM@RBP~`i9M@=g!Jz6?oq;~vf^1`T z(P{;Cq>YCVUWRm2)VrDPIcc-Y^&&>jJ#2QCyh+P(^?BJP66bA9d4)2kBYl3h0{}u> zkHUIDDE$|x(F9ffsC1i77&PxY@VB5wu;Lh$vnSM#W-G1+&V+F3Ef!7qgZe^f_(-+U z>^aS<1s*)*biEh)>u*J+AHxFeV?}CHby{ZUq*tmzX&0$uvOOYBJ#XrZVZLmrA6rOj zTtAK{b&uC76^VuYZC1a;?1&`JA**zZI~+uhKb({|KaaeY>w19%WlBE*0$ZUju)v+M z5)FvB2mZ=(tq{4IQ*@C9E6w4#v30erMiRL6IOLlMwTc}MzB!kBMs$!25z;B`fUk0> z4=`V;q`HO^ye&`8(G_NFav{cCFxHApdtj~_Y8^9IUVyoXGXiIRr~}MUmn4|!5oW|# zHRj~tLYz#26JE~}{-vkE%1o%A7_)-qXkI;G5#hDiri$YuCtM1iE(>)SPmPVYT6k&! z)=y&Qp1crqm(Lw04Kyiz2=uN9^;1AkLQg^iy11 zr$ZH2h5B$T&lREyXoIuHI!PVkoRI_6*G&BkrgHT{RG!(Ey_rWTUsC#65bByxKbwUj zuyXb^R^pOxT$nJDG35lx@Ua{kHQz>b8sjxr)>z*d_sHOsFp1DA8YPr1hO*oeR+Wk2 z)LE9>9JDHx83qTW0SKey_|=LvGoqgZ9j;RwvsIA)Hy7P_eN8YuT%8#~p7N2w-3h}R zZzkuKn(euW_MV15^z&Go&!0n^ql@De>IE>`1Ei@J!b4sZ>KDUUinVr46fC@OC#u*& z#Va1j>zWrJrMv|8y*AV@jhzaLRxERG0!m-UQon2tsT@!Jcw8Oy$-l zt22g0WX<@gs6lqoqf))kNFi3O`Z!1IR&RVOzkS2lzkl1%_MsDVL&p9cLnjUG%;i!g zrbJ5n zcfy#Fy0;A(qUasrkdcbIw%5B(YIdDi@7fvaw}IIklX`!9JI%~!57T@H^#1>#^iHVg zU7>zAtB9%I6eUNsNnMBR$i8`VsNcilEQUBaK-`ez>%DXFwVe5SANZ0fAn^WBe;|qC zW+7rnL^uI+8Tr9be+WSQf5K@CWu%h&@xu$$j~_{^UD-h}7XJjM2aR;2T1+M+13{y_ z(xB>6U4j1StOAn%d~ON#$7a!&I`ZRbb?#v`PuZx>m*ONsu5VQrs9Tdo(QX{0px@d8Am>Fm@$3H49o>AV(S3g!N{*W%&C*2mA{ zsQUQ=QS}Rss^h_FbK-r+Rk!L6^e^X#tGhz|tAq*^vmk->>-!&AcO$UwP6pO*qQJT* z)V~Gp?1ow1o?x6yr$KngUCFrmoy67eRrg$R^#}OGA4C18*0}mJ1MknSo0rlYk@c4- zvhEG_Uztn(TWE3yp6*G;*54LzDE)VMZR>RX53u>qQ2#5|?Y)iYqD4r&AuYx;9*ik* zjL{MqXxl7^1DTd$oFk2v37(D!Pj(sDXKkv1G80Hd9CZrF{203!ex!kcy3xM!5cS{{ zQc5OE;x9ABSs_Th0=2ve)vzT8BK66@{IvTq&I=i>5Iid*JXxO&$=i$|AO`~%R3_z<3%KSSe30rb>91o=CNR0GQ zjKBvr;}udG&z1~HgY}8~vnx5anj82+v^_o27EF)~NyiJyA!)CXG{Jm=OBN=PBx$RF zw1r6{bqzColxW248MMLj33!FHO(sW)hAB1^=5`rBF)^MXEID(BjLl29Q83U+f@)_1 zRYKE}uufhW6`g`{NmvgPR3i~8*&aO$S}}aPf}MC9fBHzqM&KF6ic>9k7L|tSRGvFf zW+v3e*vllroF-`61ZXChhx43~`$y(Ij3g$KCI&J29b~ZRe0MCp78`=mOr9e%oP+L=&K1nf%ur`aGQ)W? zc7HR&`G7`d;8!v;j0zIY3>V-lq(@1+y=F>?B{dU`0T8~#Y$g-M9+{6s@n|0CohwmX zh!MEQUc5qj44$otVqbk?do|VMVUBcRVp^0gE)v8SOFJs75)%i?EN3!l@PFt6#T7l4 z;iLMhiH9DCUI?)tuaGVg6jAD6>dn-VlkvwV#uMZvb>wBNE%F*U>j@Z_a?27_g$Pxn zSb_oJz&0l6TN4+1ST4VoS(QsAM|c z+}zF+xx(U|M`c?jn~Z2H(kGY64wRXH203bU-P3GgYvZG=jv@6(!lk63r#U_rF=pau?^O<0D zE;+i$T*hZwX2;ok_wLzCQ%pN?T1eA8fotX&e1!BwJflAe(7jX|vz0lZR$%jN3mV{xI0`Qud(-6tcBlac2Ck6C zyn_f$cDWfJ|<>gZdKQU}V+35)o2elINIRRDpAhw%#O z8FAtFBqA0a(gP_yDe=Xug-GijMw;iqeKEv>8 zIC?M&70W$<;B@U%jat+AIQ|#&57P^P25NaBULm~*&sZ(Y(tK)pu|U710S$Jql}1x7 zix*VO^1511#ev^*Gr>aC(IA)w-<&}`^d(*4$e;;D9*=d$#E^3F#;UQ8wI;);XDXq zn|+)P8NTu;7^YW(Ahh=?yh3_4p0W0r`8l*_oBhQa@%4~=bX$U61HdseRpQeGIkBbJ zO3!LOXd3st04KS2-VZDa=yezwU5pcSoDAa91KogjNUz5;&1dkujjFKaqk7bk!takl zp;KeEya5nugT?!}xFf+L^U;(|^hOMfnSBj1e5HbJ6s&K;)9gkE6Z7fK()kwdTjE$MgX}pLD zxV==P3VjlN=6V$b)sajl@H|}ha_nGy*rSPz3MMNV`V>R-KyO7`?$CTXqR?Npt1PMw z*DM10X9Rgaui{nQSeoREVm^IVdiw&8Nya>Wjxl!d?U5P!Jb(8g?bf7m=4skK@Q=EMI@M%2ly6$APfH~RRJ6E?8&(;Rnn zC;DL36(t-h<{K{T)3?!&Xv7JpF?f}EG(??jq=Tj#<^3uv_9p;3Y%aCk zgY;8D771SU?OF*^PFCn=(!CO;i3dk^a6gV*tK!qorGGVR4_755d)Kw{3&GhN<yas2ai56p z;r5C=PWE|Ig|GFo5`Qamdyr|W;KG@v-!T}6oK?u<jVW&c6esEK$?X**V|T-6HXF4>OE8L1?ZpkxM&Ht?uTQknNchVbd97fsoxQ|GOLqrPS|oQBsT1EJb>Yd& z_8u(Sw_(w~2aEOwm5O%PdgXt)X0C3zu^KJK-7>ugPgLb6aH*IQR49vDel~C!QEKj* f*%L1V; literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/treewidget.doctree b/documentation/build/doctrees/widgets/treewidget.doctree new file mode 100644 index 0000000000000000000000000000000000000000..3d17105765b1a75a0e515527823f7e27573d5da1 GIT binary patch literal 7154 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 literal 0 HcmV?d00001 diff --git a/documentation/build/doctrees/widgets/verticallabel.doctree b/documentation/build/doctrees/widgets/verticallabel.doctree new file mode 100644 index 0000000000000000000000000000000000000000..c4d66a574b2c32db97c8db7d2ed317a335ba0e85 GIT binary patch literal 5483 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 iqFJJK8 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 literal 0 HcmV?d00001 diff --git a/documentation/build/html/_modules/index.html b/documentation/build/html/_modules/index.html new file mode 100644 index 00000000..4438a15a --- /dev/null +++ b/documentation/build/html/_modules/index.html @@ -0,0 +1,89 @@ + + + + + + + + + Overview: module code — pyqtgraph v1.8 documentation + + + + + + + + + + + +
+
+
+
+ +

All modules for which code is available

+ + +
+
+
+
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/_modules/pyqtgraph.html b/documentation/build/html/_modules/pyqtgraph.html new file mode 100644 index 00000000..7779bd3a --- /dev/null +++ b/documentation/build/html/_modules/pyqtgraph.html @@ -0,0 +1,192 @@ + + + + + + + + + pyqtgraph — pyqtgraph v1.8 documentation + + + + + + + + + + + + +
+
+
+
+ +

Source code for pyqtgraph

+# -*- coding: utf-8 -*-
+### import all the goodies and add some helper functions for easy CLI use
+
+## 'Qt' is a local module; it is intended mainly to cover up the differences
+## between PyQt4 and PySide.
+from Qt import QtGui 
+
+
+CONFIG_OPTIONS = {
+    'leftButtonPan': True
+}
+
+def setConfigOption(opt, value):
+    CONFIG_OPTIONS[opt] = value
+
+def getConfigOption(opt):
+    return CONFIG_OPTIONS[opt]
+
+## Import almost everything to make it available from a single namespace
+## don't import the more complex systems--canvas, parametertree, flowchart, dockarea
+## these must be imported separately.
+
+import os
+def importAll(path):
+    d = os.path.join(os.path.split(__file__)[0], path)
+    files = []
+    for f in os.listdir(d):
+        if os.path.isdir(os.path.join(d, f)):
+            files.append(f)
+        elif f[-3:] == '.py' and f != '__init__.py':
+            files.append(f[:-3])
+        
+    for modName in files:
+        mod = __import__(path+"."+modName, globals(), locals(), fromlist=['*'])
+        if hasattr(mod, '__all__'):
+            names = mod.__all__
+        else:
+            names = [n for n in dir(mod) if n[0] != '_']
+        for k in names:
+            if hasattr(mod, k):
+                globals()[k] = getattr(mod, k)
+
+importAll('graphicsItems')
+importAll('widgets')
+
+from imageview import *
+from WidgetGroup import *
+from Point import Point
+from Transform import Transform
+from functions import *
+from graphicsWindows import *
+from SignalProxy import *
+
+
+
+
+## Convenience functions for command-line use
+
+
+
+plots = []
+images = []
+QAPP = None
+
+
[docs]def plot(*args, **kargs): + """ + | Create and return a PlotWindow (this is just a window with PlotWidget inside), plot data in it. + | Accepts a *title* argument to set the title of the window. + | All other arguments are used to plot data. (see :func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`) + """ + mkQApp() + if 'title' in kargs: + w = PlotWindow(title=kargs['title']) + del kargs['title'] + else: + w = PlotWindow() + if len(args)+len(kargs) > 0: + w.plot(*args, **kargs) + plots.append(w) + w.show() + return w +
+
[docs]def image(*args, **kargs): + """ + | Create and return an ImageWindow (this is just a window with ImageView widget inside), show image data inside. + | Will show 2D or 3D image data. + | Accepts a *title* argument to set the title of the window. + | All other arguments are used to show data. (see :func:`ImageView.setImage() <pyqtgraph.ImageView.setImage>`) + """ + mkQApp() + w = ImageWindow(*args, **kargs) + images.append(w) + w.show() + return w
+show = image ## for backward compatibility + + +def mkQApp(): + if QtGui.QApplication.instance() is None: + global QAPP + QAPP = QtGui.QApplication([]) +
+ +
+
+
+
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/_sources/apireference.txt b/documentation/build/html/_sources/apireference.txt new file mode 100644 index 00000000..ab4ec666 --- /dev/null +++ b/documentation/build/html/_sources/apireference.txt @@ -0,0 +1,11 @@ +API Reference +============= + +Contents: + +.. toctree:: + :maxdepth: 2 + + functions + graphicsItems/index + widgets/index diff --git a/documentation/build/html/_sources/functions.txt b/documentation/build/html/_sources/functions.txt new file mode 100644 index 00000000..3d56a4d9 --- /dev/null +++ b/documentation/build/html/_sources/functions.txt @@ -0,0 +1,53 @@ +Pyqtgraph's Helper Functions +============================ + +Simple Data Display Functions +----------------------------- + +.. autofunction:: pyqtgraph.plot + +.. autofunction:: pyqtgraph.image + + + +Color, Pen, and Brush Functions +------------------------------- + +Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and fill shapes. These classes are highly capable but somewhat awkward to use. Pyqtgraph offers the functions :func:`~pyqtgraph.mkColor`, :func:`~pyqtgraph.mkPen`, and :func:`~pyqtgraph.mkBrush` to simplify the process of creating these classes. In most cases, however, it will be unnecessary to call these functions directly--any function or method that accepts *pen* or *brush* arguments will make use of these functions for you. For example, the following three lines all have the same effect:: + + pg.plot(xdata, ydata, pen='r') + pg.plot(xdata, ydata, pen=pg.mkPen('r')) + pg.plot(xdata, ydata, pen=QPen(QColor(255, 0, 0))) + + +.. autofunction:: pyqtgraph.mkColor + +.. autofunction:: pyqtgraph.mkPen + +.. autofunction:: pyqtgraph.mkBrush + +.. autofunction:: pyqtgraph.hsvColor + +.. autofunction:: pyqtgraph.intColor + +.. autofunction:: pyqtgraph.colorTuple + +.. autofunction:: pyqtgraph.colorStr + + +Data Slicing +------------ + +.. autofunction:: pyqtgraph.affineSlice + + + +SI Unit Conversion Functions +---------------------------- + +.. autofunction:: pyqtgraph.siFormat + +.. autofunction:: pyqtgraph.siScale + +.. autofunction:: pyqtgraph.siEval + diff --git a/documentation/build/html/_sources/graphicsItems/arrowitem.txt b/documentation/build/html/_sources/graphicsItems/arrowitem.txt new file mode 100644 index 00000000..250957a5 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/arrowitem.txt @@ -0,0 +1,8 @@ +ArrowItem +========= + +.. autoclass:: pyqtgraph.ArrowItem + :members: + + .. automethod:: pyqtgraph.ArrowItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/axisitem.txt b/documentation/build/html/_sources/graphicsItems/axisitem.txt new file mode 100644 index 00000000..8f76d130 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/axisitem.txt @@ -0,0 +1,8 @@ +AxisItem +======== + +.. autoclass:: pyqtgraph.AxisItem + :members: + + .. automethod:: pyqtgraph.AxisItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/buttonitem.txt b/documentation/build/html/_sources/graphicsItems/buttonitem.txt new file mode 100644 index 00000000..44469db6 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/buttonitem.txt @@ -0,0 +1,8 @@ +ButtonItem +========== + +.. autoclass:: pyqtgraph.ButtonItem + :members: + + .. automethod:: pyqtgraph.ButtonItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/curvearrow.txt b/documentation/build/html/_sources/graphicsItems/curvearrow.txt new file mode 100644 index 00000000..4c7f11ab --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/curvearrow.txt @@ -0,0 +1,8 @@ +CurveArrow +========== + +.. autoclass:: pyqtgraph.CurveArrow + :members: + + .. automethod:: pyqtgraph.CurveArrow.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/curvepoint.txt b/documentation/build/html/_sources/graphicsItems/curvepoint.txt new file mode 100644 index 00000000..f19791f7 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/curvepoint.txt @@ -0,0 +1,8 @@ +CurvePoint +========== + +.. autoclass:: pyqtgraph.CurvePoint + :members: + + .. automethod:: pyqtgraph.CurvePoint.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/gradienteditoritem.txt b/documentation/build/html/_sources/graphicsItems/gradienteditoritem.txt new file mode 100644 index 00000000..02d40956 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/gradienteditoritem.txt @@ -0,0 +1,8 @@ +GradientEditorItem +================== + +.. autoclass:: pyqtgraph.GradientEditorItem + :members: + + .. automethod:: pyqtgraph.GradientEditorItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/gradientlegend.txt b/documentation/build/html/_sources/graphicsItems/gradientlegend.txt new file mode 100644 index 00000000..f47031c0 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/gradientlegend.txt @@ -0,0 +1,8 @@ +GradientLegend +============== + +.. autoclass:: pyqtgraph.GradientLegend + :members: + + .. automethod:: pyqtgraph.GradientLegend.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/graphicslayout.txt b/documentation/build/html/_sources/graphicsItems/graphicslayout.txt new file mode 100644 index 00000000..f45dfd87 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/graphicslayout.txt @@ -0,0 +1,8 @@ +GraphicsLayout +============== + +.. autoclass:: pyqtgraph.GraphicsLayout + :members: + + .. automethod:: pyqtgraph.GraphicsLayout.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/graphicsobject.txt b/documentation/build/html/_sources/graphicsItems/graphicsobject.txt new file mode 100644 index 00000000..736d941e --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/graphicsobject.txt @@ -0,0 +1,8 @@ +GraphicsObject +============== + +.. autoclass:: pyqtgraph.GraphicsObject + :members: + + .. automethod:: pyqtgraph.GraphicsObject.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/graphicswidget.txt b/documentation/build/html/_sources/graphicsItems/graphicswidget.txt new file mode 100644 index 00000000..7cf23bbe --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/graphicswidget.txt @@ -0,0 +1,8 @@ +GraphicsWidget +============== + +.. autoclass:: pyqtgraph.GraphicsWidget + :members: + + .. automethod:: pyqtgraph.GraphicsWidget.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/griditem.txt b/documentation/build/html/_sources/graphicsItems/griditem.txt new file mode 100644 index 00000000..aa932766 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/griditem.txt @@ -0,0 +1,8 @@ +GridItem +======== + +.. autoclass:: pyqtgraph.GridItem + :members: + + .. automethod:: pyqtgraph.GridItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/histogramlutitem.txt b/documentation/build/html/_sources/graphicsItems/histogramlutitem.txt new file mode 100644 index 00000000..db0e18cb --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/histogramlutitem.txt @@ -0,0 +1,8 @@ +HistogramLUTItem +================ + +.. autoclass:: pyqtgraph.HistogramLUTItem + :members: + + .. automethod:: pyqtgraph.HistogramLUTItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/imageitem.txt b/documentation/build/html/_sources/graphicsItems/imageitem.txt new file mode 100644 index 00000000..49a981dc --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/imageitem.txt @@ -0,0 +1,8 @@ +ImageItem +========= + +.. autoclass:: pyqtgraph.ImageItem + :members: + + .. automethod:: pyqtgraph.ImageItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/index.txt b/documentation/build/html/_sources/graphicsItems/index.txt new file mode 100644 index 00000000..46f5a938 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/index.txt @@ -0,0 +1,37 @@ +Pyqtgraph's Graphics Items +========================== + +Since pyqtgraph relies on Qt's GraphicsView framework, most of its graphics functionality is implemented as QGraphicsItem subclasses. This has two important consequences: 1) virtually anything you want to draw can be easily accomplished using the functionality provided by Qt. 2) Many of pyqtgraph's GraphicsItem classes can be used in any normal QGraphicsScene. + + +Contents: + +.. toctree:: + :maxdepth: 2 + + plotdataitem + plotcurveitem + scatterplotitem + plotitem + imageitem + viewbox + linearregionitem + infiniteline + roi + graphicslayout + axisitem + arrowitem + curvepoint + curvearrow + griditem + scalebar + labelitem + vtickgroup + gradienteditoritem + histogramlutitem + gradientlegend + buttonitem + graphicsobject + graphicswidget + uigraphicsitem + diff --git a/documentation/build/html/_sources/graphicsItems/infiniteline.txt b/documentation/build/html/_sources/graphicsItems/infiniteline.txt new file mode 100644 index 00000000..e95987bc --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/infiniteline.txt @@ -0,0 +1,8 @@ +InfiniteLine +============ + +.. autoclass:: pyqtgraph.InfiniteLine + :members: + + .. automethod:: pyqtgraph.InfiniteLine.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/labelitem.txt b/documentation/build/html/_sources/graphicsItems/labelitem.txt new file mode 100644 index 00000000..ca420d76 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/labelitem.txt @@ -0,0 +1,8 @@ +LabelItem +========= + +.. autoclass:: pyqtgraph.LabelItem + :members: + + .. automethod:: pyqtgraph.LabelItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/linearregionitem.txt b/documentation/build/html/_sources/graphicsItems/linearregionitem.txt new file mode 100644 index 00000000..9bcb534c --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/linearregionitem.txt @@ -0,0 +1,8 @@ +LinearRegionItem +================ + +.. autoclass:: pyqtgraph.LinearRegionItem + :members: + + .. automethod:: pyqtgraph.LinearRegionItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/plotcurveitem.txt b/documentation/build/html/_sources/graphicsItems/plotcurveitem.txt new file mode 100644 index 00000000..f0b2171d --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/plotcurveitem.txt @@ -0,0 +1,8 @@ +PlotCurveItem +============= + +.. autoclass:: pyqtgraph.PlotCurveItem + :members: + + .. automethod:: pyqtgraph.PlotCurveItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/plotdataitem.txt b/documentation/build/html/_sources/graphicsItems/plotdataitem.txt new file mode 100644 index 00000000..275084e9 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/plotdataitem.txt @@ -0,0 +1,8 @@ +PlotDataItem +============ + +.. autoclass:: pyqtgraph.PlotDataItem + :members: + + .. automethod:: pyqtgraph.PlotDataItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/plotitem.txt b/documentation/build/html/_sources/graphicsItems/plotitem.txt new file mode 100644 index 00000000..cbf5f9f4 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/plotitem.txt @@ -0,0 +1,7 @@ +PlotItem +======== + +.. autoclass:: pyqtgraph.PlotItem + :members: + + .. automethod:: pyqtgraph.PlotItem.__init__ diff --git a/documentation/build/html/_sources/graphicsItems/roi.txt b/documentation/build/html/_sources/graphicsItems/roi.txt new file mode 100644 index 00000000..22945ade --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/roi.txt @@ -0,0 +1,8 @@ +ROI +=== + +.. autoclass:: pyqtgraph.ROI + :members: + + .. automethod:: pyqtgraph.ROI.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/scalebar.txt b/documentation/build/html/_sources/graphicsItems/scalebar.txt new file mode 100644 index 00000000..2ab33967 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/scalebar.txt @@ -0,0 +1,8 @@ +ScaleBar +======== + +.. autoclass:: pyqtgraph.ScaleBar + :members: + + .. automethod:: pyqtgraph.ScaleBar.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/scatterplotitem.txt b/documentation/build/html/_sources/graphicsItems/scatterplotitem.txt new file mode 100644 index 00000000..be2c874b --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/scatterplotitem.txt @@ -0,0 +1,8 @@ +ScatterPlotItem +=============== + +.. autoclass:: pyqtgraph.ScatterPlotItem + :members: + + .. automethod:: pyqtgraph.ScatterPlotItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/uigraphicsitem.txt b/documentation/build/html/_sources/graphicsItems/uigraphicsitem.txt new file mode 100644 index 00000000..4f0b9933 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/uigraphicsitem.txt @@ -0,0 +1,8 @@ +UIGraphicsItem +============== + +.. autoclass:: pyqtgraph.UIGraphicsItem + :members: + + .. automethod:: pyqtgraph.UIGraphicsItem.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/viewbox.txt b/documentation/build/html/_sources/graphicsItems/viewbox.txt new file mode 100644 index 00000000..3593d295 --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/viewbox.txt @@ -0,0 +1,8 @@ +ViewBox +======= + +.. autoclass:: pyqtgraph.ViewBox + :members: + + .. automethod:: pyqtgraph.ViewBox.__init__ + diff --git a/documentation/build/html/_sources/graphicsItems/vtickgroup.txt b/documentation/build/html/_sources/graphicsItems/vtickgroup.txt new file mode 100644 index 00000000..342705de --- /dev/null +++ b/documentation/build/html/_sources/graphicsItems/vtickgroup.txt @@ -0,0 +1,8 @@ +VTickGroup +========== + +.. autoclass:: pyqtgraph.VTickGroup + :members: + + .. automethod:: pyqtgraph.VTickGroup.__init__ + diff --git a/documentation/build/html/_sources/graphicswindow.txt b/documentation/build/html/_sources/graphicswindow.txt new file mode 100644 index 00000000..3d5641c3 --- /dev/null +++ b/documentation/build/html/_sources/graphicswindow.txt @@ -0,0 +1,8 @@ +Basic display widgets +===================== + + - GraphicsWindow + - GraphicsView + - GraphicsLayoutItem + - ViewBox + diff --git a/documentation/build/html/_sources/how_to_use.txt b/documentation/build/html/_sources/how_to_use.txt new file mode 100644 index 00000000..74e901d0 --- /dev/null +++ b/documentation/build/html/_sources/how_to_use.txt @@ -0,0 +1,47 @@ +How to use pyqtgraph +==================== + +There are a few suggested ways to use pyqtgraph: + +* From the interactive shell (python -i, ipython, etc) +* Displaying pop-up windows from an application +* Embedding widgets in a PyQt application + + + +Command-line use +---------------- + +Pyqtgraph makes it very easy to visualize data from the command line. Observe:: + + import pyqtgraph as pg + pg.plot(data) # data can be a list of values or a numpy array + +The example above would open a window displaying a line plot of the data given. I don't think it could reasonably be any simpler than that. The call to pg.plot returns a handle to the plot widget that is created, allowing more data to be added to the same window. + +Further examples:: + + pw = pg.plot(xVals, yVals, pen='r') # plot x vs y in red + pw.plot(xVals, yVals2, pen='b') + + win = pg.GraphicsWindow() # Automatically generates grids with multiple items + win.addPlot(data1, row=0, col=0) + win.addPlot(data2, row=0, col=1) + win.addPlot(data3, row=1, col=0, colspan=2) + + pg.show(imageData) # imageData must be a numpy array with 2 to 4 dimensions + +We're only scratching the surface here--these functions accept many different data formats and options for customizing the appearance of your data. + + +Displaying windows from within an application +--------------------------------------------- + +While I consider this approach somewhat lazy, it is often the case that 'lazy' is indistinguishable from 'highly efficient'. The approach here is simply to use the very same functions that would be used on the command line, but from within an existing application. I often use this when I simply want to get a immediate feedback about the state of data in my application without taking the time to build a user interface for it. + + +Embedding widgets inside PyQt applications +------------------------------------------ + +For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: PlotWidget, ImageView, GraphicsView, GraphicsLayoutWidget. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality. + diff --git a/documentation/build/html/_sources/images.txt b/documentation/build/html/_sources/images.txt new file mode 100644 index 00000000..461a9cb7 --- /dev/null +++ b/documentation/build/html/_sources/images.txt @@ -0,0 +1,26 @@ +Displaying images and video +=========================== + +Pyqtgraph displays 2D numpy arrays as images and provides tools for determining how to translate between the numpy data type and RGB values on the screen. If you want to display data from common image and video file formats, you will need to load the data first using another library (PIL works well for images and built-in numpy conversion). + +The easiest way to display 2D or 3D data is using the :func:`pyqtgraph.image` function:: + + import pyqtgraph as pg + pg.image(imageData) + +This function will accept any floating-point or integer data types and displays a single :class:`~pyqtgraph.ImageView` widget containing your data. This widget includes controls for determining how the image data will be converted to 32-bit RGBa values. Conversion happens in two steps (both are optional): + +1. Scale and offset the data (by selecting the dark/light levels on the displayed histogram) +2. Convert the data to color using a lookup table (determined by the colors shown in the gradient editor) + +If the data is 3D (time, x, y), then a time axis will be shown with a slider that can set the currently displayed frame. (if the axes in your data are ordered differently, use numpy.transpose to rearrange them) + +There are a few other methods for displaying images as well: + +* The :class:`~pyqtgraph.ImageView` class can also be instantiated directly and embedded in Qt applications. +* Instances of :class:`~pyqtgraph.ImageItem` can be used inside a GraphicsView. +* For higher performance, use :class:`~pyqtgraph.RawImageWidget`. + +Any of these classes are acceptable for displaying video by calling setImage() to display a new frame. To increase performance, the image processing system uses scipy.weave to produce compiled libraries. If your computer has a compiler available, weave will automatically attempt to build the libraries it needs on demand. If this fails, then the slower pure-python methods will be used instead. + +For more information, see the classes listed above and the 'VideoSpeedTest', 'ImageItem', 'ImageView', and 'HistogramLUT' :ref:`examples`. \ No newline at end of file diff --git a/documentation/build/html/_sources/index.txt b/documentation/build/html/_sources/index.txt new file mode 100644 index 00000000..aa6753ef --- /dev/null +++ b/documentation/build/html/_sources/index.txt @@ -0,0 +1,32 @@ +.. pyqtgraph documentation master file, created by + sphinx-quickstart on Fri Nov 18 19:33:12 2011. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to the documentation for pyqtgraph 1.8 +============================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + introduction + how_to_use + plotting + images + style + region_of_interest + graphicswindow + parametertree + internals + apireference + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/documentation/build/html/_sources/introduction.txt b/documentation/build/html/_sources/introduction.txt new file mode 100644 index 00000000..c5c1dfab --- /dev/null +++ b/documentation/build/html/_sources/introduction.txt @@ -0,0 +1,51 @@ +Introduction +============ + + + +What is pyqtgraph? +------------------ + +Pyqtgraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer). + +Pyqtgraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt's GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort. + +It is known to run on Linux, Windows, and OSX + + +What can it do? +--------------- + +Amongst the core features of pyqtgraph are: + +* Basic data visualization primitives: Images, line and scatter plots +* Fast enough for realtime update of video/plot data +* Interactive scaling/panning, averaging, FFTs, SVG/PNG export +* Widgets for marking/selecting plot regions +* Widgets for marking/selecting image region-of-interest and automatically slicing multi-dimensional image data +* Framework for building customized image region-of-interest widgets +* Docking system that replaces/complements Qt's dock system to allow more complex (and more predictable) docking arrangements +* ParameterTree widget for rapid prototyping of dynamic interfaces (Similar to the property trees in Qt Designer and many other applications) + + +.. _examples: + +Examples +-------- + +Pyqtgraph includes an extensive set of examples that can be accessed by running:: + + import pyqtgraph.examples + pyqtgraph.examples.run() + +This will start a launcher with a list of available examples. Select an item from the list to view its source code and double-click an item to run the example. + + +How does it compare to... +------------------------- + +* matplotlib: For plotting and making publication-quality graphics, matplotlib is far more mature than pyqtgraph. However, matplotlib is also much slower and not suitable for applications requiring realtime update of plots/video or rapid interactivity. It also does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. + +* pyqwt5: pyqwt is generally more mature than pyqtgraph for plotting and is about as fast. The major differences are 1) pyqtgraph is written in pure python, so it is somewhat more portable than pyqwt, which often lags behind pyqt in development (and can be a pain to install on some platforms) and 2) like matplotlib, pyqwt does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. + +(My experience with these libraries is somewhat outdated; please correct me if I am wrong here) diff --git a/documentation/build/html/_sources/parametertree.txt b/documentation/build/html/_sources/parametertree.txt new file mode 100644 index 00000000..de699492 --- /dev/null +++ b/documentation/build/html/_sources/parametertree.txt @@ -0,0 +1,7 @@ +Rapid GUI prototyping +===================== + + - parametertree + - dockarea + - flowchart + - canvas diff --git a/documentation/build/html/_sources/plotting.txt b/documentation/build/html/_sources/plotting.txt new file mode 100644 index 00000000..ee9ed6dc --- /dev/null +++ b/documentation/build/html/_sources/plotting.txt @@ -0,0 +1,73 @@ +Plotting in pyqtgraph +===================== + +There are a few basic ways to plot data in pyqtgraph: + +================================================================ ================================================== +:func:`pyqtgraph.plot` Create a new plot window showing your data +:func:`PlotWidget.plot() ` Add a new set of data to an existing plot widget +:func:`PlotItem.plot() ` Add a new set of data to an existing plot widget +:func:`GraphicsWindow.addPlot() ` Add a new plot to a grid of plots +================================================================ ================================================== + +All of these will accept the same basic arguments which control how the plot data is interpreted and displayed: + +* x - Optional X data; if not specified, then a range of integers will be generated automatically. +* y - Y data. +* pen - The pen to use when drawing plot lines, or None to disable lines. +* symbol - A string describing the shape of symbols to use for each point. Optionally, this may also be a sequence of strings with a different symbol for each point. +* symbolPen - The pen (or sequence of pens) to use when drawing the symbol outline. +* symbolBrush - The brush (or sequence of brushes) to use when filling the symbol. +* fillLevel - Fills the area under the plot curve to this Y-value. +* brush - The brush to use when filling under the curve. + +See the 'plotting' :ref:`example ` for a demonstration of these arguments. + +All of the above functions also return handles to the objects that are created, allowing the plots and data to be further modified. + +Organization of Plotting Classes +-------------------------------- + +There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. Pyqtgraph is based heavily on Qt's GraphicsView framework--if you are not already familiar with this, it's worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView. + +* Data Classes (all subclasses of QGraphicsItem) + * PlotCurveItem - Displays a plot line given x,y data + * ScatterPlotItem - Displays points given x,y data + * :class:`PlotDataItem ` - Combines PlotCurveItem and ScatterPlotItem. The plotting functions discussed above create objects of this type. +* Container Classes (subclasses of QGraphicsItem; contain other QGraphicsItem objects and must be viewed from within a GraphicsView) + * PlotItem - Contains a ViewBox for displaying data as well as AxisItems and labels for displaying the axes and title. This is a QGraphicsItem subclass and thus may only be used from within a GraphicsView + * GraphicsLayoutItem - QGraphicsItem subclass which displays a grid of items. This is used to display multiple PlotItems together. + * ViewBox - A QGraphicsItem subclass for displaying data. The user may scale/pan the contents of a ViewBox using the mouse. Typically all PlotData/PlotCurve/ScatterPlotItems are displayed from within a ViewBox. + * AxisItem - Displays axis values, ticks, and labels. Most commonly used with PlotItem. +* Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs) + * PlotWidget - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. + * GraphicsLayoutWidget - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget. + +.. image:: images/plottingClasses.png + + +Examples +-------- + +See the 'plotting' and 'PlotWidget' :ref:`examples included with pyqtgraph ` for more information. + +Show x,y data as scatter plot:: + + import pyqtgraph as pg + import numpy as np + x = np.random.normal(size=1000) + y = np.random.normal(size=1000) + pg.plot(x, y, pen=None, symbol='o') ## setting pen=None disables line drawing + +Create/show a plot widget, display three data curves:: + + import pyqtgraph as pg + import numpy as np + x = np.arange(1000) + y = np.random.normal(size=(3, 1000)) + plotWidget = pg.plot(title="Three plot curves") + for i in range(3): + plotWidget.plot(x, y[i], pen=(i,3)) ## setting pen=(i,3) automaticaly creates three different-colored pens + + + diff --git a/documentation/build/html/_sources/region_of_interest.txt b/documentation/build/html/_sources/region_of_interest.txt new file mode 100644 index 00000000..24799cb7 --- /dev/null +++ b/documentation/build/html/_sources/region_of_interest.txt @@ -0,0 +1,19 @@ +Region-of-interest controls +=========================== + +Slicing Multidimensional Data +----------------------------- + +Linear Selection and Marking +---------------------------- + +2D Selection and Marking +------------------------ + + + + +- translate / rotate / scale +- highly configurable control handles +- automated data slicing +- linearregion, infiniteline diff --git a/documentation/build/html/_sources/style.txt b/documentation/build/html/_sources/style.txt new file mode 100644 index 00000000..fc172420 --- /dev/null +++ b/documentation/build/html/_sources/style.txt @@ -0,0 +1,17 @@ +Line, Fill, and Color +===================== + +Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color. + +For these function arguments, the following values may be used: + +* single-character string representing color (b, g, r, c, m, y, k, w) +* (r, g, b) or (r, g, b, a) tuple +* single greyscale value (0.0 - 1.0) +* (index, maximum) tuple for automatically iterating through colors (see functions.intColor) +* QColor +* QPen / QBrush where appropriate + +Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes. + +Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt's QColor class diff --git a/documentation/build/html/_sources/widgets/checktable.txt b/documentation/build/html/_sources/widgets/checktable.txt new file mode 100644 index 00000000..5301a4e9 --- /dev/null +++ b/documentation/build/html/_sources/widgets/checktable.txt @@ -0,0 +1,8 @@ +CheckTable +========== + +.. autoclass:: pyqtgraph.CheckTable + :members: + + .. automethod:: pyqtgraph.CheckTable.__init__ + diff --git a/documentation/build/html/_sources/widgets/colorbutton.txt b/documentation/build/html/_sources/widgets/colorbutton.txt new file mode 100644 index 00000000..690239d8 --- /dev/null +++ b/documentation/build/html/_sources/widgets/colorbutton.txt @@ -0,0 +1,8 @@ +ColorButton +=========== + +.. autoclass:: pyqtgraph.ColorButton + :members: + + .. automethod:: pyqtgraph.ColorButton.__init__ + diff --git a/documentation/build/html/_sources/widgets/datatreewidget.txt b/documentation/build/html/_sources/widgets/datatreewidget.txt new file mode 100644 index 00000000..f6bbdbaf --- /dev/null +++ b/documentation/build/html/_sources/widgets/datatreewidget.txt @@ -0,0 +1,8 @@ +DataTreeWidget +============== + +.. autoclass:: pyqtgraph.DataTreeWidget + :members: + + .. automethod:: pyqtgraph.DataTreeWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/dockarea.txt b/documentation/build/html/_sources/widgets/dockarea.txt new file mode 100644 index 00000000..09a6acca --- /dev/null +++ b/documentation/build/html/_sources/widgets/dockarea.txt @@ -0,0 +1,5 @@ +dockarea module +=============== + +.. automodule:: pyqtgraph.dockarea + :members: diff --git a/documentation/build/html/_sources/widgets/filedialog.txt b/documentation/build/html/_sources/widgets/filedialog.txt new file mode 100644 index 00000000..bf2f9c07 --- /dev/null +++ b/documentation/build/html/_sources/widgets/filedialog.txt @@ -0,0 +1,8 @@ +FileDialog +========== + +.. autoclass:: pyqtgraph.FileDialog + :members: + + .. automethod:: pyqtgraph.FileDialog.__init__ + diff --git a/documentation/build/html/_sources/widgets/gradientwidget.txt b/documentation/build/html/_sources/widgets/gradientwidget.txt new file mode 100644 index 00000000..a2587503 --- /dev/null +++ b/documentation/build/html/_sources/widgets/gradientwidget.txt @@ -0,0 +1,8 @@ +GradientWidget +============== + +.. autoclass:: pyqtgraph.GradientWidget + :members: + + .. automethod:: pyqtgraph.GradientWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/graphicslayoutwidget.txt b/documentation/build/html/_sources/widgets/graphicslayoutwidget.txt new file mode 100644 index 00000000..5f885f07 --- /dev/null +++ b/documentation/build/html/_sources/widgets/graphicslayoutwidget.txt @@ -0,0 +1,8 @@ +GraphicsLayoutWidget +==================== + +.. autoclass:: pyqtgraph.GraphicsLayoutWidget + :members: + + .. automethod:: pyqtgraph.GraphicsLayoutWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/graphicsview.txt b/documentation/build/html/_sources/widgets/graphicsview.txt new file mode 100644 index 00000000..ac7ae3bf --- /dev/null +++ b/documentation/build/html/_sources/widgets/graphicsview.txt @@ -0,0 +1,8 @@ +GraphicsView +============ + +.. autoclass:: pyqtgraph.GraphicsView + :members: + + .. automethod:: pyqtgraph.GraphicsView.__init__ + diff --git a/documentation/build/html/_sources/widgets/histogramlutwidget.txt b/documentation/build/html/_sources/widgets/histogramlutwidget.txt new file mode 100644 index 00000000..9d8f3b20 --- /dev/null +++ b/documentation/build/html/_sources/widgets/histogramlutwidget.txt @@ -0,0 +1,8 @@ +HistogramLUTWidget +================== + +.. autoclass:: pyqtgraph.HistogramLUTWidget + :members: + + .. automethod:: pyqtgraph.HistogramLUTWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/imageview.txt b/documentation/build/html/_sources/widgets/imageview.txt new file mode 100644 index 00000000..1eadabbf --- /dev/null +++ b/documentation/build/html/_sources/widgets/imageview.txt @@ -0,0 +1,8 @@ +ImageView +========= + +.. autoclass:: pyqtgraph.ImageView + :members: + + .. automethod:: pyqtgraph.ImageView.__init__ + diff --git a/documentation/build/html/_sources/widgets/index.txt b/documentation/build/html/_sources/widgets/index.txt new file mode 100644 index 00000000..bce5b070 --- /dev/null +++ b/documentation/build/html/_sources/widgets/index.txt @@ -0,0 +1,31 @@ +Pyqtgraph's Widgets +=================== + +Pyqtgraph provides several QWidget subclasses which are useful for building user interfaces. These widgets can generally be used in any Qt application and provide functionality that is frequently useful in science and engineering applications. + +Contents: + +.. toctree:: + :maxdepth: 2 + + plotwidget + imageview + datatreewidget + checktable + tablewidget + gradientwidget + colorbutton + graphicslayoutwidget + dockarea + parametertree + histogramlutwidget + progressdialog + spinbox + filedialog + graphicsview + joystickbutton + multiplotwidget + treewidget + verticallabel + rawimagewidget + diff --git a/documentation/build/html/_sources/widgets/joystickbutton.txt b/documentation/build/html/_sources/widgets/joystickbutton.txt new file mode 100644 index 00000000..4d21e16f --- /dev/null +++ b/documentation/build/html/_sources/widgets/joystickbutton.txt @@ -0,0 +1,8 @@ +JoystickButton +============== + +.. autoclass:: pyqtgraph.JoystickButton + :members: + + .. automethod:: pyqtgraph.JoystickButton.__init__ + diff --git a/documentation/build/html/_sources/widgets/multiplotwidget.txt b/documentation/build/html/_sources/widgets/multiplotwidget.txt new file mode 100644 index 00000000..46986db0 --- /dev/null +++ b/documentation/build/html/_sources/widgets/multiplotwidget.txt @@ -0,0 +1,8 @@ +MultiPlotWidget +=============== + +.. autoclass:: pyqtgraph.MultiPlotWidget + :members: + + .. automethod:: pyqtgraph.MultiPlotWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/parametertree.txt b/documentation/build/html/_sources/widgets/parametertree.txt new file mode 100644 index 00000000..565b930b --- /dev/null +++ b/documentation/build/html/_sources/widgets/parametertree.txt @@ -0,0 +1,5 @@ +parametertree module +==================== + +.. automodule:: pyqtgraph.parametertree + :members: diff --git a/documentation/build/html/_sources/widgets/plotwidget.txt b/documentation/build/html/_sources/widgets/plotwidget.txt new file mode 100644 index 00000000..cbded80d --- /dev/null +++ b/documentation/build/html/_sources/widgets/plotwidget.txt @@ -0,0 +1,8 @@ +PlotWidget +========== + +.. autoclass:: pyqtgraph.PlotWidget + :members: + + .. automethod:: pyqtgraph.PlotWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/progressdialog.txt b/documentation/build/html/_sources/widgets/progressdialog.txt new file mode 100644 index 00000000..fff04cb3 --- /dev/null +++ b/documentation/build/html/_sources/widgets/progressdialog.txt @@ -0,0 +1,8 @@ +ProgressDialog +============== + +.. autoclass:: pyqtgraph.ProgressDialog + :members: + + .. automethod:: pyqtgraph.ProgressDialog.__init__ + diff --git a/documentation/build/html/_sources/widgets/rawimagewidget.txt b/documentation/build/html/_sources/widgets/rawimagewidget.txt new file mode 100644 index 00000000..29fda791 --- /dev/null +++ b/documentation/build/html/_sources/widgets/rawimagewidget.txt @@ -0,0 +1,8 @@ +RawImageWidget +============== + +.. autoclass:: pyqtgraph.RawImageWidget + :members: + + .. automethod:: pyqtgraph.RawImageWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/spinbox.txt b/documentation/build/html/_sources/widgets/spinbox.txt new file mode 100644 index 00000000..33da1f4c --- /dev/null +++ b/documentation/build/html/_sources/widgets/spinbox.txt @@ -0,0 +1,8 @@ +SpinBox +======= + +.. autoclass:: pyqtgraph.SpinBox + :members: + + .. automethod:: pyqtgraph.SpinBox.__init__ + diff --git a/documentation/build/html/_sources/widgets/tablewidget.txt b/documentation/build/html/_sources/widgets/tablewidget.txt new file mode 100644 index 00000000..283b540b --- /dev/null +++ b/documentation/build/html/_sources/widgets/tablewidget.txt @@ -0,0 +1,8 @@ +TableWidget +=========== + +.. autoclass:: pyqtgraph.TableWidget + :members: + + .. automethod:: pyqtgraph.TableWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/treewidget.txt b/documentation/build/html/_sources/widgets/treewidget.txt new file mode 100644 index 00000000..00f9fa28 --- /dev/null +++ b/documentation/build/html/_sources/widgets/treewidget.txt @@ -0,0 +1,8 @@ +TreeWidget +========== + +.. autoclass:: pyqtgraph.TreeWidget + :members: + + .. automethod:: pyqtgraph.TreeWidget.__init__ + diff --git a/documentation/build/html/_sources/widgets/verticallabel.txt b/documentation/build/html/_sources/widgets/verticallabel.txt new file mode 100644 index 00000000..4f627437 --- /dev/null +++ b/documentation/build/html/_sources/widgets/verticallabel.txt @@ -0,0 +1,8 @@ +VerticalLabel +============= + +.. autoclass:: pyqtgraph.VerticalLabel + :members: + + .. automethod:: pyqtgraph.VerticalLabel.__init__ + diff --git a/documentation/build/html/_static/basic.css b/documentation/build/html/_static/basic.css new file mode 100644 index 00000000..69f30d4f --- /dev/null +++ b/documentation/build/html/_static/basic.css @@ -0,0 +1,509 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +img { + border: 0; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +.align-left { + text-align: left; +} + +.align-center { + clear: both; + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlighted { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.refcount { + color: #060; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} diff --git a/documentation/build/html/_static/default.css b/documentation/build/html/_static/default.css new file mode 100644 index 00000000..b30cb790 --- /dev/null +++ b/documentation/build/html/_static/default.css @@ -0,0 +1,255 @@ +/* + * default.css_t + * ~~~~~~~~~~~~~ + * + * Sphinx stylesheet -- default theme. + * + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: sans-serif; + font-size: 100%; + background-color: #11303d; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + background-color: #1c4e63; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 230px; +} + +div.body { + background-color: #ffffff; + color: #000000; + padding: 0 20px 30px 20px; +} + +div.footer { + color: #ffffff; + width: 100%; + padding: 9px 0 9px 0; + text-align: center; + font-size: 75%; +} + +div.footer a { + color: #ffffff; + text-decoration: underline; +} + +div.related { + background-color: #133f52; + line-height: 30px; + color: #ffffff; +} + +div.related a { + color: #ffffff; +} + +div.sphinxsidebar { +} + +div.sphinxsidebar h3 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.4em; + font-weight: normal; + margin: 0; + padding: 0; +} + +div.sphinxsidebar h3 a { + color: #ffffff; +} + +div.sphinxsidebar h4 { + font-family: 'Trebuchet MS', sans-serif; + color: #ffffff; + font-size: 1.3em; + font-weight: normal; + margin: 5px 0 0 0; + padding: 0; +} + +div.sphinxsidebar p { + color: #ffffff; +} + +div.sphinxsidebar p.topless { + margin: 5px 10px 10px 10px; +} + +div.sphinxsidebar ul { + margin: 10px; + padding: 0; + color: #ffffff; +} + +div.sphinxsidebar a { + color: #98dbcc; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + + +/* -- hyperlink styles ------------------------------------------------------ */ + +a { + color: #355f7c; + text-decoration: none; +} + +a:visited { + color: #355f7c; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + + + +/* -- body styles ----------------------------------------------------------- */ + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Trebuchet MS', sans-serif; + background-color: #f2f2f2; + font-weight: normal; + color: #20435c; + border-bottom: 1px solid #ccc; + margin: 20px -20px 10px -20px; + padding: 3px 0 3px 10px; +} + +div.body h1 { margin-top: 0; font-size: 200%; } +div.body h2 { font-size: 160%; } +div.body h3 { font-size: 140%; } +div.body h4 { font-size: 120%; } +div.body h5 { font-size: 110%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #c60f0f; + font-size: 0.8em; + padding: 0 4px 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + background-color: #c60f0f; + color: white; +} + +div.body p, div.body dd, div.body li { + text-align: justify; + line-height: 130%; +} + +div.admonition p.admonition-title + p { + display: inline; +} + +div.admonition p { + margin-bottom: 5px; +} + +div.admonition pre { + margin-bottom: 5px; +} + +div.admonition ul, div.admonition ol { + margin-bottom: 5px; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre { + padding: 5px; + background-color: #eeffcc; + color: #333333; + line-height: 120%; + border: 1px solid #ac9; + border-left: none; + border-right: none; +} + +tt { + background-color: #ecf0f3; + padding: 0 1px 0 1px; + font-size: 0.95em; +} + +th { + background-color: #ede; +} + +.warning tt { + background: #efc2c2; +} + +.note tt { + background: #d6d6d6; +} + +.viewcode-back { + font-family: sans-serif; +} + +div.viewcode-block:target { + background-color: #f4debf; + border-top: 1px solid #ac9; + border-bottom: 1px solid #ac9; +} \ No newline at end of file diff --git a/documentation/build/html/_static/doctools.js b/documentation/build/html/_static/doctools.js new file mode 100644 index 00000000..eeea95ea --- /dev/null +++ b/documentation/build/html/_static/doctools.js @@ -0,0 +1,247 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Sphinx JavaScript utilties for all documentation. + * + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + +/** + * make the code below compatible with browsers without + * an installed firebug like debugger +if (!window.console || !console.firebug) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", + "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", + "profile", "profileEnd"]; + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +} + */ + +/** + * small helper function to urldecode strings + */ +jQuery.urldecode = function(x) { + return decodeURIComponent(x).replace(/\+/g, ' '); +} + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s == 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * small function to check if an array contains + * a given item. + */ +jQuery.contains = function(arr, item) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] == item) + return true; + } + return false; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node) { + if (node.nodeType == 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { + var span = document.createElement("span"); + span.className = className; + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this); + }); + } + } + return this.each(function() { + highlight(this); + }); +}; + +/** + * Small JavaScript module for the documentation. + */ +var Documentation = { + + init : function() { + this.fixFirefoxAnchorBug(); + this.highlightSearchWords(); + this.initIndexTable(); + }, + + /** + * i18n support + */ + TRANSLATIONS : {}, + PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, + LOCALE : 'unknown', + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext : function(string) { + var translated = Documentation.TRANSLATIONS[string]; + if (typeof translated == 'undefined') + return string; + return (typeof translated == 'string') ? translated : translated[0]; + }, + + ngettext : function(singular, plural, n) { + var translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated == 'undefined') + return (n == 1) ? singular : plural; + return translated[Documentation.PLURALEXPR(n)]; + }, + + addTranslations : function(catalog) { + for (var key in catalog.messages) + this.TRANSLATIONS[key] = catalog.messages[key]; + this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); + this.LOCALE = catalog.locale; + }, + + /** + * add context elements like header anchor links + */ + addContextElements : function() { + $('div[id] > :header:first').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this headline')). + appendTo(this); + }); + $('dt[id]').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this definition')). + appendTo(this); + }); + }, + + /** + * workaround a firefox stupidity + */ + fixFirefoxAnchorBug : function() { + if (document.location.hash && $.browser.mozilla) + window.setTimeout(function() { + document.location.href += ''; + }, 10); + }, + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords : function() { + var params = $.getQueryParameters(); + var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + if (terms.length) { + var body = $('div.body'); + window.setTimeout(function() { + $.each(terms, function() { + body.highlightText(this.toLowerCase(), 'highlighted'); + }); + }, 10); + $('') + .appendTo($('.sidebar .this-page-menu')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('tr.cg-' + idnum).toggle(); + if (src.substr(-9) == 'minus.png') + $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); + else + $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); + }).css('display', ''); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { + togglers.click(); + } + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('.sidebar .this-page-menu li.highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + }, + + /** + * make the url absolute + */ + makeURL : function(relativeURL) { + return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + }, + + /** + * get the current relative url + */ + getCurrentURL : function() { + var path = document.location.pathname; + var parts = path.split(/\//); + $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { + if (this == '..') + parts.pop(); + }); + var url = parts.join('/'); + return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + } +}; + +// quick alias for translations +_ = Documentation.gettext; + +$(document).ready(function() { + Documentation.init(); +}); diff --git a/documentation/build/html/_static/file.png b/documentation/build/html/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..d18082e397e7e54f20721af768c4c2983258f1b4 GIT binary patch literal 392 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP$HyOL$D9)yc9|lc|nKf<9@eUiWd>3GuTC!a5vdfWYEazjncPj5ZQX%+1 zt8B*4=d)!cdDz4wr^#OMYfqGz$1LDFF>|#>*O?AGil(WEs?wLLy{Gj2J_@opDm%`dlax3yA*@*N$G&*ukFv>P8+2CBWO(qz zD0k1@kN>hhb1_6`&wrCswzINE(evt-5C1B^STi2@PmdKI;Vst0PQB6!2kdN literal 0 HcmV?d00001 diff --git a/documentation/build/html/_static/jquery.js b/documentation/build/html/_static/jquery.js new file mode 100644 index 00000000..5c99a8d4 --- /dev/null +++ b/documentation/build/html/_static/jquery.js @@ -0,0 +1,8176 @@ +/*! + * jQuery JavaScript Library v1.5 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Mon Jan 31 08:31:29 2011 -0500 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Check for digits + rdigit = /\d/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // Has the ready events already been bound? + readyBound = false, + + // The deferred used on DOM ready + readyList, + + // Promise methods + promiseMethods = "then done fail isResolved isRejected promise".split( " " ), + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = "body"; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = (context ? context.ownerDocument || context : document); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return (context || rootjQuery).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if (selector.selector !== undefined) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.5", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + (this.selector ? " " : "") + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.done( fn ); + + return this; + }, + + eq: function( i ) { + return i === -1 ? + this.slice( i ) : + this.slice( i, +i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + // A third-party is pushing the ready event forwards + if ( wait === true ) { + jQuery.readyWait--; + } + + // Make sure that the DOM is not already loaded + if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).unbind( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyBound ) { + return; + } + + readyBound = true; + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent("onreadystatechange", DOMContentLoaded); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + // A crude way of determining if an object is a window + isWindow: function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }, + + isNaN: function( obj ) { + return obj == null || !rdigit.test( obj ) || isNaN( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw msg; + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test(data.replace(rvalidescape, "@") + .replace(rvalidtokens, "]") + .replace(rvalidbraces, "")) ) { + + // Try to use the native JSON parser first + return window.JSON && window.JSON.parse ? + window.JSON.parse( data ) : + (new Function("return " + data))(); + + } else { + jQuery.error( "Invalid JSON: " + data ); + } + }, + + // Cross-browser xml parsing + // (xml & tmp used internally) + parseXML: function( data , xml , tmp ) { + + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + + tmp = xml.documentElement; + + if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) { + jQuery.error( "Invalid XML: " + data ); + } + + return xml; + }, + + noop: function() {}, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && rnotwhite.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + + if ( jQuery.support.scriptEval() ) { + script.appendChild( document.createTextNode( data ) ); + } else { + script.text = data; + } + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction(object); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // The extra typeof function check is to prevent crashes + // in Safari 2 (See: #3039) + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type(array); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var ret = [], value; + + // Go through the array, translating each of the items to their + // new value (or values). + for ( var i = 0, length = elems.length; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + proxy: function( fn, proxy, thisObject ) { + if ( arguments.length === 2 ) { + if ( typeof proxy === "string" ) { + thisObject = fn; + fn = thisObject[ proxy ]; + proxy = undefined; + + } else if ( proxy && !jQuery.isFunction( proxy ) ) { + thisObject = proxy; + proxy = undefined; + } + } + + if ( !proxy && fn ) { + proxy = function() { + return fn.apply( thisObject || this, arguments ); + }; + } + + // Set the guid of unique handler to the same of original handler, so it can be removed + if ( fn ) { + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + } + + // So proxy can be declared as an argument + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can be optionally by executed if its a function + access: function( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + jQuery.access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; + }, + + now: function() { + return (new Date()).getTime(); + }, + + // Create a simple deferred (one callbacks list) + _Deferred: function() { + var // callbacks list + callbacks = [], + // stored [ context , args ] + fired, + // to avoid firing when already doing so + firing, + // flag to know if the deferred has been cancelled + cancelled, + // the deferred itself + deferred = { + + // done( f1, f2, ...) + done: function() { + if ( !cancelled ) { + var args = arguments, + i, + length, + elem, + type, + _fired; + if ( fired ) { + _fired = fired; + fired = 0; + } + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + deferred.done.apply( deferred, elem ); + } else if ( type === "function" ) { + callbacks.push( elem ); + } + } + if ( _fired ) { + deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); + } + } + return this; + }, + + // resolve with given context and args + resolveWith: function( context, args ) { + if ( !cancelled && !fired && !firing ) { + firing = 1; + try { + while( callbacks[ 0 ] ) { + callbacks.shift().apply( context, args ); + } + } + finally { + fired = [ context, args ]; + firing = 0; + } + } + return this; + }, + + // resolve with this as context and given arguments + resolve: function() { + deferred.resolveWith( jQuery.isFunction( this.promise ) ? this.promise() : this, arguments ); + return this; + }, + + // Has this deferred been resolved? + isResolved: function() { + return !!( firing || fired ); + }, + + // Cancel + cancel: function() { + cancelled = 1; + callbacks = []; + return this; + } + }; + + return deferred; + }, + + // Full fledged deferred (two callbacks list) + Deferred: function( func ) { + var deferred = jQuery._Deferred(), + failDeferred = jQuery._Deferred(), + promise; + // Add errorDeferred methods, then and promise + jQuery.extend( deferred, { + then: function( doneCallbacks, failCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ); + return this; + }, + fail: failDeferred.done, + rejectWith: failDeferred.resolveWith, + reject: failDeferred.resolve, + isRejected: failDeferred.isResolved, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj , i /* internal */ ) { + if ( obj == null ) { + if ( promise ) { + return promise; + } + promise = obj = {}; + } + i = promiseMethods.length; + while( i-- ) { + obj[ promiseMethods[ i ] ] = deferred[ promiseMethods[ i ] ]; + } + return obj; + } + } ); + // Make sure only one callback list will be used + deferred.then( failDeferred.cancel, deferred.cancel ); + // Unexpose cancel + delete deferred.cancel; + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + return deferred; + }, + + // Deferred helper + when: function( object ) { + var args = arguments, + length = args.length, + deferred = length <= 1 && object && jQuery.isFunction( object.promise ) ? + object : + jQuery.Deferred(), + promise = deferred.promise(), + resolveArray; + + if ( length > 1 ) { + resolveArray = new Array( length ); + jQuery.each( args, function( index, element ) { + jQuery.when( element ).then( function( value ) { + resolveArray[ index ] = arguments.length > 1 ? slice.call( arguments, 0 ) : value; + if( ! --length ) { + deferred.resolveWith( promise, resolveArray ); + } + }, deferred.reject ); + } ); + } else if ( deferred !== object ) { + deferred.resolve( object ); + } + return promise; + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySubclass( selector, context ) { + return new jQuerySubclass.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySubclass, this ); + jQuerySubclass.superclass = this; + jQuerySubclass.fn = jQuerySubclass.prototype = this(); + jQuerySubclass.fn.constructor = jQuerySubclass; + jQuerySubclass.subclass = this.subclass; + jQuerySubclass.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySubclass) ) { + context = jQuerySubclass(context); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySubclass ); + }; + jQuerySubclass.fn.init.prototype = jQuerySubclass.fn; + var rootjQuerySubclass = jQuerySubclass(document); + return jQuerySubclass; + }, + + browser: {} +}); + +// Create readyList deferred +readyList = jQuery._Deferred(); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +if ( indexOf ) { + jQuery.inArray = function( elem, array ) { + return indexOf.call( array, elem ); + }; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +// Expose jQuery to the global object +return (window.jQuery = window.$ = jQuery); + +})(); + + +(function() { + + jQuery.support = {}; + + var div = document.createElement("div"); + + div.style.display = "none"; + div.innerHTML = "
a"; + + var all = div.getElementsByTagName("*"), + a = div.getElementsByTagName("a")[0], + select = document.createElement("select"), + opt = select.appendChild( document.createElement("option") ); + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return; + } + + jQuery.support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText insted) + style: /red/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55$/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: div.getElementsByTagName("input")[0].value === "on", + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Will be defined later + deleteExpando: true, + optDisabled: false, + checkClone: false, + _scriptEval: null, + noCloneEvent: true, + boxModel: null, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableHiddenOffsets: true + }; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as diabled) + select.disabled = true; + jQuery.support.optDisabled = !opt.disabled; + + jQuery.support.scriptEval = function() { + if ( jQuery.support._scriptEval === null ) { + var root = document.documentElement, + script = document.createElement("script"), + id = "script" + jQuery.now(); + + script.type = "text/javascript"; + try { + script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); + } catch(e) {} + + root.insertBefore( script, root.firstChild ); + + // Make sure that the execution of code works by injecting a script + // tag with appendChild/createTextNode + // (IE doesn't support this, fails, and uses .text instead) + if ( window[ id ] ) { + jQuery.support._scriptEval = true; + delete window[ id ]; + } else { + jQuery.support._scriptEval = false; + } + + root.removeChild( script ); + // release memory in IE + root = script = id = null; + } + + return jQuery.support._scriptEval; + }; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + + } catch(e) { + jQuery.support.deleteExpando = false; + } + + if ( div.attachEvent && div.fireEvent ) { + div.attachEvent("onclick", function click() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + jQuery.support.noCloneEvent = false; + div.detachEvent("onclick", click); + }); + div.cloneNode(true).fireEvent("onclick"); + } + + div = document.createElement("div"); + div.innerHTML = ""; + + var fragment = document.createDocumentFragment(); + fragment.appendChild( div.firstChild ); + + // WebKit doesn't clone checked state correctly in fragments + jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; + + // Figure out if the W3C box model works as expected + // document.body must exist before we can do this + jQuery(function() { + var div = document.createElement("div"), + body = document.getElementsByTagName("body")[0]; + + // Frameset documents with no body should not run this code + if ( !body ) { + return; + } + + div.style.width = div.style.paddingLeft = "1px"; + body.appendChild( div ); + jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; + + if ( "zoom" in div.style ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2; + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
"; + jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2; + } + + div.innerHTML = "
t
"; + var tds = div.getElementsByTagName("td"); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0; + + tds[0].style.display = ""; + tds[1].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE < 8 fail this test) + jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0; + div.innerHTML = ""; + + body.removeChild( div ).style.display = "none"; + div = tds = null; + }); + + // Technique from Juriy Zaytsev + // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + var eventSupported = function( eventName ) { + var el = document.createElement("div"); + eventName = "on" + eventName; + + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( !el.attachEvent ) { + return true; + } + + var isSupported = (eventName in el); + if ( !isSupported ) { + el.setAttribute(eventName, "return;"); + isSupported = typeof el[eventName] === "function"; + } + el = null; + + return isSupported; + }; + + jQuery.support.submitBubbles = eventSupported("submit"); + jQuery.support.changeBubbles = eventSupported("change"); + + // release memory in IE + div = all = a = null; +})(); + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + + return !!elem && !jQuery.isEmptyObject(elem); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache, + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ jQuery.expando ] = id = ++jQuery.uuid; + } else { + id = jQuery.expando; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" ) { + if ( pvt ) { + cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name); + } else { + cache[ id ] = jQuery.extend(cache[ id ], name); + } + } + + thisCache = cache[ id ]; + + // Internal jQuery data is stored in a separate object inside the object's data + // cache in order to avoid key collisions between internal data and user-defined + // data + if ( pvt ) { + if ( !thisCache[ internalKey ] ) { + thisCache[ internalKey ] = {}; + } + + thisCache = thisCache[ internalKey ]; + } + + if ( data !== undefined ) { + thisCache[ name ] = data; + } + + // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should + // not attempt to inspect the internal events object using jQuery.data, as this + // internal data object is undocumented and subject to change. + if ( name === "events" && !thisCache[name] ) { + return thisCache[ internalKey ] && thisCache[ internalKey ].events; + } + + return getByName ? thisCache[ name ] : thisCache; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var internalKey = jQuery.expando, isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ]; + + if ( thisCache ) { + delete thisCache[ name ]; + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !jQuery.isEmptyObject(thisCache) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( pvt ) { + delete cache[ id ][ internalKey ]; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !jQuery.isEmptyObject(cache[ id ]) ) { + return; + } + } + + var internalCache = cache[ id ][ internalKey ]; + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + if ( jQuery.support.deleteExpando || cache != window ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the entire user cache at once because it's faster than + // iterating through each key, but we need to continue to persist internal + // data if it existed + if ( internalCache ) { + cache[ id ] = {}; + cache[ id ][ internalKey ] = internalCache; + + // Otherwise, we need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + } else if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ jQuery.expando ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + } else { + elem[ jQuery.expando ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var data = null; + + if ( typeof key === "undefined" ) { + if ( this.length ) { + data = jQuery.data( this[0] ); + + if ( this[0].nodeType === 1 ) { + var attr = this[0].attributes, name; + for ( var i = 0, l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = name.substr( 5 ); + dataAttr( this[0], name, data[ name ] ); + } + } + } + } + + return data; + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + // Try to fetch any internally stored data first + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + data = dataAttr( this[0], key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + + } else { + return this.each(function() { + var $this = jQuery( this ), + args = [ parts[0], value ]; + + $this.triggerHandler( "setData" + parts[1] + "!", args ); + jQuery.data( this, key, value ); + $this.triggerHandler( "changeData" + parts[1] + "!", args ); + }); + } + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + data = elem.getAttribute( "data-" + key ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + !jQuery.isNaN( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + + + + +jQuery.extend({ + queue: function( elem, type, data ) { + if ( !elem ) { + return; + } + + type = (type || "fx") + "queue"; + var q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( !data ) { + return q || []; + } + + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + + } else { + q.push( data ); + } + + return q; + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(); + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift("inprogress"); + } + + fn.call(elem, function() { + jQuery.dequeue(elem, type); + }); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue", true ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function( i ) { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + type = type || "fx"; + + return this.queue( type, function() { + var elem = this; + setTimeout(function() { + jQuery.dequeue( elem, type ); + }, time ); + }); + }, + + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspaces = /\s+/, + rreturn = /\r/g, + rspecialurl = /^(?:href|src|style)$/, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rradiocheck = /^(?:radio|checkbox)$/i; + +jQuery.props = { + "for": "htmlFor", + "class": "className", + readonly: "readOnly", + maxlength: "maxLength", + cellspacing: "cellSpacing", + rowspan: "rowSpan", + colspan: "colSpan", + tabindex: "tabIndex", + usemap: "useMap", + frameborder: "frameBorder" +}; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.attr ); + }, + + removeAttr: function( name, fn ) { + return this.each(function(){ + jQuery.attr( this, name, "" ); + if ( this.nodeType === 1 ) { + this.removeAttribute( name ); + } + }); + }, + + addClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.addClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( value && typeof value === "string" ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className ) { + elem.className = value; + + } else { + var className = " " + elem.className + " ", + setClass = elem.className; + + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { + setClass += " " + classNames[c]; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.removeClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + var className = (" " + elem.className + " ").replace(rclass, " "); + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[c] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this); + self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspaces ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " "; + for ( var i = 0, l = this.length; i < l; i++ ) { + if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + if ( !arguments.length ) { + var elem = this[0]; + + if ( elem ) { + if ( jQuery.nodeName( elem, "option" ) ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { + return elem.getAttribute("value") === null ? "on" : elem.value; + } + + // Everything else, we just grab the value + return (elem.value || "").replace(rreturn, ""); + + } + + return undefined; + } + + var isFunction = jQuery.isFunction(value); + + return this.each(function(i) { + var self = jQuery(this), val = value; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call(this, i, self.val()); + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray(val) ) { + val = jQuery.map(val, function (value) { + return value == null ? "" : value + ""; + }); + } + + if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { + this.checked = jQuery.inArray( self.val(), val ) >= 0; + + } else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(val); + + jQuery( "option", this ).each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + this.selectedIndex = -1; + } + + } else { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || elem.nodeType === 2 ) { + return undefined; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery(elem)[name](value); + } + + var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), + // Whether we are setting (or getting) + set = value !== undefined; + + // Try to normalize/fix the name + name = notxml && jQuery.props[ name ] || name; + + // Only do all the following if this is a node (faster for style) + if ( elem.nodeType === 1 ) { + // These attributes require special treatment + var special = rspecialurl.test( name ); + + // Safari mis-reports the default selected property of an option + // Accessing the parent's selectedIndex property fixes it + if ( name === "selected" && !jQuery.support.optSelected ) { + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + + // If applicable, access the attribute via the DOM 0 way + // 'in' checks fail in Blackberry 4.7 #6931 + if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { + if ( set ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } + + if ( value === null ) { + if ( elem.nodeType === 1 ) { + elem.removeAttribute( name ); + } + + } else { + elem[ name ] = value; + } + } + + // browsers index elements by id/name on forms, give priority to attributes. + if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { + return elem.getAttributeNode( name ).nodeValue; + } + + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + if ( name === "tabIndex" ) { + var attributeNode = elem.getAttributeNode( "tabIndex" ); + + return attributeNode && attributeNode.specified ? + attributeNode.value : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + + return elem[ name ]; + } + + if ( !jQuery.support.style && notxml && name === "style" ) { + if ( set ) { + elem.style.cssText = "" + value; + } + + return elem.style.cssText; + } + + if ( set ) { + // convert the value to a string (all browsers do this but IE) see #1070 + elem.setAttribute( name, "" + value ); + } + + // Ensure that missing attributes return undefined + // Blackberry 4.7 returns "" from getAttribute #6938 + if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { + return undefined; + } + + var attr = !jQuery.support.hrefNormalized && notxml && special ? + // Some attributes require a special call on IE + elem.getAttribute( name, 2 ) : + elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return attr === null ? undefined : attr; + } + // Handle everything which isn't a DOM element node + if ( set ) { + elem[ name ] = value; + } + return elem[ name ]; + } +}); + + + + +var rnamespaces = /\.(.*)$/, + rformElems = /^(?:textarea|input|select)$/i, + rperiod = /\./g, + rspace = / /g, + rescape = /[^\w\s.|`]/g, + fcleanup = function( nm ) { + return nm.replace(rescape, "\\$&"); + }, + eventKey = "events"; + +/* + * A number of helper functions used for managing events. + * Many of the ideas behind this code originated from + * Dean Edwards' addEvent library. + */ +jQuery.event = { + + // Bind an event to an element + // Original by Dean Edwards + add: function( elem, types, handler, data ) { + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // For whatever reason, IE has trouble passing the window object + // around, causing it to be cloned in the process + if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) { + elem = window; + } + + if ( handler === false ) { + handler = returnFalse; + } else if ( !handler ) { + // Fixes bug #7229. Fix recommended by jdalton + return; + } + + var handleObjIn, handleObj; + + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + + // Make sure that the function being executed has a unique ID + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure + var elemData = jQuery._data( elem ); + + // If no elemData is found then we must be trying to bind to one of the + // banned noData elements + if ( !elemData ) { + return; + } + + var events = elemData[ eventKey ], + eventHandle = elemData.handle; + + if ( typeof events === "function" ) { + // On plain objects events is a fn that holds the the data + // which prevents this data from being JSON serialized + // the function does not need to be called, it just contains the data + eventHandle = events.handle; + events = events.events; + + } else if ( !events ) { + if ( !elem.nodeType ) { + // On plain objects, create a fn that acts as the holder + // of the values to avoid JSON serialization of event data + elemData[ eventKey ] = elemData = function(){}; + } + + elemData.events = events = {}; + } + + if ( !eventHandle ) { + elemData.handle = eventHandle = function() { + // Handle the second event of a trigger and when + // an event is called after a page has unloaded + return typeof jQuery !== "undefined" && !jQuery.event.triggered ? + jQuery.event.handle.apply( eventHandle.elem, arguments ) : + undefined; + }; + } + + // Add elem as a property of the handle function + // This is to prevent a memory leak with non-native events in IE. + eventHandle.elem = elem; + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = types.split(" "); + + var type, i = 0, namespaces; + + while ( (type = types[ i++ ]) ) { + handleObj = handleObjIn ? + jQuery.extend({}, handleObjIn) : + { handler: handler, data: data }; + + // Namespaced event handlers + if ( type.indexOf(".") > -1 ) { + namespaces = type.split("."); + type = namespaces.shift(); + handleObj.namespace = namespaces.slice(0).sort().join("."); + + } else { + namespaces = []; + handleObj.namespace = ""; + } + + handleObj.type = type; + if ( !handleObj.guid ) { + handleObj.guid = handler.guid; + } + + // Get the current list of functions bound to this event + var handlers = events[ type ], + special = jQuery.event.special[ type ] || {}; + + // Init the event handler queue + if ( !handlers ) { + handlers = events[ type ] = []; + + // Check for a special event handler + // Only use addEventListener/attachEvent if the special + // events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add the function to the element's handler list + handlers.push( handleObj ); + + // Keep track of which events have been used, for global triggering + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, pos ) { + // don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + if ( handler === false ) { + handler = returnFalse; + } + + var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + events = elemData && elemData[ eventKey ]; + + if ( !elemData || !events ) { + return; + } + + if ( typeof events === "function" ) { + elemData = events; + events = events.events; + } + + // types is actually an event object here + if ( types && types.type ) { + handler = types.handler; + types = types.type; + } + + // Unbind all events for the element + if ( !types || typeof types === "string" && types.charAt(0) === "." ) { + types = types || ""; + + for ( type in events ) { + jQuery.event.remove( elem, type + types ); + } + + return; + } + + // Handle multiple events separated by a space + // jQuery(...).unbind("mouseover mouseout", fn); + types = types.split(" "); + + while ( (type = types[ i++ ]) ) { + origType = type; + handleObj = null; + all = type.indexOf(".") < 0; + namespaces = []; + + if ( !all ) { + // Namespaced event handlers + namespaces = type.split("."); + type = namespaces.shift(); + + namespace = new RegExp("(^|\\.)" + + jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + eventType = events[ type ]; + + if ( !eventType ) { + continue; + } + + if ( !handler ) { + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( all || namespace.test( handleObj.namespace ) ) { + jQuery.event.remove( elem, origType, handleObj.handler, j ); + eventType.splice( j--, 1 ); + } + } + + continue; + } + + special = jQuery.event.special[ type ] || {}; + + for ( j = pos || 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( handler.guid === handleObj.guid ) { + // remove the given handler for the given type + if ( all || namespace.test( handleObj.namespace ) ) { + if ( pos == null ) { + eventType.splice( j--, 1 ); + } + + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + + if ( pos != null ) { + break; + } + } + } + + // remove generic event handler if no more handlers exist + if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + ret = null; + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + var handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + delete elemData.events; + delete elemData.handle; + + if ( typeof elemData === "function" ) { + jQuery.removeData( elem, eventKey, true ); + + } else if ( jQuery.isEmptyObject( elemData ) ) { + jQuery.removeData( elem, undefined, true ); + } + } + }, + + // bubbling is internal + trigger: function( event, data, elem /*, bubbling */ ) { + // Event object or event type + var type = event.type || event, + bubbling = arguments[3]; + + if ( !bubbling ) { + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + jQuery.extend( jQuery.Event(type), event ) : + // Just the event type (string) + jQuery.Event(type); + + if ( type.indexOf("!") >= 0 ) { + event.type = type = type.slice(0, -1); + event.exclusive = true; + } + + // Handle a global trigger + if ( !elem ) { + // Don't bubble custom events when global (to avoid too much overhead) + event.stopPropagation(); + + // Only trigger if we've ever bound an event for it + if ( jQuery.event.global[ type ] ) { + // XXX This code smells terrible. event.js should not be directly + // inspecting the data cache + jQuery.each( jQuery.cache, function() { + // internalKey variable is just used to make it easier to find + // and potentially change this stuff later; currently it just + // points to jQuery.expando + var internalKey = jQuery.expando, + internalCache = this[ internalKey ]; + if ( internalCache && internalCache.events && internalCache.events[type] ) { + jQuery.event.trigger( event, data, internalCache.handle.elem ); + } + }); + } + } + + // Handle triggering a single element + + // don't do events on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + // Clean up in case it is reused + event.result = undefined; + event.target = elem; + + // Clone the incoming data, if any + data = jQuery.makeArray( data ); + data.unshift( event ); + } + + event.currentTarget = elem; + + // Trigger the event, it is assumed that "handle" is a function + var handle = elem.nodeType ? + jQuery._data( elem, "handle" ) : + (jQuery._data( elem, eventKey ) || {}).handle; + + if ( handle ) { + handle.apply( elem, data ); + } + + var parent = elem.parentNode || elem.ownerDocument; + + // Trigger an inline bound script + try { + if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { + if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { + event.result = false; + event.preventDefault(); + } + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (inlineError) {} + + if ( !event.isPropagationStopped() && parent ) { + jQuery.event.trigger( event, data, parent, true ); + + } else if ( !event.isDefaultPrevented() ) { + var old, + target = event.target, + targetType = type.replace( rnamespaces, "" ), + isClick = jQuery.nodeName( target, "a" ) && targetType === "click", + special = jQuery.event.special[ targetType ] || {}; + + if ( (!special._default || special._default.call( elem, event ) === false) && + !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { + + try { + if ( target[ targetType ] ) { + // Make sure that we don't accidentally re-trigger the onFOO events + old = target[ "on" + targetType ]; + + if ( old ) { + target[ "on" + targetType ] = null; + } + + jQuery.event.triggered = true; + target[ targetType ](); + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (triggerError) {} + + if ( old ) { + target[ "on" + targetType ] = old; + } + + jQuery.event.triggered = false; + } + } + }, + + handle: function( event ) { + var all, handlers, namespaces, namespace_re, events, + namespace_sort = [], + args = jQuery.makeArray( arguments ); + + event = args[0] = jQuery.event.fix( event || window.event ); + event.currentTarget = this; + + // Namespaced event handlers + all = event.type.indexOf(".") < 0 && !event.exclusive; + + if ( !all ) { + namespaces = event.type.split("."); + event.type = namespaces.shift(); + namespace_sort = namespaces.slice(0).sort(); + namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.namespace = event.namespace || namespace_sort.join("."); + + events = jQuery._data(this, eventKey); + + if ( typeof events === "function" ) { + events = events.events; + } + + handlers = (events || {})[ event.type ]; + + if ( events && handlers ) { + // Clone the handlers to prevent manipulation + handlers = handlers.slice(0); + + for ( var j = 0, l = handlers.length; j < l; j++ ) { + var handleObj = handlers[ j ]; + + // Filter the functions by class + if ( all || namespace_re.test( handleObj.namespace ) ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + event.handler = handleObj.handler; + event.data = handleObj.data; + event.handleObj = handleObj; + + var ret = handleObj.handler.apply( this, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + } + + return event.result; + }, + + props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // store a copy of the original event object + // and "clone" to set read-only properties + var originalEvent = event; + event = jQuery.Event( originalEvent ); + + for ( var i = this.props.length, prop; i; ) { + prop = this.props[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary + if ( !event.target ) { + // Fixes #1925 where srcElement might not be defined either + event.target = event.srcElement || document; + } + + // check if target is a textnode (safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && event.fromElement ) { + event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; + } + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && event.clientX != null ) { + var doc = document.documentElement, + body = document.body; + + event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); + event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); + } + + // Add which for key events + if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { + event.which = event.charCode != null ? event.charCode : event.keyCode; + } + + // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) + if ( !event.metaKey && event.ctrlKey ) { + event.metaKey = event.ctrlKey; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && event.button !== undefined ) { + event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); + } + + return event; + }, + + // Deprecated, use jQuery.guid instead + guid: 1E8, + + // Deprecated, use jQuery.proxy instead + proxy: jQuery.proxy, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady, + teardown: jQuery.noop + }, + + live: { + add: function( handleObj ) { + jQuery.event.add( this, + liveConvert( handleObj.origType, handleObj.selector ), + jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); + }, + + remove: function( handleObj ) { + jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); + } + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src ) { + // Allow instantiation without the 'new' keyword + if ( !this.preventDefault ) { + return new jQuery.Event( src ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // timeStamp is buggy for some events on Firefox(#3843) + // So we won't rely on the native value + this.timeStamp = jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Checks if an event happened on an element within another element +// Used in jQuery.event.special.mouseenter and mouseleave handlers +var withinElement = function( event ) { + // Check if mouse(over|out) are still within the same parent element + var parent = event.relatedTarget; + + // Firefox sometimes assigns relatedTarget a XUL element + // which we cannot access the parentNode property of + try { + // Traverse up the tree + while ( parent && parent !== this ) { + parent = parent.parentNode; + } + + if ( parent !== this ) { + // set the correct event type + event.type = event.data; + + // handle event if we actually just moused on to a non sub-element + jQuery.event.handle.apply( this, arguments ); + } + + // assuming we've left the element since we most likely mousedover a xul element + } catch(e) { } +}, + +// In case of event delegation, we only need to rename the event.type, +// liveHandler will take care of the rest. +delegate = function( event ) { + event.type = event.data; + jQuery.event.handle.apply( this, arguments ); +}; + +// Create mouseenter and mouseleave events +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + setup: function( data ) { + jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); + }, + teardown: function( data ) { + jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + } + }; +}); + +// submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function( data, namespaces ) { + if ( this.nodeName && this.nodeName.toLowerCase() !== "form" ) { + jQuery.event.add(this, "click.specialSubmit", function( e ) { + var elem = e.target, + type = elem.type; + + if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + jQuery.event.add(this, "keypress.specialSubmit", function( e ) { + var elem = e.target, + type = elem.type; + + if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + } else { + return false; + } + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialSubmit" ); + } + }; + +} + +// change delegation, happens here so we have bind. +if ( !jQuery.support.changeBubbles ) { + + var changeFilters, + + getVal = function( elem ) { + var type = elem.type, val = elem.value; + + if ( type === "radio" || type === "checkbox" ) { + val = elem.checked; + + } else if ( type === "select-multiple" ) { + val = elem.selectedIndex > -1 ? + jQuery.map( elem.options, function( elem ) { + return elem.selected; + }).join("-") : + ""; + + } else if ( elem.nodeName.toLowerCase() === "select" ) { + val = elem.selectedIndex; + } + + return val; + }, + + testChange = function testChange( e ) { + var elem = e.target, data, val; + + if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { + return; + } + + data = jQuery._data( elem, "_change_data" ); + val = getVal(elem); + + // the current data will be also retrieved by beforeactivate + if ( e.type !== "focusout" || elem.type !== "radio" ) { + jQuery._data( elem, "_change_data", val ); + } + + if ( data === undefined || val === data ) { + return; + } + + if ( data != null || val ) { + e.type = "change"; + e.liveFired = undefined; + return jQuery.event.trigger( e, arguments[1], elem ); + } + }; + + jQuery.event.special.change = { + filters: { + focusout: testChange, + + beforedeactivate: testChange, + + click: function( e ) { + var elem = e.target, type = elem.type; + + if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { + return testChange.call( this, e ); + } + }, + + // Change has to be called before submit + // Keydown will be called before keypress, which is used in submit-event delegation + keydown: function( e ) { + var elem = e.target, type = elem.type; + + if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || + (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || + type === "select-multiple" ) { + return testChange.call( this, e ); + } + }, + + // Beforeactivate happens also before the previous element is blurred + // with this event you can't trigger a change event, but you can store + // information + beforeactivate: function( e ) { + var elem = e.target; + jQuery._data( elem, "_change_data", getVal(elem) ); + } + }, + + setup: function( data, namespaces ) { + if ( this.type === "file" ) { + return false; + } + + for ( var type in changeFilters ) { + jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + } + + return rformElems.test( this.nodeName ); + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialChange" ); + + return rformElems.test( this.nodeName ); + } + }; + + changeFilters = jQuery.event.special.change.filters; + + // Handle when the input is .focus()'d + changeFilters.focus = changeFilters.beforeactivate; +} + +function trigger( type, elem, args ) { + args[0].type = type; + return jQuery.event.handle.apply( elem, args ); +} + +// Create "bubbling" focus and blur events +if ( document.addEventListener ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + jQuery.event.special[ fix ] = { + setup: function() { + this.addEventListener( orig, handler, true ); + }, + teardown: function() { + this.removeEventListener( orig, handler, true ); + } + }; + + function handler( e ) { + e = jQuery.event.fix( e ); + e.type = fix; + return jQuery.event.handle.call( this, e ); + } + }); +} + +jQuery.each(["bind", "one"], function( i, name ) { + jQuery.fn[ name ] = function( type, data, fn ) { + // Handle object literals + if ( typeof type === "object" ) { + for ( var key in type ) { + this[ name ](key, data, type[key], fn); + } + return this; + } + + if ( jQuery.isFunction( data ) || data === false ) { + fn = data; + data = undefined; + } + + var handler = name === "one" ? jQuery.proxy( fn, function( event ) { + jQuery( this ).unbind( event, handler ); + return fn.apply( this, arguments ); + }) : fn; + + if ( type === "unload" && name !== "one" ) { + this.one( type, data, fn ); + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.add( this[i], type, handler, data ); + } + } + + return this; + }; +}); + +jQuery.fn.extend({ + unbind: function( type, fn ) { + // Handle object literals + if ( typeof type === "object" && !type.preventDefault ) { + for ( var key in type ) { + this.unbind(key, type[key]); + } + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.remove( this[i], type, fn ); + } + } + + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.live( types, data, fn, selector ); + }, + + undelegate: function( selector, types, fn ) { + if ( arguments.length === 0 ) { + return this.unbind( "live" ); + + } else { + return this.die( types, null, fn, selector ); + } + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + + triggerHandler: function( type, data ) { + if ( this[0] ) { + var event = jQuery.Event( type ); + event.preventDefault(); + event.stopPropagation(); + jQuery.event.trigger( event, data, this[0] ); + return event.result; + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + i = 1; + + // link all the functions, so any of them can unbind this click handler + while ( i < args.length ) { + jQuery.proxy( fn, args[ i++ ] ); + } + + return this.click( jQuery.proxy( fn, function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + })); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +var liveMap = { + focus: "focusin", + blur: "focusout", + mouseenter: "mouseover", + mouseleave: "mouseout" +}; + +jQuery.each(["live", "die"], function( i, name ) { + jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { + var type, i = 0, match, namespaces, preType, + selector = origSelector || this.selector, + context = origSelector ? this : jQuery( this.context ); + + if ( typeof types === "object" && !types.preventDefault ) { + for ( var key in types ) { + context[ name ]( key, data, types[key], selector ); + } + + return this; + } + + if ( jQuery.isFunction( data ) ) { + fn = data; + data = undefined; + } + + types = (types || "").split(" "); + + while ( (type = types[ i++ ]) != null ) { + match = rnamespaces.exec( type ); + namespaces = ""; + + if ( match ) { + namespaces = match[0]; + type = type.replace( rnamespaces, "" ); + } + + if ( type === "hover" ) { + types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); + continue; + } + + preType = type; + + if ( type === "focus" || type === "blur" ) { + types.push( liveMap[ type ] + namespaces ); + type = type + namespaces; + + } else { + type = (liveMap[ type ] || type) + namespaces; + } + + if ( name === "live" ) { + // bind live handler + for ( var j = 0, l = context.length; j < l; j++ ) { + jQuery.event.add( context[j], "live." + liveConvert( type, selector ), + { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); + } + + } else { + // unbind live handler + context.unbind( "live." + liveConvert( type, selector ), fn ); + } + } + + return this; + }; +}); + +function liveHandler( event ) { + var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, + elems = [], + selectors = [], + events = jQuery._data( this, eventKey ); + + if ( typeof events === "function" ) { + events = events.events; + } + + // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911) + if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) { + return; + } + + if ( event.namespace ) { + namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.liveFired = this; + + var live = events.live.slice(0); + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { + selectors.push( handleObj.selector ); + + } else { + live.splice( j--, 1 ); + } + } + + match = jQuery( event.target ).closest( selectors, event.currentTarget ); + + for ( i = 0, l = match.length; i < l; i++ ) { + close = match[i]; + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) { + elem = close.elem; + related = null; + + // Those two events require additional checking + if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { + event.type = handleObj.preType; + related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; + } + + if ( !related || related !== elem ) { + elems.push({ elem: elem, handleObj: handleObj, level: close.level }); + } + } + } + } + + for ( i = 0, l = elems.length; i < l; i++ ) { + match = elems[i]; + + if ( maxLevel && match.level > maxLevel ) { + break; + } + + event.currentTarget = match.elem; + event.data = match.handleObj.data; + event.handleObj = match.handleObj; + + ret = match.handleObj.origHandler.apply( match.elem, arguments ); + + if ( ret === false || event.isPropagationStopped() ) { + maxLevel = match.level; + + if ( ret === false ) { + stop = false; + } + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + + return stop; +} + +function liveConvert( type, selector ) { + return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&"); +} + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.bind( name, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } +}); + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set; + + if ( !expr ) { + return []; + } + + for ( var i = 0, l = Expr.order.length; i < l; i++ ) { + var match, + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace(/\\/g, ""); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( var type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + var found, item, + filter = Expr.filter[ type ], + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw "Syntax error, unrecognized expression: " + msg; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !/\W/.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !/\W/.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace(/\\/g, "") + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace(/\\/g, ""); + }, + + TAG: function( match, curLoop ) { + return match[1].toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace(/\\/g, ""); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + elem.parentNode.selectedIndex; + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + return "text" === elem.type; + }, + radio: function( elem ) { + return "radio" === elem.type; + }, + + checkbox: function( elem ) { + return "checkbox" === elem.type; + }, + + file: function( elem ) { + return "file" === elem.type; + }, + password: function( elem ) { + return "password" === elem.type; + }, + + submit: function( elem ) { + return "submit" === elem.type; + }, + + image: function( elem ) { + return "image" === elem.type; + }, + + reset: function( elem ) { + return "reset" === elem.type; + }, + + button: function( elem ) { + return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + var first = match[2], + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // If the nodes are siblings (or identical) we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Utility function for retreiving the text value of an array of DOM nodes +Sizzle.getText = function( elems ) { + var ret = "", elem; + + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; + + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += Sizzle.getText( elem.childNodes ); + } + } + + return ret; +}; + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + context.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector, + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + if ( matches ) { + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + return matches.call( node, expr ); + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.POS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var ret = this.pushStack( "", "find", selector ), + length = 0; + + for ( var i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( var n = length; n < ret.length; n++ ) { + for ( var r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && jQuery.filter( selector, this ).length > 0; + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + if ( jQuery.isArray( selectors ) ) { + var match, selector, + matches = {}, + level = 1; + + if ( cur && selectors.length ) { + for ( i = 0, l = selectors.length; i < l; i++ ) { + selector = selectors[i]; + + if ( !matches[selector] ) { + matches[selector] = jQuery.expr.match.POS.test( selector ) ? + jQuery( selector, context || this.context ) : + selector; + } + } + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( selector in matches ) { + match = matches[selector]; + + if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { + ret.push({ selector: selector, elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + } + + return ret; + } + + var pos = POS.test( selectors ) ? + jQuery( selectors, context || this.context ) : null; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique(ret) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + if ( !elem || typeof elem === "string" ) { + return jQuery.inArray( this[0], + // If it receives a string, the selector is used + // If it receives nothing, the siblings are used + elem ? jQuery( elem ) : this.parent().children() ); + } + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ), + // The variable 'args' was introduced in + // https://github.com/jquery/jquery/commit/52a0238 + // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed. + // http://code.google.com/p/v8/issues/detail?id=1050 + args = slice.call(arguments); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, args.join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return (elem === qualifier) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + }); +} + + + + +var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /", "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/build/html/functions.html b/documentation/build/html/functions.html new file mode 100644 index 00000000..bc11f5d3 --- /dev/null +++ b/documentation/build/html/functions.html @@ -0,0 +1,341 @@ + + + + + + + + + Pyqtgraph’s Helper Functions — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

Pyqtgraph’s Helper Functions¶

+
+

Simple Data Display Functions¶

+
+
+pyqtgraph.plot(*args, **kargs)[source]¶
+
+
Create and return a PlotWindow (this is just a window with PlotWidget inside), plot data in it.
+
Accepts a title argument to set the title of the window.
+
All other arguments are used to plot data. (see PlotItem.plot())
+
+
+ +
+
+pyqtgraph.image(*args, **kargs)[source]¶
+
+
Create and return an ImageWindow (this is just a window with ImageView widget inside), show image data inside.
+
Will show 2D or 3D image data.
+
Accepts a title argument to set the title of the window.
+
All other arguments are used to show data. (see ImageView.setImage())
+
+
+ +
+
+

Color, Pen, and Brush Functions¶

+

Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and fill shapes. These classes are highly capable but somewhat awkward to use. Pyqtgraph offers the functions mkColor(), mkPen(), and mkBrush() to simplify the process of creating these classes. In most cases, however, it will be unnecessary to call these functions directly–any function or method that accepts pen or brush arguments will make use of these functions for you. For example, the following three lines all have the same effect:

+
pg.plot(xdata, ydata, pen='r')
+pg.plot(xdata, ydata, pen=pg.mkPen('r'))
+pg.plot(xdata, ydata, pen=QPen(QColor(255, 0, 0)))
+
+
+
+
+pyqtgraph.mkColor(*args)¶
+

Convenience function for constructing QColor from a variety of argument types. Accepted arguments are:

+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
‘c’one of: r, g, b, c, m, y, k, w
R, G, B, [A]integers 0-255
(R, G, B, [A])tuple of integers 0-255
floatgreyscale, 0.0-1.0
intsee intColor()
(int, hues)see intColor()
“RGB”hexadecimal strings; may begin with ‘#’
“RGBA” 
“RRGGBB” 
“RRGGBBAA” 
QColorQColor instance; makes a copy.
+
+ +
+
+pyqtgraph.mkPen(*args, **kargs)¶
+

Convenience function for constructing QPen.

+

Examples:

+
mkPen(color)
+mkPen(color, width=2)
+mkPen(cosmetic=False, width=4.5, color='r')
+mkPen({'color': "FF0", width: 2})
+mkPen(None)   # (no pen)
+
+
+

In these examples, color may be replaced with any arguments accepted by mkColor()

+
+ +
+
+pyqtgraph.mkBrush(*args)¶
+
+
Convenience function for constructing Brush.
+
This function always constructs a solid brush and accepts the same arguments as mkColor()
+
Calling mkBrush(None) returns an invisible brush.
+
+
+ +
+
+pyqtgraph.hsvColor(h, s=1.0, v=1.0, a=1.0)¶
+

Generate a QColor from HSVa values.

+
+ +
+
+pyqtgraph.intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs)¶
+

Creates a QColor from a single index. Useful for stepping through a predefined list of colors.

+

The argument index determines which color from the set will be returned. All other arguments determine what the set of predefined colors will be

+

Colors are chosen by cycling across hues while varying the value (brightness). +By default, this selects from a list of 9 hues.

+
+ +
+
+pyqtgraph.colorTuple(c)¶
+

Return a tuple (R,G,B,A) from a QColor

+
+ +
+
+pyqtgraph.colorStr(c)¶
+

Generate a hex string code from a QColor

+
+ +
+
+

Data Slicing¶

+
+
+pyqtgraph.affineSlice(data, shape, origin, vectors, axes, **kargs)¶
+

Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.

+

The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets.

+

For a graphical interface to this function, see ROI.getArrayRegion()

+

Arguments:

+
+
+
data (ndarray): the original dataset
+
shape: the shape of the slice to take (Note the return value may have more dimensions than len(shape))
+
origin: the location in the original dataset that will become the origin in the sliced data.
+
vectors: list of unit vectors which point in the direction of the slice axes
+
+
    +
  • each vector must have the same length as axes
  • +
  • If the vectors are not unit length, the result will be scaled.
  • +
  • If the vectors are not orthogonal, the result will be sheared.
  • +
+

axes: the axes in the original dataset which correspond to the slice vectors

+
+

Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes

+
+
    +
  • data = array with dims (time, x, y, z) = (100, 40, 40, 40)
  • +
  • The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1)
  • +
  • The origin of the slice will be at (x,y,z) = (40, 0, 0)
  • +
  • We will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20)
  • +
+
+

The call for this example would look like:

+
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
+
+
+

Note the following must be true:

+
+
+
len(shape) == len(vectors)
+
len(origin) == len(axes) == len(vectors[0])
+
+
+
+ +
+
+

SI Unit Conversion Functions¶

+
+
+pyqtgraph.siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True)¶
+

Return the number x formatted in engineering notation with SI prefix.

+

Example:

+
siFormat(0.0001, suffix='V')  # returns "100 μV"
+
+
+
+ +
+
+pyqtgraph.siScale(x, minVal=1e-25, allowUnicode=True)¶
+

Return the recommended scale factor and SI prefix string for x.

+

Example:

+
siScale(0.0001)   # returns (1e6, 'μ')
+# This indicates that the number 0.0001 is best represented as 0.0001 * 1e6 = 100 μUnits
+
+
+
+ +
+
+pyqtgraph.siEval(s)¶
+

Convert a value written in SI notation to its equivalent prefixless value

+

Example:

+
siEval("100 μV")  # returns 0.0001
+
+
+
+ +
+
+ + +
+
+
+
+
+

Table Of Contents

+ + +

Previous topic

+

API Reference

+

Next topic

+

Pyqtgraph’s Graphics Items

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/genindex.html b/documentation/build/html/genindex.html new file mode 100644 index 00000000..50d88bf2 --- /dev/null +++ b/documentation/build/html/genindex.html @@ -0,0 +1,457 @@ + + + + + + + + + Index — pyqtgraph v1.8 documentation + + + + + + + + + + + +
+
+
+
+ + +

Index

+ +
+ _ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | P | R | S | T | U | V +
+

_

+ + +
+
__init__() (pyqtgraph.ArrowItem method)
+
+
(pyqtgraph.AxisItem method)
+
(pyqtgraph.ButtonItem method)
+
(pyqtgraph.CheckTable method)
+
(pyqtgraph.ColorButton method)
+
(pyqtgraph.CurveArrow method)
+
(pyqtgraph.CurvePoint method)
+
(pyqtgraph.DataTreeWidget method)
+
(pyqtgraph.FileDialog method)
+
(pyqtgraph.GradientEditorItem method)
+
(pyqtgraph.GradientLegend method)
+
(pyqtgraph.GradientWidget method)
+
(pyqtgraph.GraphicsLayout method)
+
(pyqtgraph.GraphicsLayoutWidget method)
+
(pyqtgraph.GraphicsObject method)
+
(pyqtgraph.GraphicsView method)
+
(pyqtgraph.GraphicsWidget method)
+
(pyqtgraph.GridItem method)
+
(pyqtgraph.HistogramLUTItem method)
+
(pyqtgraph.HistogramLUTWidget method)
+
(pyqtgraph.ImageItem method)
+
(pyqtgraph.ImageView method)
+
(pyqtgraph.InfiniteLine method)
+
(pyqtgraph.JoystickButton method)
+
(pyqtgraph.LabelItem method)
+
(pyqtgraph.LinearRegionItem method)
+
(pyqtgraph.MultiPlotWidget method)
+
(pyqtgraph.PlotCurveItem method)
+
(pyqtgraph.PlotDataItem method)
+
(pyqtgraph.PlotItem method)
+
(pyqtgraph.PlotWidget method)
+
(pyqtgraph.ProgressDialog method)
+
(pyqtgraph.ROI method)
+
(pyqtgraph.RawImageWidget method)
+
(pyqtgraph.ScaleBar method)
+
(pyqtgraph.ScatterPlotItem method)
+
(pyqtgraph.SpinBox method)
+
(pyqtgraph.TableWidget method)
+
(pyqtgraph.TreeWidget method)
+
(pyqtgraph.UIGraphicsItem method)
+
(pyqtgraph.VTickGroup method)
+
(pyqtgraph.VerticalLabel method)
+
(pyqtgraph.ViewBox method)
+
+
+ +

A

+ + + +
+
addAvgCurve() (pyqtgraph.PlotItem method)
+
affineSlice() (in module pyqtgraph)
+
appendData() (pyqtgraph.TableWidget method)
+
+
ArrowItem (class in pyqtgraph)
+
AxisItem (class in pyqtgraph)
+
+ +

B

+ + +
+
ButtonItem (class in pyqtgraph)
+
+ +

C

+ + + +
+
CheckTable (class in pyqtgraph)
+
childrenBoundingRect() (pyqtgraph.ViewBox method)
+
childTransform() (pyqtgraph.ViewBox method)
+
ColorButton (class in pyqtgraph)
+
colorStr() (in module pyqtgraph)
+
+
colorTuple() (in module pyqtgraph)
+
copy() (pyqtgraph.TableWidget method)
+
CurveArrow (class in pyqtgraph)
+
CurvePoint (class in pyqtgraph)
+
+ +

D

+ + + +
+
DataTreeWidget (class in pyqtgraph)
+
+
deviceTransform() (pyqtgraph.GraphicsObject method)
+
+ +

E

+ + + +
+
editingFinishedEvent() (pyqtgraph.SpinBox method)
+
+
enableAutoScale() (pyqtgraph.PlotItem method)
+
+ +

F

+ + +
+
FileDialog (class in pyqtgraph)
+
+ +

G

+ + + +
+
getArrayRegion() (pyqtgraph.ROI method)
+
getArraySlice() (pyqtgraph.ROI method)
+
getBoundingParents() (pyqtgraph.GraphicsObject method)
+
getGlobalTransform() (pyqtgraph.ROI method)
+
getHistogram() (pyqtgraph.ImageItem method)
+
getLocalHandlePositions() (pyqtgraph.ROI method)
+
getLookupTable() (pyqtgraph.GradientEditorItem method)
+
getRegion() (pyqtgraph.LinearRegionItem method)
+
getViewBox() (pyqtgraph.GraphicsObject method)
+
getViewWidget() (pyqtgraph.GraphicsObject method)
+
+
GradientEditorItem (class in pyqtgraph)
+
GradientLegend (class in pyqtgraph)
+
GradientWidget (class in pyqtgraph)
+
GraphicsLayout (class in pyqtgraph)
+
GraphicsLayoutWidget (class in pyqtgraph)
+
GraphicsObject (class in pyqtgraph)
+
GraphicsView (class in pyqtgraph)
+
GraphicsWidget (class in pyqtgraph)
+
GridItem (class in pyqtgraph)
+
+ +

H

+ + + +
+
handleChange() (pyqtgraph.ROI method)
+
HistogramLUTItem (class in pyqtgraph)
+
+
HistogramLUTWidget (class in pyqtgraph)
+
hsvColor() (in module pyqtgraph)
+
+ +

I

+ + + +
+
image() (in module pyqtgraph)
+
ImageItem (class in pyqtgraph)
+
ImageView (class in pyqtgraph)
+
InfiniteLine (class in pyqtgraph)
+
intColor() (in module pyqtgraph)
+
+
interpret() (pyqtgraph.SpinBox method)
+
itemBoundingRect() (pyqtgraph.ViewBox method)
+
itemMoving() (pyqtgraph.TreeWidget method)
+
iteratorFn() (pyqtgraph.TableWidget method)
+
+ +

J

+ + + +
+
JoystickButton (class in pyqtgraph)
+
+
jumpFrames() (pyqtgraph.ImageView method)
+
+ +

K

+ + +
+
keyPressEvent() (pyqtgraph.ViewBox method)
+
+ +

L

+ + + +
+
LabelItem (class in pyqtgraph)
+
LinearRegionItem (class in pyqtgraph)
+
+
linkXChanged() (pyqtgraph.PlotItem method)
+
linkYChanged() (pyqtgraph.PlotItem method)
+
+ +

M

+ + + +
+
mapFromView() (pyqtgraph.ViewBox method)
+
mapSceneToView() (pyqtgraph.ViewBox method)
+
mapToView() (pyqtgraph.ViewBox method)
+
mapViewToScene() (pyqtgraph.ViewBox method)
+
mkBrush() (in module pyqtgraph)
+
+
mkColor() (in module pyqtgraph)
+
mkPen() (in module pyqtgraph)
+
mouseShape() (pyqtgraph.UIGraphicsItem method)
+
MultiPlotWidget (class in pyqtgraph)
+
+ +

N

+ + + +
+
nextCol() (pyqtgraph.GraphicsLayout method)
+
+
nextRow() (pyqtgraph.GraphicsLayout method)
+
+ +

P

+ + + +
+
pixelLength() (pyqtgraph.GraphicsObject method)
+
pixelSize() (pyqtgraph.GraphicsView method)
+
+
(pyqtgraph.ImageItem method)
+
+
pixelVectors() (pyqtgraph.GraphicsObject method)
+
plot() (in module pyqtgraph)
+
+
(pyqtgraph.PlotItem method)
+
+
PlotCurveItem (class in pyqtgraph)
+
PlotDataItem (class in pyqtgraph)
+
+
PlotItem (class in pyqtgraph)
+
PlotWidget (class in pyqtgraph)
+
ProgressDialog (class in pyqtgraph)
+
pyqtgraph.dockarea (module)
+
pyqtgraph.parametertree (module)
+
+ +

R

+ + + +
+
RawImageWidget (class in pyqtgraph)
+
realBoundingRect() (pyqtgraph.UIGraphicsItem method)
+
+
ROI (class in pyqtgraph)
+
+ +

S

+ + + +
+
saveState() (pyqtgraph.ROI method)
+
ScaleBar (class in pyqtgraph)
+
scaleBy() (pyqtgraph.ViewBox method)
+
scaleToImage() (pyqtgraph.GraphicsView method)
+
ScatterPlotItem (class in pyqtgraph)
+
setAngle() (pyqtgraph.InfiniteLine method)
+
setAspectLocked() (pyqtgraph.ViewBox method)
+
setAttr() (pyqtgraph.LabelItem method)
+
setBounds() (pyqtgraph.InfiniteLine method)
+
setCentralWidget() (pyqtgraph.GraphicsView method)
+
setData() (pyqtgraph.DataTreeWidget method)
+
+
(pyqtgraph.PlotCurveItem method)
+
(pyqtgraph.PlotDataItem method)
+
+
setGrid() (pyqtgraph.AxisItem method)
+
setImage() (pyqtgraph.ImageItem method)
+
+
(pyqtgraph.ImageView method)
+
(pyqtgraph.RawImageWidget method)
+
+
setLabel() (pyqtgraph.PlotItem method)
+
setLabels() (pyqtgraph.GradientLegend method)
+
setLevels() (pyqtgraph.ImageItem method)
+
setLookupTable() (pyqtgraph.ImageItem method)
+
setNewBounds() (pyqtgraph.UIGraphicsItem method)
+
setPen() (pyqtgraph.PlotDataItem method)
+
+
setPoints() (pyqtgraph.ScatterPlotItem method)
+
setProperty() (pyqtgraph.SpinBox method)
+
setPxMode() (pyqtgraph.ImageItem method)
+
setRange() (pyqtgraph.ViewBox method)
+
setScale() (pyqtgraph.AxisItem method)
+
setShadowPen() (pyqtgraph.PlotDataItem method)
+
setText() (pyqtgraph.LabelItem method)
+
setTitle() (pyqtgraph.PlotItem method)
+
setValue() (pyqtgraph.SpinBox method)
+
setXLink() (pyqtgraph.PlotItem method)
+
setYLink() (pyqtgraph.PlotItem method)
+
showAxis() (pyqtgraph.PlotItem method)
+
showLabel() (pyqtgraph.PlotItem method)
+
siEval() (in module pyqtgraph)
+
siFormat() (in module pyqtgraph)
+
sigRangeChanged (pyqtgraph.PlotItem attribute)
+
siScale() (in module pyqtgraph)
+
SpinBox (class in pyqtgraph)
+
+ +

T

+ + + +
+
TableWidget (class in pyqtgraph)
+
targetRect() (pyqtgraph.ViewBox method)
+
timeIndex() (pyqtgraph.ImageView method)
+
+
translate() (pyqtgraph.ROI method)
+
TreeWidget (class in pyqtgraph)
+
+ +

U

+ + + +
+
UIGraphicsItem (class in pyqtgraph)
+
updatePlotList() (pyqtgraph.PlotItem method)
+
+
updateXScale() (pyqtgraph.PlotItem method)
+
updateYScale() (pyqtgraph.PlotItem method)
+
+ +

V

+ + + +
+
VerticalLabel (class in pyqtgraph)
+
ViewBox (class in pyqtgraph)
+
viewChangedEvent() (pyqtgraph.UIGraphicsItem method)
+
viewGeometry() (pyqtgraph.PlotItem method)
+
+
viewRangeChanged() (pyqtgraph.UIGraphicsItem method)
+
viewRect() (pyqtgraph.GraphicsObject method)
+
+
(pyqtgraph.GraphicsView method)
+
(pyqtgraph.ViewBox method)
+
+
viewTransform() (pyqtgraph.GraphicsObject method)
+
VTickGroup (class in pyqtgraph)
+
+ + + +
+
+
+
+
+ + + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/arrowitem.html b/documentation/build/html/graphicsItems/arrowitem.html new file mode 100644 index 00000000..f6a543e6 --- /dev/null +++ b/documentation/build/html/graphicsItems/arrowitem.html @@ -0,0 +1,132 @@ + + + + + + + + + ArrowItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ArrowItem¶

+
+
+class pyqtgraph.ArrowItem(**opts)¶
+

For displaying scale-invariant arrows. +For arrows pointing to a location on a curve, see CurveArrow

+
+
+__init__(**opts)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

AxisItem

+

Next topic

+

CurvePoint

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/axisitem.html b/documentation/build/html/graphicsItems/axisitem.html new file mode 100644 index 00000000..c370aea2 --- /dev/null +++ b/documentation/build/html/graphicsItems/axisitem.html @@ -0,0 +1,154 @@ + + + + + + + + + AxisItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

AxisItem¶

+
+
+class pyqtgraph.AxisItem(orientation, pen=None, linkView=None, parent=None, maxTickLength=-5)¶
+
+
+__init__(orientation, pen=None, linkView=None, parent=None, maxTickLength=-5)¶
+

GraphicsItem showing a single plot axis with ticks, values, and label. +Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. +Ticks can be extended to make a grid.

+
+ +
+
+setGrid(grid)¶
+

Set the alpha value for the grid, or False to disable.

+
+ +
+
+setScale(scale=None)¶
+

Set the value scaling for this axis. +The scaling value 1) multiplies the values displayed along the axis +and 2) changes the way units are displayed in the label. +For example:

+
+If the axis spans values from -0.1 to 0.1 and has units set to ‘V’ +then a scale of 1000 would cause the axis to display values -100 to 100 +and the units would appear as ‘mV’
+

If scale is None, then it will be determined automatically based on the current +range displayed by the axis.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

GraphicsLayout

+

Next topic

+

ArrowItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/buttonitem.html b/documentation/build/html/graphicsItems/buttonitem.html new file mode 100644 index 00000000..85e06f4e --- /dev/null +++ b/documentation/build/html/graphicsItems/buttonitem.html @@ -0,0 +1,131 @@ + + + + + + + + + ButtonItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ButtonItem¶

+
+
+class pyqtgraph.ButtonItem(imageFile, width=None, parentItem=None)¶
+

Button graphicsItem displaying an image.

+
+
+__init__(imageFile, width=None, parentItem=None)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

GradientLegend

+

Next topic

+

GraphicsObject

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/curvearrow.html b/documentation/build/html/graphicsItems/curvearrow.html new file mode 100644 index 00000000..72605c08 --- /dev/null +++ b/documentation/build/html/graphicsItems/curvearrow.html @@ -0,0 +1,132 @@ + + + + + + + + + CurveArrow — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

CurveArrow¶

+
+
+class pyqtgraph.CurveArrow(curve, index=0, pos=None, **opts)¶
+

Provides an arrow that points to any specific sample on a PlotCurveItem. +Provides properties that can be animated.

+
+
+__init__(curve, index=0, pos=None, **opts)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

CurvePoint

+

Next topic

+

GridItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/curvepoint.html b/documentation/build/html/graphicsItems/curvepoint.html new file mode 100644 index 00000000..a1833226 --- /dev/null +++ b/documentation/build/html/graphicsItems/curvepoint.html @@ -0,0 +1,136 @@ + + + + + + + + + CurvePoint — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

CurvePoint¶

+
+
+class pyqtgraph.CurvePoint(curve, index=0, pos=None)¶
+

A GraphicsItem that sets its location to a point on a PlotCurveItem. +Also rotates to be tangent to the curve. +The position along the curve is a Qt property, and thus can be easily animated.

+

Note: This class does not display anything; see CurveArrow for an applied example

+
+
+__init__(curve, index=0, pos=None)¶
+

Position can be set either as an index referring to the sample number or +the position 0.0 - 1.0

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ArrowItem

+

Next topic

+

CurveArrow

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/gradienteditoritem.html b/documentation/build/html/graphicsItems/gradienteditoritem.html new file mode 100644 index 00000000..c3061263 --- /dev/null +++ b/documentation/build/html/graphicsItems/gradienteditoritem.html @@ -0,0 +1,136 @@ + + + + + + + + + GradientEditorItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GradientEditorItem¶

+
+
+class pyqtgraph.GradientEditorItem(*args, **kargs)¶
+
+
+__init__(*args, **kargs)¶
+
+ +
+
+getLookupTable(nPts, alpha=True)¶
+

Return an RGB/A lookup table.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

VTickGroup

+

Next topic

+

HistogramLUTItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/gradientlegend.html b/documentation/build/html/graphicsItems/gradientlegend.html new file mode 100644 index 00000000..0d949f0b --- /dev/null +++ b/documentation/build/html/graphicsItems/gradientlegend.html @@ -0,0 +1,138 @@ + + + + + + + + + GradientLegend — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GradientLegend¶

+
+
+class pyqtgraph.GradientLegend(view, size, offset)¶
+

Draws a color gradient rectangle along with text labels denoting the value at specific +points along the gradient.

+
+
+__init__(view, size, offset)¶
+
+ +
+
+setLabels(l)¶
+

Defines labels to appear next to the color scale. Accepts a dict of {text: value} pairs

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

HistogramLUTItem

+

Next topic

+

ButtonItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/graphicslayout.html b/documentation/build/html/graphicsItems/graphicslayout.html new file mode 100644 index 00000000..6ba6ebf8 --- /dev/null +++ b/documentation/build/html/graphicsItems/graphicslayout.html @@ -0,0 +1,144 @@ + + + + + + + + + GraphicsLayout — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GraphicsLayout¶

+
+
+class pyqtgraph.GraphicsLayout(parent=None, border=None)¶
+

Used for laying out GraphicsWidgets in a grid.

+
+
+__init__(parent=None, border=None)¶
+
+ +
+
+nextCol(colspan=1)¶
+

Advance to next column, while returning the current column number +(generally only for internal use–called by addItem)

+
+ +
+
+nextRow()¶
+

Advance to next row for automatic item placement

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ROI

+

Next topic

+

AxisItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/graphicsobject.html b/documentation/build/html/graphicsItems/graphicsobject.html new file mode 100644 index 00000000..ad3dcd5f --- /dev/null +++ b/documentation/build/html/graphicsItems/graphicsobject.html @@ -0,0 +1,189 @@ + + + + + + + + + GraphicsObject — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GraphicsObject¶

+
+
+class pyqtgraph.GraphicsObject(*args)¶
+

Extends QGraphicsObject with a few important functions. +(Most of these assume that the object is in a scene with a single view)

+

This class also generates a cache of the Qt-internal addresses of each item +so that GraphicsScene.items() can return the correct objects (this is a PyQt bug)

+
+
+__init__(*args)¶
+
+ +
+
+deviceTransform(viewportTransform=None)¶
+

Return the transform that converts item coordinates to device coordinates (usually pixels). +Extends deviceTransform to automatically determine the viewportTransform.

+
+ +
+
+getBoundingParents()¶
+

Return a list of parents to this item that have child clipping enabled.

+
+ +
+
+getViewBox()¶
+

Return the first ViewBox or GraphicsView which bounds this item’s visible space. +If this item is not contained within a ViewBox, then the GraphicsView is returned. +If the item is contained inside nested ViewBoxes, then the inner-most ViewBox is returned. +The result is cached; clear the cache with forgetViewBox()

+
+ +
+
+getViewWidget()¶
+

Return the view widget for this item. If the scene has multiple views, only the first view is returned. +The return value is cached; clear the cached value with forgetViewWidget()

+
+ +
+
+pixelLength(direction)¶
+

Return the length of one pixel in the direction indicated (in local coordinates)

+
+ +
+
+pixelVectors()¶
+

Return vectors in local coordinates representing the width and height of a view pixel.

+
+ +
+
+viewRect()¶
+

Return the bounds (in item coordinates) of this item’s ViewBox or GraphicsWidget

+
+ +
+
+viewTransform()¶
+

Return the transform that maps from local coordinates to the item’s ViewBox coordinates +If there is no ViewBox, return the scene transform. +Returns None if the item does not have a view.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ButtonItem

+

Next topic

+

GraphicsWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/graphicswidget.html b/documentation/build/html/graphicsItems/graphicswidget.html new file mode 100644 index 00000000..d0283c97 --- /dev/null +++ b/documentation/build/html/graphicsItems/graphicswidget.html @@ -0,0 +1,132 @@ + + + + + + + + + GraphicsWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GraphicsWidget¶

+
+
+class pyqtgraph.GraphicsWidget(*args, **kargs)¶
+
+
+__init__(*args, **kargs)¶
+

Extends QGraphicsWidget with a workaround for a PyQt bug. +This class is otherwise identical to QGraphicsWidget.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

GraphicsObject

+

Next topic

+

UIGraphicsItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/griditem.html b/documentation/build/html/graphicsItems/griditem.html new file mode 100644 index 00000000..8abbf11f --- /dev/null +++ b/documentation/build/html/graphicsItems/griditem.html @@ -0,0 +1,132 @@ + + + + + + + + + GridItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GridItem¶

+
+
+class pyqtgraph.GridItem¶
+

Displays a rectangular grid of lines indicating major divisions within a coordinate system. +Automatically determines what divisions to use.

+
+
+__init__()¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

CurveArrow

+

Next topic

+

ScaleBar

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/histogramlutitem.html b/documentation/build/html/graphicsItems/histogramlutitem.html new file mode 100644 index 00000000..529876dc --- /dev/null +++ b/documentation/build/html/graphicsItems/histogramlutitem.html @@ -0,0 +1,130 @@ + + + + + + + + + HistogramLUTItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

HistogramLUTItem¶

+
+
+class pyqtgraph.HistogramLUTItem(image=None)¶
+
+
+__init__(image=None)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

GradientEditorItem

+

Next topic

+

GradientLegend

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/imageitem.html b/documentation/build/html/graphicsItems/imageitem.html new file mode 100644 index 00000000..e4a2aff2 --- /dev/null +++ b/documentation/build/html/graphicsItems/imageitem.html @@ -0,0 +1,184 @@ + + + + + + + + + ImageItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ImageItem¶

+
+
+class pyqtgraph.ImageItem(image=None, **kargs)¶
+

GraphicsObject displaying an image. Optimized for rapid update (ie video display)

+
+
+__init__(image=None, **kargs)¶
+

See setImage for all allowed arguments.

+
+ +
+
+getHistogram(bins=500, step=3)¶
+

returns x and y arrays containing the histogram values for the current image. +The step argument causes pixels to be skipped when computing the histogram to save time.

+
+ +
+
+pixelSize()¶
+

return scene-size of a single pixel in the image

+
+ +
+
+setImage(image=None, autoLevels=None, **kargs)¶
+

Update the image displayed by this item. +Arguments:

+
+image +autoLevels +lut +levels +opacity +compositionMode +border
+
+ +
+
+setLevels(levels, update=True)¶
+
+
Set image scaling levels. Can be one of:
+
[blackLevel, whiteLevel] +[[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
+
+

Only the first format is compatible with lookup tables.

+
+ +
+
+setLookupTable(lut, update=True)¶
+

Set the lookup table to use for this image. (see functions.makeARGB for more information on how this is used) +Optionally, lut can be a callable that accepts the current image as an argument and returns the lookup table to use.

+
+ +
+
+setPxMode(b)¶
+

Set whether the item ignores transformations and draws directly to screen pixels.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

PlotItem

+

Next topic

+

ViewBox

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/index.html b/documentation/build/html/graphicsItems/index.html new file mode 100644 index 00000000..7d8fbe62 --- /dev/null +++ b/documentation/build/html/graphicsItems/index.html @@ -0,0 +1,149 @@ + + + + + + + + + Pyqtgraph’s Graphics Items — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

Pyqtgraph’s Graphics Items¶

+

Since pyqtgraph relies on Qt’s GraphicsView framework, most of its graphics functionality is implemented as QGraphicsItem subclasses. This has two important consequences: 1) virtually anything you want to draw can be easily accomplished using the functionality provided by Qt. 2) Many of pyqtgraph’s GraphicsItem classes can be used in any normal QGraphicsScene.

+

Contents:

+ +
+ + +
+
+
+
+
+

Previous topic

+

Pyqtgraph’s Helper Functions

+

Next topic

+

PlotDataItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/infiniteline.html b/documentation/build/html/graphicsItems/infiniteline.html new file mode 100644 index 00000000..0f7b3630 --- /dev/null +++ b/documentation/build/html/graphicsItems/infiniteline.html @@ -0,0 +1,155 @@ + + + + + + + + + InfiniteLine — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

InfiniteLine¶

+
+
+class pyqtgraph.InfiniteLine(pos=None, angle=90, pen=None, movable=False, bounds=None)¶
+

Displays a line of infinite length. +This line may be dragged to indicate a position in data coordinates.

+
+
+__init__(pos=None, angle=90, pen=None, movable=False, bounds=None)¶
+
+
Initialization options:
+
pos - Position of the line. This can be a QPointF or a single value for vertical/horizontal lines. +angle - Angle of line in degrees. 0 is horizontal, 90 is vertical. +pen - Pen to use when drawing line +movable - If True, the line can be dragged to a new position by the user +bounds - Optional [min, max] bounding values. Bounds are only valid if the line is vertical or horizontal.
+
+
+ +
+
+setAngle(angle)¶
+

Takes angle argument in degrees. +0 is horizontal; 90 is vertical.

+

Note that the use of value() and setValue() changes if the line is +not vertical or horizontal.

+
+ +
+
+setBounds(bounds)¶
+

Set the (minimum, maximum) allowable values when dragging.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

LinearRegionItem

+

Next topic

+

ROI

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/labelitem.html b/documentation/build/html/graphicsItems/labelitem.html new file mode 100644 index 00000000..058fd7da --- /dev/null +++ b/documentation/build/html/graphicsItems/labelitem.html @@ -0,0 +1,154 @@ + + + + + + + + + LabelItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

LabelItem¶

+
+
+class pyqtgraph.LabelItem(text, parent=None, **args)¶
+

GraphicsWidget displaying text. +Used mainly as axis labels, titles, etc.

+
+
Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use QGraphicsTextItem
+
with the flag ItemIgnoresTransformations set.
+
+
+
+__init__(text, parent=None, **args)¶
+
+ +
+
+setAttr(attr, value)¶
+

Set default text properties. See setText() for accepted parameters.

+
+ +
+
+setText(text, **args)¶
+

Set the text and text properties in the label. Accepts optional arguments for auto-generating +a CSS style string:

+
+color: string (example: ‘CCFF00’) +size: string (example: ‘8pt’) +bold: boolean +italic: boolean
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ScaleBar

+

Next topic

+

VTickGroup

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/linearregionitem.html b/documentation/build/html/graphicsItems/linearregionitem.html new file mode 100644 index 00000000..70795d12 --- /dev/null +++ b/documentation/build/html/graphicsItems/linearregionitem.html @@ -0,0 +1,138 @@ + + + + + + + + + LinearRegionItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

LinearRegionItem¶

+
+
+class pyqtgraph.LinearRegionItem(values=[, 0, 1], orientation=None, brush=None, movable=True, bounds=None)¶
+

Used for marking a horizontal or vertical region in plots. +The region can be dragged and is bounded by lines which can be dragged individually.

+
+
+__init__(values=[, 0, 1], orientation=None, brush=None, movable=True, bounds=None)¶
+
+ +
+
+getRegion()¶
+

Return the values at the edges of the region.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ViewBox

+

Next topic

+

InfiniteLine

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/plotcurveitem.html b/documentation/build/html/graphicsItems/plotcurveitem.html new file mode 100644 index 00000000..a2586879 --- /dev/null +++ b/documentation/build/html/graphicsItems/plotcurveitem.html @@ -0,0 +1,141 @@ + + + + + + + + + PlotCurveItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

PlotCurveItem¶

+
+
+class pyqtgraph.PlotCurveItem(y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, color=None, clickable=False)¶
+

Class representing a single plot curve. Provides: +- Fast data update +- FFT display mode +- shadow pen +- mouse interaction

+
+
+__init__(y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, color=None, clickable=False)¶
+
+ +
+
+setData(x, y, copy=False)¶
+

For Qwt compatibility

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

PlotDataItem

+

Next topic

+

ScatterPlotItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/plotdataitem.html b/documentation/build/html/graphicsItems/plotdataitem.html new file mode 100644 index 00000000..e0f95a14 --- /dev/null +++ b/documentation/build/html/graphicsItems/plotdataitem.html @@ -0,0 +1,289 @@ + + + + + + + + + PlotDataItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

PlotDataItem¶

+
+
+class pyqtgraph.PlotDataItem(*args, **kargs)¶
+

GraphicsItem for displaying plot curves, scatter plots, or both.

+
+
+__init__(*args, **kargs)¶
+

There are many different ways to create a PlotDataItem:

+

Data initialization: (x,y data only)

+
+ ++++ + + + + + + + + + + + + + + +
PlotDataItem(xValues, yValues)x and y values may be any sequence (including ndarray) of real numbers
PlotDataItem(yValues)y values only – x will be automatically set to range(len(y))
PlotDataItem(x=xValues, y=yValues)x and y given by keyword arguments
PlotDataItem(ndarray(Nx2))numpy array with shape (N, 2) where x=data[:,0] and y=data[:,1]
+
+

Data initialization: (x,y data AND may include spot style)

+
+ ++++ + + + + + + + + + + + + + + +
PlotDataItem(recarray)numpy array with dtype=[(‘x’, float), (‘y’, float), ...]
PlotDataItem(list-of-dicts)[{‘x’: x, ‘y’: y, ...}, ...]
PlotDataItem(dict-of-lists){‘x’: [...], ‘y’: [...], ...}
PlotDataItem(MetaArray)1D array of Y values with X sepecified as axis values +OR 2D array with a column ‘y’ and extra columns as needed.
+
+

Line style keyword arguments:

+
+ ++++ + + + + + + + + + + + + + + +
penpen to use for drawing line between points. Default is solid grey, 1px width. Use None to disable line drawing.
shadowPenpen for secondary line to draw behind the primary line. disabled by default.
fillLevelfill the area between the curve and fillLevel
fillBrushfill to use when fillLevel is specified
+
+

Point style keyword arguments:

+
+ ++++ + + + + + + + + + + + + + + + + + +
symbolsymbol to use for drawing points OR list of symbols, one per point. Default is no symbol. +options are o, s, t, d, +
symbolPenoutline pen for drawing points OR list of pens, one per point
symbolBrushbrush for filling points OR list of brushes, one per point
symbolSizediameter of symbols OR list of diameters
pxMode(bool) If True, then symbolSize is specified in pixels. If False, then symbolSize is +specified in data coordinates.
+
+

Optimization keyword arguments:

+
+ ++++ + + + + + + + + +
identicalspots are all identical. The spot image will be rendered only once and repeated for every point
decimate(int) decimate data
+
+

Meta-info keyword arguments:

+
+ ++++ + + + + + +
namename of dataset. This would appear in a legend
+
+
+ +
+
+setData(*args, **kargs)¶
+

Clear any data displayed by this item and display new data. +See __init__() for details; it accepts the same arguments.

+
+ +
+
+setPen(pen)¶
+
+
Sets the pen used to draw lines between points.
+
pen can be a QPen or any argument accepted by pyqtgraph.mkPen()
+
+
+ +
+
+setShadowPen(pen)¶
+
+
Sets the shadow pen used to draw lines between points (this is for enhancing contrast or +emphacizing data).
+
This line is drawn behind the primary pen (see setPen()) +and should generally be assigned greater width than the primary pen.
+
pen can be a QPen or any argument accepted by pyqtgraph.mkPen()
+
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

Pyqtgraph’s Graphics Items

+

Next topic

+

PlotCurveItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/plotitem.html b/documentation/build/html/graphicsItems/plotitem.html new file mode 100644 index 00000000..9c07f964 --- /dev/null +++ b/documentation/build/html/graphicsItems/plotitem.html @@ -0,0 +1,245 @@ + + + + + + + + + PlotItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

PlotItem¶

+
+
+class pyqtgraph.PlotItem(parent=None, name=None, labels=None, title=None, **kargs)¶
+
+
+__init__(parent=None, name=None, labels=None, title=None, **kargs)¶
+
+ +
+
+addAvgCurve(curve)¶
+

Add a single curve into the pool of curves averaged together

+
+ +
+
+enableAutoScale()¶
+

Enable auto-scaling. The plot will continuously scale to fit the boundaries of its data.

+
+ +
+
+linkXChanged(plot)¶
+

Called when a linked plot has changed its X scale

+
+ +
+
+linkYChanged(plot)¶
+

Called when a linked plot has changed its Y scale

+
+ +
+
+plot(*args, **kargs)¶
+

Add and return a new plot. +See PlotDataItem.__init__ for data arguments

+
+
Extra allowed arguments are:
+
clear - clear all plots before displaying new data +params - meta-parameters to associate with this data
+
+
+ +
+
+setLabel(axis, text=None, units=None, unitPrefix=None, **args)¶
+

Set the label for an axis. Basic HTML formatting is allowed. +Arguments:

+
+

axis - must be one of ‘left’, ‘bottom’, ‘right’, or ‘top’ +text - text to display along the axis. HTML allowed. +units - units to display after the title. If units are given,

+
+then an SI prefix will be automatically appended +and the axis values will be scaled accordingly. +(ie, use ‘V’ instead of ‘mV’; ‘m’ will be added automatically)
+
+
+ +
+
+setTitle(title=None, **args)¶
+

Set the title of the plot. Basic HTML formatting is allowed. +If title is None, then the title will be hidden.

+
+ +
+ +

Link this plot’s X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)

+
+ +
+ +

Link this plot’s Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)

+
+ +
+
+showAxis(axis, show=True)¶
+

Show or hide one of the plot’s axes. +axis must be one of ‘left’, ‘bottom’, ‘right’, or ‘top’

+
+ +
+
+showLabel(axis, show=True)¶
+

Show or hide one of the plot’s axis labels (the axis itself will be unaffected). +axis must be one of ‘left’, ‘bottom’, ‘right’, or ‘top’

+
+ +
+
+sigRangeChanged¶
+

Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox.

+
+ +
+
+updatePlotList()¶
+

Update the list of all plotWidgets in the “link” combos

+
+ +
+
+updateXScale()¶
+

Set plot to autoscale or not depending on state of radio buttons

+
+ +
+
+updateYScale(b=False)¶
+

Set plot to autoscale or not depending on state of radio buttons

+
+ +
+
+viewGeometry()¶
+

return the screen geometry of the viewbox

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ScatterPlotItem

+

Next topic

+

ImageItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/roi.html b/documentation/build/html/graphicsItems/roi.html new file mode 100644 index 00000000..111304f4 --- /dev/null +++ b/documentation/build/html/graphicsItems/roi.html @@ -0,0 +1,185 @@ + + + + + + + + + ROI — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ROI¶

+
+
+class pyqtgraph.ROI(pos, size=Point(1.000000, 1.000000), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True)¶
+

Generic region-of-interest widget. +Can be used for implementing many types of selection box with rotate/translate/scale handles.

+
+
+__init__(pos, size=Point(1.000000, 1.000000), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True)¶
+
+ +
+
+getArrayRegion(data, img, axes=(0, 1))¶
+

Use the position of this ROI relative to an imageItem to pull a slice from an array.

+
+ +
+
+getArraySlice(data, img, axes=(0, 1), returnSlice=True)¶
+

Return a tuple of slice objects that can be used to slice the region from data covered by this ROI. +Also returns the transform which maps the ROI into data coordinates.

+

If returnSlice is set to False, the function returns a pair of tuples with the values that would have +been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))

+
+ +
+
+getGlobalTransform(relativeTo=None)¶
+

Return global transformation (rotation angle+translation) required to move from relative state to current state. If relative state isn’t specified, +then we use the state of the ROI when mouse is pressed.

+
+ +
+
+getLocalHandlePositions(index=None)¶
+

Returns the position of a handle in ROI coordinates

+
+ +
+
+handleChange()¶
+

The state of the ROI has changed; redraw if needed.

+
+ +
+
+saveState()¶
+

Return the state of the widget in a format suitable for storing to disk.

+
+ +
+
+translate(*args, **kargs)¶
+

accepts either (x, y, snap) or ([x,y], snap) as arguments

+
+
snap can be:
+
None (default): use self.translateSnap and self.snapSize to determine whether/how to snap +False: do no snap +Point(w,h) snap to rectangular grid with spacing (w,h) +True: snap using self.snapSize (and ignoring self.translateSnap)
+
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

InfiniteLine

+

Next topic

+

GraphicsLayout

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/scalebar.html b/documentation/build/html/graphicsItems/scalebar.html new file mode 100644 index 00000000..2a198eb2 --- /dev/null +++ b/documentation/build/html/graphicsItems/scalebar.html @@ -0,0 +1,131 @@ + + + + + + + + + ScaleBar — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ScaleBar¶

+
+
+class pyqtgraph.ScaleBar(size, width=5, color=(100, 100, 255))¶
+

Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view.

+
+
+__init__(size, width=5, color=(100, 100, 255))¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

GridItem

+

Next topic

+

LabelItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/scatterplotitem.html b/documentation/build/html/graphicsItems/scatterplotitem.html new file mode 100644 index 00000000..4333b0c4 --- /dev/null +++ b/documentation/build/html/graphicsItems/scatterplotitem.html @@ -0,0 +1,171 @@ + + + + + + + + + ScatterPlotItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ScatterPlotItem¶

+
+
+class pyqtgraph.ScatterPlotItem(spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=7, symbol=None, identical=False, data=None)¶
+
+
+__init__(spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=7, symbol=None, identical=False, data=None)¶
+
+
Arguments:
+
+
spots: list of dicts. Each dict specifies parameters for a single spot:
+
{‘pos’: (x,y), ‘size’, ‘pen’, ‘brush’, ‘symbol’}
+
+

x,y: array of x,y values. Alternatively, specify spots[‘pos’] = (x,y) +pxMode: If True, spots are always the same size regardless of scaling, and size is given in px.

+
+Otherwise, size is in scene coordinates and the spots scale with the view.
+
+
identical: If True, all spots are forced to look identical.
+
This can result in performance enhancement.
+
symbol can be one of:
+
‘o’ circle +‘s’ square +‘t’ triangle +‘d’ diamond +‘+’ plus
+
+
+
+
+ +
+
+setPoints(spots=None, x=None, y=None, data=None)¶
+

Remove all existing points in the scatter plot and add a new set. +Arguments:

+
+
+
spots - list of dicts specifying parameters for each spot
+
[ {‘pos’: (x,y), ‘pen’: ‘r’, ...}, ...]
+
x, y - arrays specifying location of spots to add.
+
all other parameters (pen, symbol, etc.) will be set to the default +values for this scatter plot. +these arguments are IGNORED if ‘spots’ is specified
+
data - list of arbitrary objects to be assigned to spot.data for each spot
+
(this is useful for identifying spots that are clicked on)
+
+
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

PlotCurveItem

+

Next topic

+

PlotItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/uigraphicsitem.html b/documentation/build/html/graphicsItems/uigraphicsitem.html new file mode 100644 index 00000000..36ac517d --- /dev/null +++ b/documentation/build/html/graphicsItems/uigraphicsitem.html @@ -0,0 +1,179 @@ + + + + + + + + + UIGraphicsItem — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

UIGraphicsItem¶

+
+
+class pyqtgraph.UIGraphicsItem(bounds=None, parent=None)¶
+

Base class for graphics items with boundaries relative to a GraphicsView or ViewBox. +The purpose of this class is to allow the creation of GraphicsItems which live inside +a scalable view, but whose boundaries will always stay fixed relative to the view’s boundaries. +For example: GridItem, InfiniteLine

+

The view can be specified on initialization or it can be automatically detected when the item is painted.

+

NOTE: Only the item’s boundingRect is affected; the item is not transformed in any way. Use viewRangeChanged +to respond to changes in the view.

+
+
+__init__(bounds=None, parent=None)¶
+
+
Initialization Arguments:
+

#view: The view box whose bounds will be used as a reference vor this item’s bounds +bounds: QRectF with coordinates relative to view box. The default is QRectF(0,0,1,1),

+
+which means the item will have the same bounds as the view.
+
+
+
+ +
+
+mouseShape()¶
+

Return the shape of this item after expanding by 2 pixels

+
+ +
+
+realBoundingRect()¶
+

Called by ViewBox for determining the auto-range bounds. +If the height or with of the rect is 0, that dimension will be ignored. +By default, UIGraphicsItems are excluded from autoRange by returning +a zero-size rect.

+
+ +
+
+setNewBounds()¶
+

Update the item’s bounding rect to match the viewport

+
+ +
+
+viewChangedEvent()¶
+

Called whenever the view coordinates have changed. +This is a good method to override if you want to respond to change of coordinates.

+
+ +
+
+viewRangeChanged()¶
+

Called when the view widget/viewbox is resized/rescaled

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

GraphicsWidget

+

Next topic

+

Pyqtgraph’s Widgets

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/viewbox.html b/documentation/build/html/graphicsItems/viewbox.html new file mode 100644 index 00000000..044c71cf --- /dev/null +++ b/documentation/build/html/graphicsItems/viewbox.html @@ -0,0 +1,227 @@ + + + + + + + + + ViewBox — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ViewBox¶

+
+
+class pyqtgraph.ViewBox(parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False)¶
+

Box that allows internal scaling/panning of children by mouse drag. +Not really compatible with GraphicsView having the same functionality.

+
+
+__init__(parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False)¶
+
+ +
+
+childTransform()¶
+

Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. +(This maps from inside the viewbox to outside)

+
+ +
+
+childrenBoundingRect(item=None)¶
+

Return the bounding rect of all children. Returns None if there are no bounded children

+
+ +
+
+itemBoundingRect(item)¶
+

Return the bounding rect of the item in view coordinates

+
+ +
+
+keyPressEvent(ev)¶
+

This routine should capture key presses in the current view box. +Key presses are used only when self.useLeftButtonPan is false +The following events are implemented: +ctrl-A : zooms out to the default “full” view of the plot +ctrl-+ : moves forward in the zooming stack (if it exists) +ctrl– : moves backward in the zooming stack (if it exists)

+
+ +
+
+mapFromView(obj)¶
+

Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox

+
+ +
+
+mapSceneToView(obj)¶
+

Maps from scene coordinates to the coordinate system displayed inside the ViewBox

+
+ +
+
+mapToView(obj)¶
+

Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox

+
+ +
+
+mapViewToScene(obj)¶
+

Maps from the coordinate system displayed inside the ViewBox to scene coordinates

+
+ +
+
+scaleBy(s, center=None)¶
+

Scale by s around given center point (or center of view)

+
+ +
+
+setAspectLocked(lock=True, ratio=1)¶
+

If the aspect ratio is locked, view scaling is always forced to be isotropic. +By default, the ratio is set to 1; x and y both have the same scaling. +This ratio can be overridden (width/height), or use None to lock in the current ratio.

+
+ +
+
+setRange(ax, minimum=None, maximum=None, padding=0.02, update=True)¶
+

Set the visible range of the ViewBox. +Can be called with a QRectF:

+
+setRange(QRectF(x, y, w, h))
+
+
Or with axis, min, max:
+
setRange(0, xMin, xMax) +setRange(1, yMin, yMax)
+
+
+ +
+
+targetRect()¶
+

Return the region which has been requested to be visible. +(this is not necessarily the same as the region that is actually visible)

+
+ +
+
+viewRect()¶
+

Return a QRectF bounding the region visible within the ViewBox

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ImageItem

+

Next topic

+

LinearRegionItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/vtickgroup.html b/documentation/build/html/graphicsItems/vtickgroup.html new file mode 100644 index 00000000..d84e5bdb --- /dev/null +++ b/documentation/build/html/graphicsItems/vtickgroup.html @@ -0,0 +1,132 @@ + + + + + + + + + VTickGroup — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

VTickGroup¶

+
+
+class pyqtgraph.VTickGroup(xvals=None, yrange=None, pen=None)¶
+

Draws a set of tick marks which always occupy the same vertical range of the view, +but have x coordinates relative to the data within the view.

+
+
+__init__(xvals=None, yrange=None, pen=None)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

LabelItem

+

Next topic

+

GradientEditorItem

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/graphicswindow.html b/documentation/build/html/graphicswindow.html new file mode 100644 index 00000000..87026901 --- /dev/null +++ b/documentation/build/html/graphicswindow.html @@ -0,0 +1,123 @@ + + + + + + + + + Basic display widgets — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

Basic display widgets¶

+
+
    +
  • GraphicsWindow
  • +
  • GraphicsView
  • +
  • GraphicsLayoutItem
  • +
  • ViewBox
  • +
+
+
+ + +
+
+
+
+
+

Previous topic

+

Region-of-interest controls

+

Next topic

+

Rapid GUI prototyping

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/how_to_use.html b/documentation/build/html/how_to_use.html new file mode 100644 index 00000000..a95e0688 --- /dev/null +++ b/documentation/build/html/how_to_use.html @@ -0,0 +1,161 @@ + + + + + + + + + How to use pyqtgraph — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

How to use pyqtgraph¶

+

There are a few suggested ways to use pyqtgraph:

+
    +
  • From the interactive shell (python -i, ipython, etc)
  • +
  • Displaying pop-up windows from an application
  • +
  • Embedding widgets in a PyQt application
  • +
+
+

Command-line use¶

+

Pyqtgraph makes it very easy to visualize data from the command line. Observe:

+
import pyqtgraph as pg
+pg.plot(data)   # data can be a list of values or a numpy array
+
+
+

The example above would open a window displaying a line plot of the data given. I don’t think it could reasonably be any simpler than that. The call to pg.plot returns a handle to the plot widget that is created, allowing more data to be added to the same window.

+

Further examples:

+
pw = pg.plot(xVals, yVals, pen='r')  # plot x vs y in red
+pw.plot(xVals, yVals2, pen='b')
+
+win = pg.GraphicsWindow()  # Automatically generates grids with multiple items
+win.addPlot(data1, row=0, col=0)
+win.addPlot(data2, row=0, col=1)
+win.addPlot(data3, row=1, col=0, colspan=2)
+
+pg.show(imageData)  # imageData must be a numpy array with 2 to 4 dimensions
+
+
+

We’re only scratching the surface here–these functions accept many different data formats and options for customizing the appearance of your data.

+
+
+

Displaying windows from within an application¶

+

While I consider this approach somewhat lazy, it is often the case that ‘lazy’ is indistinguishable from ‘highly efficient’. The approach here is simply to use the very same functions that would be used on the command line, but from within an existing application. I often use this when I simply want to get a immediate feedback about the state of data in my application without taking the time to build a user interface for it.

+
+
+

Embedding widgets inside PyQt applications¶

+

For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: PlotWidget, ImageView, GraphicsView, GraphicsLayoutWidget. Pyqtgraph’s widgets can be included in Designer’s ui files via the “Promote To...” functionality.

+
+
+ + +
+
+
+
+
+

Table Of Contents

+ + +

Previous topic

+

Introduction

+

Next topic

+

Plotting in pyqtgraph

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/images.html b/documentation/build/html/images.html new file mode 100644 index 00000000..81213b28 --- /dev/null +++ b/documentation/build/html/images.html @@ -0,0 +1,135 @@ + + + + + + + + + Displaying images and video — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

Displaying images and video¶

+

Pyqtgraph displays 2D numpy arrays as images and provides tools for determining how to translate between the numpy data type and RGB values on the screen. If you want to display data from common image and video file formats, you will need to load the data first using another library (PIL works well for images and built-in numpy conversion).

+

The easiest way to display 2D or 3D data is using the pyqtgraph.image() function:

+
import pyqtgraph as pg
+pg.image(imageData)
+
+
+

This function will accept any floating-point or integer data types and displays a single ImageView widget containing your data. This widget includes controls for determining how the image data will be converted to 32-bit RGBa values. Conversion happens in two steps (both are optional):

+
    +
  1. Scale and offset the data (by selecting the dark/light levels on the displayed histogram)
  2. +
  3. Convert the data to color using a lookup table (determined by the colors shown in the gradient editor)
  4. +
+

If the data is 3D (time, x, y), then a time axis will be shown with a slider that can set the currently displayed frame. (if the axes in your data are ordered differently, use numpy.transpose to rearrange them)

+

There are a few other methods for displaying images as well:

+
    +
  • The ImageView class can also be instantiated directly and embedded in Qt applications.
  • +
  • Instances of ImageItem can be used inside a GraphicsView.
  • +
  • For higher performance, use RawImageWidget.
  • +
+

Any of these classes are acceptable for displaying video by calling setImage() to display a new frame. To increase performance, the image processing system uses scipy.weave to produce compiled libraries. If your computer has a compiler available, weave will automatically attempt to build the libraries it needs on demand. If this fails, then the slower pure-python methods will be used instead.

+

For more information, see the classes listed above and the ‘VideoSpeedTest’, ‘ImageItem’, ‘ImageView’, and ‘HistogramLUT’ Examples.

+
+ + +
+
+
+
+
+

Previous topic

+

Plotting in pyqtgraph

+

Next topic

+

Line, Fill, and Color

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/index.html b/documentation/build/html/index.html new file mode 100644 index 00000000..25db4fd6 --- /dev/null +++ b/documentation/build/html/index.html @@ -0,0 +1,160 @@ + + + + + + + + + Welcome to the documentation for pyqtgraph 1.8 — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/build/html/introduction.html b/documentation/build/html/introduction.html new file mode 100644 index 00000000..fc156976 --- /dev/null +++ b/documentation/build/html/introduction.html @@ -0,0 +1,163 @@ + + + + + + + + + Introduction — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

Introduction¶

+
+

What is pyqtgraph?¶

+

Pyqtgraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer).

+

Pyqtgraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt’s GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort.

+

It is known to run on Linux, Windows, and OSX

+
+
+

What can it do?¶

+

Amongst the core features of pyqtgraph are:

+
    +
  • Basic data visualization primitives: Images, line and scatter plots
  • +
  • Fast enough for realtime update of video/plot data
  • +
  • Interactive scaling/panning, averaging, FFTs, SVG/PNG export
  • +
  • Widgets for marking/selecting plot regions
  • +
  • Widgets for marking/selecting image region-of-interest and automatically slicing multi-dimensional image data
  • +
  • Framework for building customized image region-of-interest widgets
  • +
  • Docking system that replaces/complements Qt’s dock system to allow more complex (and more predictable) docking arrangements
  • +
  • ParameterTree widget for rapid prototyping of dynamic interfaces (Similar to the property trees in Qt Designer and many other applications)
  • +
+
+
+

Examples¶

+

Pyqtgraph includes an extensive set of examples that can be accessed by running:

+
import pyqtgraph.examples
+pyqtgraph.examples.run()
+
+
+

This will start a launcher with a list of available examples. Select an item from the list to view its source code and double-click an item to run the example.

+
+
+

How does it compare to...¶

+
    +
  • matplotlib: For plotting and making publication-quality graphics, matplotlib is far more mature than pyqtgraph. However, matplotlib is also much slower and not suitable for applications requiring realtime update of plots/video or rapid interactivity. It also does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph.
  • +
  • pyqwt5: pyqwt is generally more mature than pyqtgraph for plotting and is about as fast. The major differences are 1) pyqtgraph is written in pure python, so it is somewhat more portable than pyqwt, which often lags behind pyqt in development (and can be a pain to install on some platforms) and 2) like matplotlib, pyqwt does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph.
  • +
+

(My experience with these libraries is somewhat outdated; please correct me if I am wrong here)

+
+
+ + +
+
+
+
+
+

Table Of Contents

+ + +

Previous topic

+

Welcome to the documentation for pyqtgraph 1.8

+

Next topic

+

How to use pyqtgraph

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/objects.inv b/documentation/build/html/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..aa34069b2bd1ed05c16836f5f8c7044c9f2be7df GIT binary patch literal 2225 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 literal 0 HcmV?d00001 diff --git a/documentation/build/html/parametertree.html b/documentation/build/html/parametertree.html new file mode 100644 index 00000000..211df78d --- /dev/null +++ b/documentation/build/html/parametertree.html @@ -0,0 +1,123 @@ + + + + + + + + + Rapid GUI prototyping — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

Rapid GUI prototyping¶

+
+
    +
  • parametertree
  • +
  • dockarea
  • +
  • flowchart
  • +
  • canvas
  • +
+
+
+ + +
+
+
+
+
+

Previous topic

+

Basic display widgets

+

Next topic

+

API Reference

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/plotting.html b/documentation/build/html/plotting.html new file mode 100644 index 00000000..3855cbec --- /dev/null +++ b/documentation/build/html/plotting.html @@ -0,0 +1,217 @@ + + + + + + + + + Plotting in pyqtgraph — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

Plotting in pyqtgraph¶

+

There are a few basic ways to plot data in pyqtgraph:

+ ++++ + + + + + + + + + + + + + + +
pyqtgraph.plot()Create a new plot window showing your data
PlotWidget.plot()Add a new set of data to an existing plot widget
PlotItem.plot()Add a new set of data to an existing plot widget
GraphicsWindow.addPlot()Add a new plot to a grid of plots
+

All of these will accept the same basic arguments which control how the plot data is interpreted and displayed:

+
    +
  • x - Optional X data; if not specified, then a range of integers will be generated automatically.
  • +
  • y - Y data.
  • +
  • pen - The pen to use when drawing plot lines, or None to disable lines.
  • +
  • symbol - A string describing the shape of symbols to use for each point. Optionally, this may also be a sequence of strings with a different symbol for each point.
  • +
  • symbolPen - The pen (or sequence of pens) to use when drawing the symbol outline.
  • +
  • symbolBrush - The brush (or sequence of brushes) to use when filling the symbol.
  • +
  • fillLevel - Fills the area under the plot curve to this Y-value.
  • +
  • brush - The brush to use when filling under the curve.
  • +
+

See the ‘plotting’ example for a demonstration of these arguments.

+

All of the above functions also return handles to the objects that are created, allowing the plots and data to be further modified.

+
+

Organization of Plotting Classes¶

+

There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. Pyqtgraph is based heavily on Qt’s GraphicsView framework–if you are not already familiar with this, it’s worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView.

+
    +
  • +
    Data Classes (all subclasses of QGraphicsItem)
    +
      +
    • PlotCurveItem - Displays a plot line given x,y data
    • +
    • ScatterPlotItem - Displays points given x,y data
    • +
    • PlotDataItem - Combines PlotCurveItem and ScatterPlotItem. The plotting functions discussed above create objects of this type.
    • +
    +
    +
    +
  • +
  • +
    Container Classes (subclasses of QGraphicsItem; contain other QGraphicsItem objects and must be viewed from within a GraphicsView)
    +
      +
    • PlotItem - Contains a ViewBox for displaying data as well as AxisItems and labels for displaying the axes and title. This is a QGraphicsItem subclass and thus may only be used from within a GraphicsView
    • +
    • GraphicsLayoutItem - QGraphicsItem subclass which displays a grid of items. This is used to display multiple PlotItems together.
    • +
    • ViewBox - A QGraphicsItem subclass for displaying data. The user may scale/pan the contents of a ViewBox using the mouse. Typically all PlotData/PlotCurve/ScatterPlotItems are displayed from within a ViewBox.
    • +
    • AxisItem - Displays axis values, ticks, and labels. Most commonly used with PlotItem.
    • +
    +
    +
    +
  • +
  • +
    Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs)
    +
      +
    • PlotWidget - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget.
    • +
    • GraphicsLayoutWidget - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget.
    • +
    +
    +
    +
  • +
+_images/plottingClasses.png +
+
+

Examples¶

+

See the ‘plotting’ and ‘PlotWidget’ examples included with pyqtgraph for more information.

+

Show x,y data as scatter plot:

+
import pyqtgraph as pg
+import numpy as np
+x = np.random.normal(size=1000)
+y = np.random.normal(size=1000)
+pg.plot(x, y, pen=None, symbol='o')  ## setting pen=None disables line drawing
+
+
+

Create/show a plot widget, display three data curves:

+
import pyqtgraph as pg
+import numpy as np
+x = np.arange(1000)
+y = np.random.normal(size=(3, 1000))
+plotWidget = pg.plot(title="Three plot curves")
+for i in range(3):
+    plotWidget.plot(x, y[i], pen=(i,3))  ## setting pen=(i,3) automaticaly creates three different-colored pens
+
+
+
+
+ + +
+
+
+
+
+

Table Of Contents

+ + +

Previous topic

+

How to use pyqtgraph

+

Next topic

+

Displaying images and video

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/py-modindex.html b/documentation/build/html/py-modindex.html new file mode 100644 index 00000000..50a684ee --- /dev/null +++ b/documentation/build/html/py-modindex.html @@ -0,0 +1,118 @@ + + + + + + + + + Python Module Index — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ + +

Python Module Index

+ +
+ p +
+ + + + + + + + + + + + + +
 
+ p
+ pyqtgraph +
    + pyqtgraph.dockarea +
    + pyqtgraph.parametertree +
+ + +
+
+
+
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/region_of_interest.html b/documentation/build/html/region_of_interest.html new file mode 100644 index 00000000..85b39832 --- /dev/null +++ b/documentation/build/html/region_of_interest.html @@ -0,0 +1,140 @@ + + + + + + + + + Region-of-interest controls — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

Region-of-interest controls¶

+
+

Slicing Multidimensional Data¶

+
+
+

Linear Selection and Marking¶

+
+
+

2D Selection and Marking¶

+
    +
  • translate / rotate / scale
  • +
  • highly configurable control handles
  • +
  • automated data slicing
  • +
  • linearregion, infiniteline
  • +
+
+
+ + +
+
+
+
+
+

Table Of Contents

+ + +

Previous topic

+

Line, Fill, and Color

+

Next topic

+

Basic display widgets

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/search.html b/documentation/build/html/search.html new file mode 100644 index 00000000..e734bc16 --- /dev/null +++ b/documentation/build/html/search.html @@ -0,0 +1,102 @@ + + + + + + + + + Search — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + + +
+
+
+
+ +

Search

+
+ +

+ Please activate JavaScript to enable the search + functionality. +

+
+

+ From here you can search these documents. Enter your search + words into the box below and click "search". Note that the search + function will automatically search for all of the words. Pages + containing fewer words won't appear in the result list. +

+
+ + + +
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/searchindex.js b/documentation/build/html/searchindex.js new file mode 100644 index 00000000..5c07321b --- /dev/null +++ b/documentation/build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({objects:{"pyqtgraph.UIGraphicsItem":{setNewBounds:[10,2,1],viewRangeChanged:[10,2,1],viewChangedEvent:[10,2,1],"__init__":[10,2,1],mouseShape:[10,2,1],realBoundingRect:[10,2,1]},"pyqtgraph.PlotCurveItem":{"__init__":[15,2,1],setData:[15,2,1]},"pyqtgraph.PlotItem":{setXLink:[14,2,1],plot:[14,2,1],setLabel:[14,2,1],enableAutoScale:[14,2,1],linkYChanged:[14,2,1],linkXChanged:[14,2,1],showLabel:[14,2,1],setTitle:[14,2,1],setYLink:[14,2,1],updateXScale:[14,2,1],updateYScale:[14,2,1],viewGeometry:[14,2,1],showAxis:[14,2,1],addAvgCurve:[14,2,1],updatePlotList:[14,2,1],sigRangeChanged:[14,4,1],"__init__":[14,2,1]},"pyqtgraph.ScatterPlotItem":{setPoints:[9,2,1],"__init__":[9,2,1]},"pyqtgraph.ScaleBar":{"__init__":[57,2,1]},"pyqtgraph.HistogramLUTWidget":{"__init__":[40,2,1]},"pyqtgraph.HistogramLUTItem":{"__init__":[41,2,1]},"pyqtgraph.GradientLegend":{setLabels:[32,2,1],"__init__":[32,2,1]},"pyqtgraph.DataTreeWidget":{"__init__":[53,2,1],setData:[53,2,1]},"pyqtgraph.CurveArrow":{"__init__":[33,2,1]},"pyqtgraph.GraphicsView":{scaleToImage:[23,2,1],viewRect:[23,2,1],pixelSize:[23,2,1],setCentralWidget:[23,2,1],"__init__":[23,2,1]},"pyqtgraph.GridItem":{"__init__":[19,2,1]},"pyqtgraph.PlotWidget":{"__init__":[7,2,1]},"pyqtgraph.AxisItem":{"__init__":[26,2,1],setScale:[26,2,1],setGrid:[26,2,1]},"pyqtgraph.ROI":{saveState:[24,2,1],getGlobalTransform:[24,2,1],getLocalHandlePositions:[24,2,1],getArrayRegion:[24,2,1],handleChange:[24,2,1],translate:[24,2,1],getArraySlice:[24,2,1],"__init__":[24,2,1]},"pyqtgraph.CheckTable":{"__init__":[56,2,1]},"pyqtgraph.LinearRegionItem":{getRegion:[45,2,1],"__init__":[45,2,1]},"pyqtgraph.PlotDataItem":{setShadowPen:[1,2,1],setData:[1,2,1],"__init__":[1,2,1],setPen:[1,2,1]},"pyqtgraph.GraphicsWidget":{"__init__":[29,2,1]},"pyqtgraph.InfiniteLine":{setAngle:[47,2,1],setBounds:[47,2,1],"__init__":[47,2,1]},"pyqtgraph.JoystickButton":{"__init__":[44,2,1]},"pyqtgraph.GradientWidget":{"__init__":[22,2,1]},"pyqtgraph.TableWidget":{iteratorFn:[0,2,1],appendData:[0,2,1],copy:[0,2,1],"__init__":[0,2,1]},"pyqtgraph.ImageView":{jumpFrames:[42,2,1],timeIndex:[42,2,1],"__init__":[42,2,1],setImage:[42,2,1]},"pyqtgraph.ArrowItem":{"__init__":[11,2,1]},"pyqtgraph.CurvePoint":{"__init__":[34,2,1]},"pyqtgraph.ColorButton":{"__init__":[51,2,1]},"pyqtgraph.ImageItem":{setPxMode:[3,2,1],setImage:[3,2,1],getHistogram:[3,2,1],setLookupTable:[3,2,1],pixelSize:[3,2,1],setLevels:[3,2,1],"__init__":[3,2,1]},"pyqtgraph.ViewBox":{targetRect:[4,2,1],mapFromView:[4,2,1],mapToView:[4,2,1],itemBoundingRect:[4,2,1],mapViewToScene:[4,2,1],viewRect:[4,2,1],keyPressEvent:[4,2,1],scaleBy:[4,2,1],childrenBoundingRect:[4,2,1],childTransform:[4,2,1],mapSceneToView:[4,2,1],setAspectLocked:[4,2,1],"__init__":[4,2,1],setRange:[4,2,1]},"pyqtgraph.VTickGroup":{"__init__":[28,2,1]},"pyqtgraph.RawImageWidget":{"__init__":[37,2,1],setImage:[37,2,1]},"pyqtgraph.VerticalLabel":{"__init__":[13,2,1]},"pyqtgraph.TreeWidget":{itemMoving:[6,2,1],"__init__":[6,2,1]},"pyqtgraph.GradientEditorItem":{getLookupTable:[49,2,1],"__init__":[49,2,1]},"pyqtgraph.ProgressDialog":{"__init__":[5,2,1]},"pyqtgraph.GraphicsObject":{viewTransform:[8,2,1],getBoundingParents:[8,2,1],pixelVectors:[8,2,1],viewRect:[8,2,1],getViewWidget:[8,2,1],getViewBox:[8,2,1],pixelLength:[8,2,1],deviceTransform:[8,2,1],"__init__":[8,2,1]},pyqtgraph:{VTickGroup:[28,3,1],GraphicsWidget:[29,3,1],affineSlice:[18,1,1],ScaleBar:[57,3,1],image:[18,1,1],mkBrush:[18,1,1],PlotDataItem:[1,3,1],GraphicsObject:[8,3,1],ImageItem:[3,3,1],LinearRegionItem:[45,3,1],ImageView:[42,3,1],FileDialog:[48,3,1],HistogramLUTWidget:[40,3,1],CheckTable:[56,3,1],MultiPlotWidget:[27,3,1],mkPen:[18,1,1],plot:[18,1,1],InfiniteLine:[47,3,1],HistogramLUTItem:[41,3,1],PlotWidget:[7,3,1],GradientWidget:[22,3,1],GridItem:[19,3,1],GradientEditorItem:[49,3,1],GradientLegend:[32,3,1],AxisItem:[26,3,1],ViewBox:[4,3,1],dockarea:[43,0,0],ArrowItem:[11,3,1],hsvColor:[18,1,1],PlotItem:[14,3,1],colorStr:[18,1,1],GraphicsLayout:[46,3,1],siEval:[18,1,1],LabelItem:[16,3,1],ROI:[24,3,1],JoystickButton:[44,3,1],CurveArrow:[33,3,1],CurvePoint:[34,3,1],SpinBox:[31,3,1],mkColor:[18,1,1],GraphicsLayoutWidget:[54,3,1],PlotCurveItem:[15,3,1],ButtonItem:[21,3,1],TreeWidget:[6,3,1],siFormat:[18,1,1],parametertree:[35,0,0],VerticalLabel:[13,3,1],intColor:[18,1,1],ColorButton:[51,3,1],RawImageWidget:[37,3,1],DataTreeWidget:[53,3,1],GraphicsView:[23,3,1],UIGraphicsItem:[10,3,1],siScale:[18,1,1],TableWidget:[0,3,1],ScatterPlotItem:[9,3,1],ProgressDialog:[5,3,1],colorTuple:[18,1,1]},"pyqtgraph.SpinBox":{setProperty:[31,2,1],setValue:[31,2,1],editingFinishedEvent:[31,2,1],"__init__":[31,2,1],interpret:[31,2,1]},"pyqtgraph.GraphicsLayoutWidget":{"__init__":[54,2,1]},"pyqtgraph.LabelItem":{setText:[16,2,1],"__init__":[16,2,1],setAttr:[16,2,1]},"pyqtgraph.ButtonItem":{"__init__":[21,2,1]},"pyqtgraph.MultiPlotWidget":{"__init__":[27,2,1]},"pyqtgraph.FileDialog":{"__init__":[48,2,1]},"pyqtgraph.GraphicsLayout":{nextCol:[46,2,1],nextRow:[46,2,1],"__init__":[46,2,1]}},terms:{roi:[18,24,52,50],all:[1,9,3,4,18,55,14,25],code:[20,18],gradienteditoritem:[52,49,50],edg:45,orthogon:18,osx:20,skip:3,global:24,makeargb:[37,3],rapid:[20,17,39,3,31],prefix:[18,14,31],subclass:[2,25,50],screen:[12,14,3,23],follow:[18,30,4],disk:24,children:4,row:[46,55],hsva:18,whose:10,setlabel:[14,32],middl:23,depend:14,decim:[1,31],intermedi:31,linkxchang:14,mapscenetoview:4,setpen:1,worth:25,sent:37,sourc:[20,18],everi:1,string:[16,0,18,30,25],delaysign:31,fals:[26,1,9,14,4,5,18,31,6,53,23,24,47,15],mous:[24,15,25,23,4],"1px":1,veri:[37,55],affect:10,setcentralwidget:23,exact:23,getarrayregion:[18,24],dim:18,imagedata:[12,55],level:[12,42,3],button:[5,14,23,21],scalabl:10,list:[0,1,9,20,18,8,55,14,12,53],griditem:[19,10,52,50],gethistogram:3,item:[26,17,1,3,4,20,46,50,6,8,52,10,23,55,14,25],vector:[8,18,23],dockarea:[43,2,39,52],refer:[17,52,10,34],arang:25,dimens:[55,18,10],properti:[16,20,33,34],slower:[12,20],direct:[8,18],consequ:50,zero:10,video:[17,3,20,12,37,42],pass:14,further:[55,25],getarrayslic:24,translatesnap:24,click:[20,9],append:14,even:[37,18],index:[17,18,42,30,6,34,33,24],what:[20,17,18,19],hide:14,appear:[26,1,55,32],compar:[20,17],section:18,current:[26,3,4,46,31,12,24],clipboard:0,rgba:[12,18],"new":[1,9,14,12,47,25],"public":20,contrast:1,qgraphicsscen:[23,50],widget:[17,18,2,53,20,42,52,31,6,7,8,27,10,23,36,12,37,24,25,55],full:[23,4],gener:[16,1,2,20,46,18,8,55,37,24,25],whitelevel:3,len:[1,18],tangent:34,uigraphicsitem:[52,10,50],address:8,locat:[34,18,9,11],along:[26,34,14,32],becom:18,modifi:25,legend:1,valu:[16,26,1,9,3,45,42,18,31,32,8,30,55,14,12,24,47,25],wait:5,invis:18,solid:[1,18],convert:[12,8,18],purpos:10,convers:[12,18,52],ahead:42,across:18,larger:18,step:[12,18,3,31],precis:18,within:[17,18,4,28,8,55,19,25],chang:[26,31,10,14,24,47],commonli:[20,25],portabl:20,overrid:[42,10],diamet:1,configur:[26,38],regardless:9,parallelepip:18,labelitem:[16,52,50],extra:[37,1,14,31],appli:34,modul:[43,17,2,52,35],getlookupt:49,setaspectlock:4,api:[17,52],visibl:[8,23,4],ax1stop:24,colortupl:18,ymin:4,select:[0,18,17,20,31,38,12,24],highli:[20,18,55,38],plot:[26,17,1,9,4,20,18,45,55,14,15,25],hexadecim:18,from:[26,17,18,4,20,8,55,12,10,24,25],describ:25,would:[26,1,24,55,18],minval:[5,18],mkbrush:[18,30],regist:14,two:[12,50],next:[46,32],few:[12,8,55,25],live:10,call:[18,4,46,6,10,14,12,55,25],graphicsitem:[26,1,21,50,34,10],recommend:18,dict:[0,1,9,53,32],type:[0,18,31,12,24,25],useopengl:23,more:[18,3,20,30,55,12,37,25],mkpen:[1,30,18],graphicslayout:[46,52,50],intcolor:[18,30],qtreewidget:6,pyqwt:20,relat:25,ital:16,enhanc:[1,9],flag:16,accept:[16,1,3,18,32,6,30,55,12,24,25],particular:20,known:20,central:23,effort:20,cach:8,must:[18,6,55,14,37,25],none:[1,3,4,5,6,7,8,9,10,16,18,21,22,23,24,25,26,27,28,44,31,33,34,14,37,47,40,41,42,45,46,51,53,54,15],graphic:[17,18,20,50,52,10,14,25],xvalu:1,getviewwidget:8,outlin:[1,25],invlov:25,column:[46,1,56],kwarg:31,can:[0,1,2,3,4,8,10,12,17,30,20,23,24,26,9,33,34,14,47,45,50,55],graphicswindow:[36,55,25],progressdialog:[5,2,52],scatter:[20,1,9,25],setbound:47,nearest:31,linkview:26,give:18,process:[12,5,18],imagewindow:18,itemignorestransform:16,indic:[17,18,19,8,42,47,57],plotwindow:18,high:20,xmin:4,minimum:[5,47,4],maxgreen:3,want:[12,55,10,50],graphicsobject:[8,52,3,50],itemmov:6,setxlink:14,alwai:[18,9,10,28,4],surfac:55,multipl:[8,55,25,31],goal:20,awkward:18,anoth:[12,14],pyqt:[17,20,29,8,55,25],divis:[19,57],how:[17,18,3,20,55,12,24,25],sever:[2,25],pure:[12,20],reject:6,opt:[33,11],instead:[12,14],simpl:[18,52],css:16,updat:[3,4,20,15,31,10,23,42,14],qwt:15,map:[8,24,4],lai:46,overridden:[23,4],max:[42,47,4],after:[10,14,31],spot:[1,9],befor:[5,14],showlabel:14,plane:18,scratch:55,aribtrari:18,compat:[15,3,31,4],mai:[1,30,23,47,25,18],npt:49,secondari:1,data:[0,1,9,17,20,15,28,18,38,52,55,14,12,53,24,47,25],averag:[20,14],attempt:12,setproperti:31,seriou:55,gradientwidget:[2,22,52],minim:20,correspond:18,exclud:10,caus:[26,3],inform:[12,3,25],maintain:6,combin:25,allow:[0,3,4,20,47,31,6,10,23,55,14,25],callabl:3,first:[12,8,3],order:12,iteratorfn:0,qgraphicswidget:[29,23],rotat:[34,24,38],fft:[20,15],rang:[26,1,4,28,10,23,25],symbols:1,through:[18,30,25],treewidget:[6,2,52],scatterplotitem:[52,9,25,50],vari:18,ax0stop:24,paramet:[16,9,14],qcolor:[18,30],style:[16,1,30],movabl:[45,24,47],directli:[12,18,3],img:[37,42,24],chosen:18,settitl:14,symbolpen:[1,25],clickabl:15,parametertre:[20,2,39,52,35],platform:20,window:[20,17,18,55,25],enablemous:[23,4],hidden:14,unitprefix:14,pixel:[1,10,3,8,23],shear:18,arrowitem:[11,52,50],them:12,good:10,"return":[0,18,42,3,4,45,46,31,6,8,10,49,23,55,24,14,25],greater:1,thei:[6,18,25],handl:[0,24,55,25,38],auto:[16,10,14],linkychang:14,timeindex:42,rectangl:32,ff0:18,framework:[20,25,50],filedialog:[48,2,52],qgraphicsitem:[25,50],dataset:[1,18],videospeedtest:12,setvalu:[5,47,31],introduct:[20,17],plotitem:[18,27,50,7,52,14,25],updateyscal:14,recarrai:1,anyth:[34,50],edit:31,drop:6,easili:[34,30,50],tablewidget:[0,2,52],mode:15,arrow:[33,11],each:[42,8,9,18,25],returnslic:24,redraw:24,side:26,mean:10,compil:12,imageitem:[3,50,52,12,37,24],maxvalu:18,replac:[20,18],individu:45,continu:14,realli:4,heavi:20,meta:[1,14],greyscal:[18,30],iter:[0,30],siscal:18,xval:[28,42,55],happen:12,lockaspect:4,extract:18,orient:[26,45,18,13,22],special:25,out:[46,18,31,4],shown:12,maptoview:4,unbound:31,space:[8,24,18],gradient:[12,32],weav:12,predefin:18,content:[17,52,2,25,50],suitabl:[20,24],rel:[28,24,10,57],correct:[20,8],red:55,yvals2:55,lag:20,linear:[17,18,31,38],insid:[16,17,18,27,4,7,8,10,12,37,55],advanc:46,given:[1,9,4,55,14,25],delai:31,reason:55,base:[26,10,25],timepoint:18,dictionari:[42,53],usual:8,region:[17,4,20,38,45,24],rect:[10,4],extend:[26,0,5,29,6,8],childrenboundingrect:4,wai:[26,1,55,12,10,25],minvalu:18,angl:[24,47],could:[5,55],synchron:26,forcewidth:13,length:[18,5,6,8,23,47],addplot:[55,25],isn:24,outsid:4,geometri:[14,23],assign:[1,9],frequent:2,histogramlut:12,origin:18,pleas:20,major:[20,19],suffix:18,render:1,symbolbrush:[1,25],onc:1,arrai:[0,1,9,3,18,55,12,53,24],scalesnap:24,qualiti:20,number:[20,34,1,18,46],alreadi:25,cosmet:18,"1e6":18,indistinguish:55,open:55,primari:[20,1],size:[16,9,3,32,10,23,24,25,57],fmri:18,guess:42,workaround:29,width:[1,4,21,18,8,23,57],associ:14,top:14,compositionmod:3,system:[12,20,19,23,4],construct:18,paint:10,necessarili:4,demonstr:[6,25],axisitem:[26,52,25,50],exampl:[16,17,18,20,5,34,10,12,55,26,25],white:42,"\u03bcunit":18,"final":18,store:24,mingreen:3,hue:18,shell:55,option:[16,1,3,31,55,12,42,47,25],tool:[12,20],copi:[0,18,15],specifi:[30,9,1,10,24,25],slider:[12,42],ydata:18,somewhat:[20,18,55],essenti:25,than:[20,1,55,18],png:20,mapfromview:4,conveni:18,setattr:16,whenev:10,provid:[0,2,20,50,33,12,15,25],remov:[9,23],pyqtgraph:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,14,37,44,47,40,41,42,45,46,48,49,50,51,52,53,54,55,56,57],tree:[6,20],structur:53,charact:30,sigrangechang:14,light:12,posit:[34,24,47],arrang:20,appenddata:0,other:[18,9,20,55,12,25],initi:[47,1,10],grei:1,graphicslayoutitem:[36,25],comput:[12,3],clip:8,rawimagewidget:[12,37,2,52],checktabl:[2,56,52],datatreewidget:[2,53,52],curvepoint:[34,52,50],ani:[26,1,2,20,18,50,33,55,14,12,10],infinitelin:[47,50,10,52,38],karg:[40,1,3,29,22,18,49,7,14,54,37,24],have:[18,4,28,31,8,10,24],tabl:[12,17,3,49],need:[1,42,18,12,37,24],minr:3,border:[46,3,4],sat:18,getboundingpar:8,engin:[20,18,2],multiplotwidget:[2,52,27],equival:18,min:[42,47,4],maxr:3,self:[24,4],plotdataitem:[1,52,25,14,50],isotrop:4,note:[16,34,18,10,47],also:[30,20,5,6,34,8,23,12,24,25],qgraphicstextitem:16,take:[47,18,55],which:[18,2,4,20,28,45,8,10,24,25],histogramlutwidget:[40,2,52],data1:55,noth:37,data3:55,data2:55,simplifi:[20,18],begin:18,pain:20,normal:[25,50],multipli:26,object:[57,8,24,25,9],rrggbbaa:18,linearregionitem:[45,52,50],most:[8,50,55,18,25],graphicsscen:8,childtransform:4,pair:[24,32],alpha:[26,18,49],"class":[0,1,3,4,5,6,7,8,9,10,11,12,13,15,16,17,18,19,21,22,23,24,25,26,27,28,29,30,31,32,33,34,14,37,44,47,40,41,42,45,46,48,49,50,51,53,54,56,57],ax0start:24,placement:46,hideroot:53,clear:[1,8,14],differ:[12,20,1,55,25],doe:[20,17,8,34],mri:18,determin:[26,18,19,8,10,12,24],axi:[16,26,1,4,14,12,42,25],minhu:18,viewtransform:8,think:55,viewchangedev:10,show:[26,18,5,55,14,25],forgetviewwidget:8,getregion:45,filllevel:[1,15,25],random:25,bring:20,bright:18,radio:14,feedback:55,vtickgroup:[28,52,50],minblu:3,onli:[1,3,4,46,31,8,55,10,42,47,25],coerc:31,ratio:4,colorbutton:[2,51,52],"true":[1,24,3,4,45,5,18,31,6,9,49,14,37,42,13,47],metaarrai:[0,1],behind:[20,1],should:[1,53,4],graphicslayoutwidget:[52,2,55,25,54],busi:5,black:42,viewport:10,qwidget:[2,25],combo:14,qprogressdialog:5,local:[8,4],control:[12,17,25,23,38],realboundingrect:10,keypressev:4,qgraphicsview:[25,23],info:1,predict:20,move:[6,42,24,31,4],get:[37,55],familiar:25,autom:38,unscal:37,state:[6,24,55,14],"import":[20,50,8,55,12,25],increas:[12,23],maxblu:3,requir:[20,5,24],setdata:[15,1,53],scale:[16,26,18,9,3,4,20,42,32,38,23,11,12,37,24,14,25,57],child:[8,4],bar:57,keyword:1,organ:[17,25],diamond:9,mkcolor:[18,30],method:[12,18,30,10,25],setlookupt:3,stuff:5,common:12,xmax:4,contain:[12,8,3,25],qgraphicsobject:8,where:[1,30],valid:47,view:[16,18,9,4,20,28,32,8,10,23,42,25,57],respond:10,set:[16,26,1,9,3,4,20,47,28,18,31,34,23,12,42,24,14,25],qpointf:47,setscal:26,crunch:20,frame:[12,42],displai:[0,1,3,4,5,55,37,12,19,16,17,18,20,21,42,25,26,34,14,11,47,52,53,36,15,57],see:[16,1,3,18,34,30,55,14,12,11,25],subtre:6,result:[8,9,18,23],arg:[16,0,1,42,48,40,29,22,18,49,8,14,37,24],fail:12,horizont:[45,47],yvalu:1,best:18,plotdata:25,infinit:47,detect:10,lut:3,getglobaltransform:24,mainli:16,boundari:[14,10,31,23],exist:[9,55,25,4],label:[16,26,25,14,32],enough:[20,37],dynam:20,between:[12,1],updateplotlist:14,drawn:1,experi:20,approach:55,qbrush:[18,30],parentitem:21,altern:9,autolevel:[42,3],kei:4,numer:31,inverti:4,complement:20,extens:20,maxhu:18,steroid:31,here:[20,55],pixelvector:8,rotatesnap:24,ipython:55,ax1start:24,pixels:[3,23],notat:[18,31],both:[12,1,4],last:18,fit:[26,42,14],cycl:18,howev:[20,18],lazi:55,showaxi:14,viewbox:[16,26,4,50,8,52,10,14,36,25],etc:[16,20,9,55,31],plotcurveitem:[50,33,34,52,15,25],instanc:[12,18],"__init__":[0,1,3,4,5,6,7,8,9,10,11,13,15,16,44,19,21,22,23,24,26,27,28,29,31,32,33,34,14,37,47,40,41,42,45,46,48,49,51,53,54,56,57],ccff00:16,mani:[1,20,30,50,55,24],fix:10,load:12,simpli:55,point:[1,9,4,18,32,33,34,12,11,24,25],instanti:[12,25],colspan:[46,55],pop:55,height:[8,10,4],header:0,written:[20,18],linux:20,cancel:5,typic:25,mouseshap:10,assum:8,viewporttransform:8,scaletoimag:23,save:3,vertic:[45,28,13,47],rgb:[12,18,49],invert:24,devic:8,compos:25,been:[6,24,4],sinc:50,much:20,interpret:[42,25,31],easiest:12,basic:[20,17,25,14,36],unambigu:23,blacklevel:3,box:[24,10,4],pxmode:[1,9],setimag:[12,37,18,42,3],imag:[17,1,3,20,21,18,41,23,12,37,42],numpi:[0,1,20,55,12,25],search:17,argument:[16,1,9,3,5,18,30,10,14,37,24,47,25],coordin:[1,24,4,28,8,9,10,23,19,47],understand:25,togeth:[25,14],demand:12,emphac:1,spin:31,opac:3,"case":[18,55],canceltext:5,multi:[20,18],ident:[1,9,29],sieval:18,look:[18,9],launcher:20,setylink:14,additem:46,graphicsview:[2,4,20,52,50,7,8,27,10,23,36,12,37,25,55],rectangular:[24,18,19,57],cursor:5,defin:[25,32],"while":[46,18,55],abov:[12,55,25],error:18,wascancel:5,aid:20,scene:[9,3,4,8,14,23],shadowpen:[1,15],observ:55,bin:3,planar:18,helper:[17,18,52],ctrl:4,pool:14,itself:14,qrectf:[10,4],vor:10,pixellength:8,histogramlutitem:[41,52,50],primit:20,verticallabel:[2,13,52],pyqwt5:20,scienc:[20,2],parent:[4,5,6,7,8,10,16,44,22,23,42,26,27,31,14,37,51,40,24,46,53,54,15],colorstr:18,enableautoscal:14,develop:[20,55],welcom:17,design:[20,55],perform:[12,37,9,23,20],suggest:55,make:[6,20,18,26,55],format:[0,18,3,55,14,12,24],fillbrush:1,same:[1,9,4,28,18,55,23,10,25],instal:20,nextcol:46,python:[12,20,55,53],pysid:20,complex:[20,30,25],pad:4,gui:[20,17,39,25],scalebar:[57,52,50],document:17,yval:55,pan:[20,42,25,23,4],higher:12,finish:[5,31],optim:[20,37,1,3],viewrect:[8,23,4],nest:[8,53],effect:18,qpen:[1,30,18],plotcurv:25,capabl:[20,18],lookup:[12,3,49],rais:[5,31],user:[2,20,5,55,47,25],canva:39,addavgcurv:14,stack:4,expand:[6,10],built:[12,30],appropri:30,center:4,labeltext:5,relativeto:24,thu:[34,25],nx2:1,well:[12,25],col:55,matplotlib:20,anim:[33,34],without:55,command:[17,55],thi:[1,3,4,6,8,10,12,18,20,23,42,25,26,9,29,31,34,14,37,47,24,50,55],dimension:[20,18],left:14,graphicswidget:[16,46,29,50,8,52],identifi:9,just:[18,55,31],hierarch:53,curvearrow:[33,34,50,52,11],getviewbox:8,shape:[37,1,10,18,25],via:[20,55,23],virtual:50,aspect:4,heavili:25,dock:20,jumpfram:42,shadow:[1,15],viewgeometri:14,signific:23,easi:55,except:[5,31],param:14,discuss:25,color:[16,17,18,15,30,32,52,12,51,25,57],add:[9,25,14],autorang:[42,10],inner:8,win:55,snap:24,els:37,busycursor:5,match:[10,23],build:[12,20,2,55],real:1,applic:[12,17,2,55,20],around:4,itemboundingrect:4,read:25,mapviewtoscen:4,dark:12,buttonitem:[21,52,50],grid:[26,19,46,55,24,25],xdata:18,background:23,press:[24,4],bit:12,tick:[26,28,25],rescal:10,name:[1,42,14],maxval:5,ignor:[9,24,10,3],like:[20,18,55],specif:[33,32],plotwidget:[16,18,2,7,52,55,14,25],qspinbox:31,childgroup:4,signal:31,arbitrari:[18,9],html:14,integ:[12,18,25,31],forgetviewbox:8,"boolean":16,singl:[26,18,27,3,15,30,7,8,9,14,12,47,25],realtim:20,setshadowpen:1,resiz:[10,23],imagefil:21,unnecessari:18,setrang:[23,4],right:[14,23],often:[20,55],captur:4,settext:16,interact:[20,15,55,14],some:[20,0],draw:[1,3,28,18,32,50,47,25],zoom:4,intern:[6,46,8,4],sampl:[33,34],setnewbound:10,importantli:[55,25],useleftbuttonpan:4,librari:[12,20],slice:[17,18,20,38,52,24],bottom:[22,14],maxticklength:26,per:1,prop:31,wrong:20,"20x20":18,pen:[26,1,9,15,28,52,18,30,55,24,47,25],scrollbar:23,unit:[26,18,52,14],allowunicod:18,notabl:30,either:[34,24,14],core:20,plu:9,run:20,bold:16,spinbox:[2,52,31],perpendicular:18,yrang:28,promot:55,offset:[12,32],rrggbb:18,joystickbutton:[44,2,52],simpler:55,lock:4,about:[20,37,55,25],actual:[31,4],transpos:12,setangl:47,page:17,degre:47,statement:5,includ:[12,20,1,55,25],dialog:5,span:26,getlocalhandleposit:24,disabl:[26,5,1,25],produc:12,"8pt":16,routin:4,own:20,effici:55,snapsiz:24,"float":[12,1,18,31],bound:[4,45,31,8,10,47],automat:[26,0,1,14,20,46,30,8,10,23,12,55,19,25],three:[18,25],diagon:18,targetrect:4,brush:[1,9,45,30,52,15,25,18],devicetransform:8,factor:18,mark:[20,17,28,45,38],your:[12,55,25],gradientlegend:[50,52,32],occupi:28,accordingli:14,dlg:5,triangl:9,ymax:4,val:31,area:[1,25],enabl:[8,14,23],hex:18,transform:[8,24,10,3,4],fast:[20,37,15],custom:[20,55],avail:[12,20,55,25],start:[20,18],reli:50,interfac:[20,18,2,55],editor:12,under:25,forward:4,setpoint:9,entir:23,"function":[0,18,2,3,4,20,17,30,50,8,52,55,12,37,24,25],creation:10,interest:[20,17,24,38],offer:18,forc:[9,4],tupl:[18,24,30],linearregion:38,hsvcolor:[18,30],amongst:20,histogram:[12,3],nextrow:46,link:14,translat:[12,24,38],setgrid:26,don:55,line:[17,1,20,18,45,30,55,19,47,25],dtype:1,bug:[8,29],scalebi:4,reset:31,pull:[18,24],immedi:55,flowchart:39,boundingrect:10,possibl:18,whether:[42,24,3],maxbound:24,access:20,maximum:[5,30,47,4],sepecifi:1,until:5,record:0,scipi:12,otherwis:[9,29],handlechang:24,similar:20,setlevel:3,curv:[1,33,34,14,11,15,25],affineslic:18,featur:[20,31],pil:12,creat:[1,55,18,23,25],"int":[1,18,31],cover:24,repres:[8,30,15,18],autoscal:14,editingfinishedev:31,implement:[27,23,4,50,7,14,24],file:[12,55],imageview:[18,2,52,55,12,42],request:4,attr:16,work:[6,12],rearrang:12,fill:[17,1,30,23,25,18],denot:32,automaticali:25,titl:[16,18,25,14],when:[1,3,4,47,6,55,23,10,24,14,25],detail:1,invalid:31,event:4,"default":[16,1,9,4,42,18,10,23,24],circl:9,bool:[1,42],varieti:18,squar:9,you:[18,50,10,12,37,25],autopixelrang:23,absurd:6,matur:20,repeat:1,"_only_":37,sequenc:[1,25],symbol:[1,9,25],qtablewidget:0,ndarrai:[37,1,42,18],multidimension:[17,38],elsewher:6,drag:[6,45,47,4],accomplish:50,embed:[12,17,55,25],consid:55,savest:24,doubl:20,setpxmod:3,prefixless:18,unaffect:14,stai:10,outdat:20,invari:11,viewrangechang:10,svg:20,updatexscal:14,visual:[20,55],tradeoff:37,text:[16,5,31,32,14,13],obj:4,time:[18,3,5,55,12,42],far:20,siformat:18,"export":20,backward:4,prototyp:[20,17,39]},objtypes:{"0":"py:module","1":"py:function","2":"py:method","3":"py:class","4":"py:attribute"},titles:["TableWidget","PlotDataItem","Pyqtgraph’s Widgets","ImageItem","ViewBox","ProgressDialog","TreeWidget","PlotWidget","GraphicsObject","ScatterPlotItem","UIGraphicsItem","ArrowItem","Displaying images and video","VerticalLabel","PlotItem","PlotCurveItem","LabelItem","Welcome to the documentation for pyqtgraph 1.8","Pyqtgraph’s Helper Functions","GridItem","Introduction","ButtonItem","GradientWidget","GraphicsView","ROI","Plotting in pyqtgraph","AxisItem","MultiPlotWidget","VTickGroup","GraphicsWidget","Line, Fill, and Color","SpinBox","GradientLegend","CurveArrow","CurvePoint","parametertree module","Basic display widgets","RawImageWidget","Region-of-interest controls","Rapid GUI prototyping","HistogramLUTWidget","HistogramLUTItem","ImageView","dockarea module","JoystickButton","LinearRegionItem","GraphicsLayout","InfiniteLine","FileDialog","GradientEditorItem","Pyqtgraph’s Graphics Items","ColorButton","API Reference","DataTreeWidget","GraphicsLayoutWidget","How to use pyqtgraph","CheckTable","ScaleBar"],objnames:{"0":"Python module","1":"Python function","2":"Python method","3":"Python class","4":"Python attribute"},filenames:["widgets/tablewidget","graphicsItems/plotdataitem","widgets/index","graphicsItems/imageitem","graphicsItems/viewbox","widgets/progressdialog","widgets/treewidget","widgets/plotwidget","graphicsItems/graphicsobject","graphicsItems/scatterplotitem","graphicsItems/uigraphicsitem","graphicsItems/arrowitem","images","widgets/verticallabel","graphicsItems/plotitem","graphicsItems/plotcurveitem","graphicsItems/labelitem","index","functions","graphicsItems/griditem","introduction","graphicsItems/buttonitem","widgets/gradientwidget","widgets/graphicsview","graphicsItems/roi","plotting","graphicsItems/axisitem","widgets/multiplotwidget","graphicsItems/vtickgroup","graphicsItems/graphicswidget","style","widgets/spinbox","graphicsItems/gradientlegend","graphicsItems/curvearrow","graphicsItems/curvepoint","widgets/parametertree","graphicswindow","widgets/rawimagewidget","region_of_interest","parametertree","widgets/histogramlutwidget","graphicsItems/histogramlutitem","widgets/imageview","widgets/dockarea","widgets/joystickbutton","graphicsItems/linearregionitem","graphicsItems/graphicslayout","graphicsItems/infiniteline","widgets/filedialog","graphicsItems/gradienteditoritem","graphicsItems/index","widgets/colorbutton","apireference","widgets/datatreewidget","widgets/graphicslayoutwidget","how_to_use","widgets/checktable","graphicsItems/scalebar"]}) \ No newline at end of file diff --git a/documentation/build/html/style.html b/documentation/build/html/style.html new file mode 100644 index 00000000..3cdea05f --- /dev/null +++ b/documentation/build/html/style.html @@ -0,0 +1,127 @@ + + + + + + + + + Line, Fill, and Color — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

Line, Fill, and Color¶

+

Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color.

+

For these function arguments, the following values may be used:

+
    +
  • single-character string representing color (b, g, r, c, m, y, k, w)
  • +
  • (r, g, b) or (r, g, b, a) tuple
  • +
  • single greyscale value (0.0 - 1.0)
  • +
  • (index, maximum) tuple for automatically iterating through colors (see functions.intColor)
  • +
  • QColor
  • +
  • QPen / QBrush where appropriate
  • +
+

Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt’s QPen and QBrush classes.

+

Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt’s QColor class

+
+ + +
+
+
+
+
+

Previous topic

+

Displaying images and video

+

Next topic

+

Region-of-interest controls

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/checktable.html b/documentation/build/html/widgets/checktable.html new file mode 100644 index 00000000..22f15e17 --- /dev/null +++ b/documentation/build/html/widgets/checktable.html @@ -0,0 +1,130 @@ + + + + + + + + + CheckTable — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

CheckTable¶

+
+
+class pyqtgraph.CheckTable(columns)¶
+
+
+__init__(columns)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

DataTreeWidget

+

Next topic

+

TableWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/colorbutton.html b/documentation/build/html/widgets/colorbutton.html new file mode 100644 index 00000000..6865391e --- /dev/null +++ b/documentation/build/html/widgets/colorbutton.html @@ -0,0 +1,130 @@ + + + + + + + + + ColorButton — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ColorButton¶

+
+
+class pyqtgraph.ColorButton(parent=None, color=(128, 128, 128))¶
+
+
+__init__(parent=None, color=(128, 128, 128))¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

GradientWidget

+

Next topic

+

GraphicsLayoutWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/datatreewidget.html b/documentation/build/html/widgets/datatreewidget.html new file mode 100644 index 00000000..449b0b48 --- /dev/null +++ b/documentation/build/html/widgets/datatreewidget.html @@ -0,0 +1,138 @@ + + + + + + + + + DataTreeWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

DataTreeWidget¶

+
+
+class pyqtgraph.DataTreeWidget(parent=None, data=None)¶
+

Widget for displaying hierarchical python data structures +(eg, nested dicts, lists, and arrays)

+
+
+__init__(parent=None, data=None)¶
+
+ +
+
+setData(data, hideRoot=False)¶
+

data should be a dictionary.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ImageView

+

Next topic

+

CheckTable

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/dockarea.html b/documentation/build/html/widgets/dockarea.html new file mode 100644 index 00000000..8473421d --- /dev/null +++ b/documentation/build/html/widgets/dockarea.html @@ -0,0 +1,120 @@ + + + + + + + + + dockarea module — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

dockarea module¶

+
+ + +
+
+
+
+
+

Previous topic

+

GraphicsLayoutWidget

+

Next topic

+

parametertree module

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/filedialog.html b/documentation/build/html/widgets/filedialog.html new file mode 100644 index 00000000..45fa4ab1 --- /dev/null +++ b/documentation/build/html/widgets/filedialog.html @@ -0,0 +1,130 @@ + + + + + + + + + FileDialog — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

FileDialog¶

+
+
+class pyqtgraph.FileDialog(*args)¶
+
+
+__init__(*args)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

SpinBox

+

Next topic

+

GraphicsView

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/gradientwidget.html b/documentation/build/html/widgets/gradientwidget.html new file mode 100644 index 00000000..3a11e659 --- /dev/null +++ b/documentation/build/html/widgets/gradientwidget.html @@ -0,0 +1,130 @@ + + + + + + + + + GradientWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GradientWidget¶

+
+
+class pyqtgraph.GradientWidget(parent=None, orientation='bottom', *args, **kargs)¶
+
+
+__init__(parent=None, orientation='bottom', *args, **kargs)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

TableWidget

+

Next topic

+

ColorButton

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/graphicslayoutwidget.html b/documentation/build/html/widgets/graphicslayoutwidget.html new file mode 100644 index 00000000..3cbc7e00 --- /dev/null +++ b/documentation/build/html/widgets/graphicslayoutwidget.html @@ -0,0 +1,130 @@ + + + + + + + + + GraphicsLayoutWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GraphicsLayoutWidget¶

+
+
+class pyqtgraph.GraphicsLayoutWidget(parent=None, **kargs)¶
+
+
+__init__(parent=None, **kargs)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ColorButton

+

Next topic

+

dockarea module

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/graphicsview.html b/documentation/build/html/widgets/graphicsview.html new file mode 100644 index 00000000..4474a07f --- /dev/null +++ b/documentation/build/html/widgets/graphicsview.html @@ -0,0 +1,162 @@ + + + + + + + + + GraphicsView — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

GraphicsView¶

+
+
+class pyqtgraph.GraphicsView(parent=None, useOpenGL=None, background='k')¶
+
+
+__init__(parent=None, useOpenGL=None, background='k')¶
+

Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the +viewed coordinate range. Also automatically creates a QGraphicsScene and a central QGraphicsWidget +that is automatically scaled to the full view geometry.

+

By default, the view coordinate system matches the widget’s pixel coordinates and +automatically updates when the view is resized. This can be overridden by setting +autoPixelRange=False. The exact visible range can be set with setRange().

+

The view can be panned using the middle mouse button and scaled using the right mouse button if +enabled via enableMouse().

+
+ +
+
+pixelSize()¶
+

Return vector with the length and width of one view pixel in scene coordinates

+
+ +
+
+scaleToImage(image)¶
+

Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase.

+
+ +
+
+setCentralWidget(item)¶
+

Sets a QGraphicsWidget to automatically fill the entire view.

+
+ +
+
+viewRect()¶
+

Return the boundaries of the view in scene coordinates

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

FileDialog

+

Next topic

+

JoystickButton

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/histogramlutwidget.html b/documentation/build/html/widgets/histogramlutwidget.html new file mode 100644 index 00000000..5866ae01 --- /dev/null +++ b/documentation/build/html/widgets/histogramlutwidget.html @@ -0,0 +1,130 @@ + + + + + + + + + HistogramLUTWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

HistogramLUTWidget¶

+
+
+class pyqtgraph.HistogramLUTWidget(parent=None, *args, **kargs)¶
+
+
+__init__(parent=None, *args, **kargs)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

parametertree module

+

Next topic

+

ProgressDialog

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/imageview.html b/documentation/build/html/widgets/imageview.html new file mode 100644 index 00000000..85680aa6 --- /dev/null +++ b/documentation/build/html/widgets/imageview.html @@ -0,0 +1,158 @@ + + + + + + + + + ImageView — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ImageView¶

+
+
+class pyqtgraph.ImageView(parent=None, name='ImageView', *args)¶
+
+
+__init__(parent=None, name='ImageView', *args)¶
+
+ +
+
+jumpFrames(n)¶
+

If this is a video, move ahead n frames

+
+ +
+
+setImage(img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None)¶
+

Set the image to be displayed in the widget. +Options are:

+
+

img: ndarray; the image to be displayed. +autoRange: bool; whether to scale/pan the view to fit the image. +autoLevels: bool; whether to update the white/black levels to fit the image. +levels: (min, max); the white and black level values to use. +axes: {‘t’:0, ‘x’:1, ‘y’:2, ‘c’:3}; Dictionary indicating the interpretation for each axis.

+
+This is only needed to override the default guess.
+
+
+ +
+
+timeIndex(slider)¶
+

Return the time and frame index indicated by a slider

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

PlotWidget

+

Next topic

+

DataTreeWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/index.html b/documentation/build/html/widgets/index.html new file mode 100644 index 00000000..8e84cb40 --- /dev/null +++ b/documentation/build/html/widgets/index.html @@ -0,0 +1,144 @@ + + + + + + + + + Pyqtgraph’s Widgets — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

Pyqtgraph’s Widgets¶

+

Pyqtgraph provides several QWidget subclasses which are useful for building user interfaces. These widgets can generally be used in any Qt application and provide functionality that is frequently useful in science and engineering applications.

+

Contents:

+ +
+ + +
+
+
+
+
+

Previous topic

+

UIGraphicsItem

+

Next topic

+

PlotWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/joystickbutton.html b/documentation/build/html/widgets/joystickbutton.html new file mode 100644 index 00000000..60e8eee3 --- /dev/null +++ b/documentation/build/html/widgets/joystickbutton.html @@ -0,0 +1,130 @@ + + + + + + + + + JoystickButton — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

JoystickButton¶

+
+
+class pyqtgraph.JoystickButton(parent=None)¶
+
+
+__init__(parent=None)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

GraphicsView

+

Next topic

+

MultiPlotWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/multiplotwidget.html b/documentation/build/html/widgets/multiplotwidget.html new file mode 100644 index 00000000..8624e2e1 --- /dev/null +++ b/documentation/build/html/widgets/multiplotwidget.html @@ -0,0 +1,131 @@ + + + + + + + + + MultiPlotWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

MultiPlotWidget¶

+
+
+class pyqtgraph.MultiPlotWidget(parent=None)¶
+

Widget implementing a graphicsView with a single PlotItem inside.

+
+
+__init__(parent=None)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

JoystickButton

+

Next topic

+

TreeWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/parametertree.html b/documentation/build/html/widgets/parametertree.html new file mode 100644 index 00000000..cb2ff84f --- /dev/null +++ b/documentation/build/html/widgets/parametertree.html @@ -0,0 +1,120 @@ + + + + + + + + + parametertree module — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

parametertree module¶

+
+ + +
+
+
+
+
+

Previous topic

+

dockarea module

+

Next topic

+

HistogramLUTWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/plotwidget.html b/documentation/build/html/widgets/plotwidget.html new file mode 100644 index 00000000..c8b060f2 --- /dev/null +++ b/documentation/build/html/widgets/plotwidget.html @@ -0,0 +1,131 @@ + + + + + + + + + PlotWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

PlotWidget¶

+
+
+class pyqtgraph.PlotWidget(parent=None, **kargs)¶
+

Widget implementing a graphicsView with a single PlotItem inside.

+
+
+__init__(parent=None, **kargs)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

Pyqtgraph’s Widgets

+

Next topic

+

ImageView

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/progressdialog.html b/documentation/build/html/widgets/progressdialog.html new file mode 100644 index 00000000..969b09af --- /dev/null +++ b/documentation/build/html/widgets/progressdialog.html @@ -0,0 +1,153 @@ + + + + + + + + + ProgressDialog — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

ProgressDialog¶

+
+
+class pyqtgraph.ProgressDialog(labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False)¶
+

Extends QProgressDialog for use in ‘with’ statements. +Arguments:

+
+labelText (required) +cancelText Text to display on cancel button, or None to disable it. +minimum +maximum +parent +wait Length of time (im ms) to wait before displaying dialog +busyCursor If True, show busy cursor until dialog finishes
+
+
Example:
+
+
with ProgressDialog(“Processing..”, minVal, maxVal) as dlg:
+

# do stuff +dlg.setValue(i) ## could also use dlg += 1 +if dlg.wasCanceled():

+
+raise Exception(“Processing canceled by user”)
+
+
+
+
+
+
+__init__(labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

HistogramLUTWidget

+

Next topic

+

SpinBox

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/rawimagewidget.html b/documentation/build/html/widgets/rawimagewidget.html new file mode 100644 index 00000000..88a89584 --- /dev/null +++ b/documentation/build/html/widgets/rawimagewidget.html @@ -0,0 +1,132 @@ + + + + + + + + + RawImageWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + +
+
+
+
+ +
+

RawImageWidget¶

+
+
+class pyqtgraph.RawImageWidget(parent=None, scaled=True)¶
+

Widget optimized for very fast video display. +Generally using an ImageItem inside GraphicsView is fast enough, +but if you need even more performance, this widget is about as fast as it gets.

+

The tradeoff is that this widget will _only_ display the unscaled image +and nothing else.

+
+
+__init__(parent=None, scaled=True)¶
+
+ +
+
+setImage(img, *args, **kargs)¶
+

img must be ndarray of shape (x,y), (x,y,3), or (x,y,4). +Extra arguments are sent to functions.makeARGB

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

VerticalLabel

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/spinbox.html b/documentation/build/html/widgets/spinbox.html new file mode 100644 index 00000000..5eaf1b51 --- /dev/null +++ b/documentation/build/html/widgets/spinbox.html @@ -0,0 +1,164 @@ + + + + + + + + + SpinBox — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

SpinBox¶

+
+
+class pyqtgraph.SpinBox(parent=None, value=0.0, **kwargs)¶
+

QSpinBox widget on steroids. Allows selection of numerical value, with extra features: +- SI prefix notation +- Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.) +- Option for unbounded values +- Delayed signals (allows multiple rapid changes with only one change signal)

+
+
+__init__(parent=None, value=0.0, **kwargs)¶
+
+ +
+
+editingFinishedEvent()¶
+

Edit has finished; set value.

+
+ +
+
+interpret()¶
+

Return value of text. Return False if text is invalid, raise exception if text is intermediate

+
+ +
+
+setProperty(prop, val)¶
+

setProperty is just for compatibility with QSpinBox

+
+ +
+
+setValue(value=None, update=True, delaySignal=False)¶
+

Set the value of this spin. +If the value is out of bounds, it will be moved to the nearest boundary +If the spin is integer type, the value will be coerced to int +Returns the actual value set.

+

If value is None, then the current value is used (this is for resetting +the value after bounds, etc. have changed)

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

ProgressDialog

+

Next topic

+

FileDialog

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/tablewidget.html b/documentation/build/html/widgets/tablewidget.html new file mode 100644 index 00000000..5e6febc4 --- /dev/null +++ b/documentation/build/html/widgets/tablewidget.html @@ -0,0 +1,168 @@ + + + + + + + + + TableWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

TableWidget¶

+
+
+class pyqtgraph.TableWidget(*args)¶
+

Extends QTableWidget with some useful functions for automatic data handling. +Can automatically format and display:

+
+

numpy arrays +numpy record arrays +metaarrays +list-of-lists [[1,2,3], [4,5,6]] +dict-of-lists {‘x’: [1,2,3], ‘y’: [4,5,6]} +list-of-dicts [

+
+
+{‘x’: 1, ‘y’: 4}, +{‘x’: 2, ‘y’: 5}, +{‘x’: 3, ‘y’: 6}
+

]

+
+
+
+
+__init__(*args)¶
+
+ +
+
+appendData(data)¶
+

Types allowed: +1 or 2D numpy array or metaArray +1D numpy record array +list-of-lists, list-of-dicts or dict-of-lists

+
+ +
+
+copy()¶
+

Copy selected data to clipboard.

+
+ +
+
+iteratorFn(data)¶
+

Return 1) a function that will provide an iterator for data and 2) a list of header strings

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

CheckTable

+

Next topic

+

GradientWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/treewidget.html b/documentation/build/html/widgets/treewidget.html new file mode 100644 index 00000000..d20bcbc4 --- /dev/null +++ b/documentation/build/html/widgets/treewidget.html @@ -0,0 +1,140 @@ + + + + + + + + + TreeWidget — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

TreeWidget¶

+
+
+class pyqtgraph.TreeWidget(parent=None)¶
+

Extends QTreeWidget to allow internal drag/drop with widgets in the tree. +Also maintains the expanded state of subtrees as they are moved. +This class demonstrates the absurd lengths one must go to to make drag/drop work.

+
+
+__init__(parent=None)¶
+
+ +
+
+itemMoving(item, parent, index)¶
+

Called when item has been dropped elsewhere in the tree. +Return True to accept the move, False to reject.

+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

MultiPlotWidget

+

Next topic

+

VerticalLabel

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/build/html/widgets/verticallabel.html b/documentation/build/html/widgets/verticallabel.html new file mode 100644 index 00000000..335a9c1e --- /dev/null +++ b/documentation/build/html/widgets/verticallabel.html @@ -0,0 +1,130 @@ + + + + + + + + + VerticalLabel — pyqtgraph v1.8 documentation + + + + + + + + + + + + + + +
+
+
+
+ +
+

VerticalLabel¶

+
+
+class pyqtgraph.VerticalLabel(text, orientation='vertical', forceWidth=True)¶
+
+
+__init__(text, orientation='vertical', forceWidth=True)¶
+
+ +
+ +
+ + +
+
+
+
+
+

Previous topic

+

TreeWidget

+

Next topic

+

RawImageWidget

+

This Page

+ + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/documentation/make.bat b/documentation/make.bat new file mode 100644 index 00000000..1d76823d --- /dev/null +++ b/documentation/make.bat @@ -0,0 +1,155 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyqtgraph.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyqtgraph.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/documentation/source/apireference.rst b/documentation/source/apireference.rst new file mode 100644 index 00000000..ab4ec666 --- /dev/null +++ b/documentation/source/apireference.rst @@ -0,0 +1,11 @@ +API Reference +============= + +Contents: + +.. toctree:: + :maxdepth: 2 + + functions + graphicsItems/index + widgets/index diff --git a/documentation/source/conf.py b/documentation/source/conf.py new file mode 100644 index 00000000..6df8d7b9 --- /dev/null +++ b/documentation/source/conf.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +# +# pyqtgraph documentation build configuration file, created by +# sphinx-quickstart on Fri Nov 18 19:33:12 2011. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(path, '..', '..', '..')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pyqtgraph' +copyright = u'2011, Luke Campagnola' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.8' +# The full version, including alpha/beta/rc tags. +release = '1.8' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pyqtgraphdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'pyqtgraph.tex', u'pyqtgraph Documentation', + u'Luke Campagnola', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'pyqtgraph', u'pyqtgraph Documentation', + [u'Luke Campagnola'], 1) +] diff --git a/documentation/source/functions.rst b/documentation/source/functions.rst new file mode 100644 index 00000000..3d56a4d9 --- /dev/null +++ b/documentation/source/functions.rst @@ -0,0 +1,53 @@ +Pyqtgraph's Helper Functions +============================ + +Simple Data Display Functions +----------------------------- + +.. autofunction:: pyqtgraph.plot + +.. autofunction:: pyqtgraph.image + + + +Color, Pen, and Brush Functions +------------------------------- + +Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and fill shapes. These classes are highly capable but somewhat awkward to use. Pyqtgraph offers the functions :func:`~pyqtgraph.mkColor`, :func:`~pyqtgraph.mkPen`, and :func:`~pyqtgraph.mkBrush` to simplify the process of creating these classes. In most cases, however, it will be unnecessary to call these functions directly--any function or method that accepts *pen* or *brush* arguments will make use of these functions for you. For example, the following three lines all have the same effect:: + + pg.plot(xdata, ydata, pen='r') + pg.plot(xdata, ydata, pen=pg.mkPen('r')) + pg.plot(xdata, ydata, pen=QPen(QColor(255, 0, 0))) + + +.. autofunction:: pyqtgraph.mkColor + +.. autofunction:: pyqtgraph.mkPen + +.. autofunction:: pyqtgraph.mkBrush + +.. autofunction:: pyqtgraph.hsvColor + +.. autofunction:: pyqtgraph.intColor + +.. autofunction:: pyqtgraph.colorTuple + +.. autofunction:: pyqtgraph.colorStr + + +Data Slicing +------------ + +.. autofunction:: pyqtgraph.affineSlice + + + +SI Unit Conversion Functions +---------------------------- + +.. autofunction:: pyqtgraph.siFormat + +.. autofunction:: pyqtgraph.siScale + +.. autofunction:: pyqtgraph.siEval + diff --git a/documentation/source/graphicsItems/arrowitem.rst b/documentation/source/graphicsItems/arrowitem.rst new file mode 100644 index 00000000..250957a5 --- /dev/null +++ b/documentation/source/graphicsItems/arrowitem.rst @@ -0,0 +1,8 @@ +ArrowItem +========= + +.. autoclass:: pyqtgraph.ArrowItem + :members: + + .. automethod:: pyqtgraph.ArrowItem.__init__ + diff --git a/documentation/source/graphicsItems/axisitem.rst b/documentation/source/graphicsItems/axisitem.rst new file mode 100644 index 00000000..8f76d130 --- /dev/null +++ b/documentation/source/graphicsItems/axisitem.rst @@ -0,0 +1,8 @@ +AxisItem +======== + +.. autoclass:: pyqtgraph.AxisItem + :members: + + .. automethod:: pyqtgraph.AxisItem.__init__ + diff --git a/documentation/source/graphicsItems/buttonitem.rst b/documentation/source/graphicsItems/buttonitem.rst new file mode 100644 index 00000000..44469db6 --- /dev/null +++ b/documentation/source/graphicsItems/buttonitem.rst @@ -0,0 +1,8 @@ +ButtonItem +========== + +.. autoclass:: pyqtgraph.ButtonItem + :members: + + .. automethod:: pyqtgraph.ButtonItem.__init__ + diff --git a/documentation/source/graphicsItems/curvearrow.rst b/documentation/source/graphicsItems/curvearrow.rst new file mode 100644 index 00000000..4c7f11ab --- /dev/null +++ b/documentation/source/graphicsItems/curvearrow.rst @@ -0,0 +1,8 @@ +CurveArrow +========== + +.. autoclass:: pyqtgraph.CurveArrow + :members: + + .. automethod:: pyqtgraph.CurveArrow.__init__ + diff --git a/documentation/source/graphicsItems/curvepoint.rst b/documentation/source/graphicsItems/curvepoint.rst new file mode 100644 index 00000000..f19791f7 --- /dev/null +++ b/documentation/source/graphicsItems/curvepoint.rst @@ -0,0 +1,8 @@ +CurvePoint +========== + +.. autoclass:: pyqtgraph.CurvePoint + :members: + + .. automethod:: pyqtgraph.CurvePoint.__init__ + diff --git a/documentation/source/graphicsItems/gradienteditoritem.rst b/documentation/source/graphicsItems/gradienteditoritem.rst new file mode 100644 index 00000000..02d40956 --- /dev/null +++ b/documentation/source/graphicsItems/gradienteditoritem.rst @@ -0,0 +1,8 @@ +GradientEditorItem +================== + +.. autoclass:: pyqtgraph.GradientEditorItem + :members: + + .. automethod:: pyqtgraph.GradientEditorItem.__init__ + diff --git a/documentation/source/graphicsItems/gradientlegend.rst b/documentation/source/graphicsItems/gradientlegend.rst new file mode 100644 index 00000000..f47031c0 --- /dev/null +++ b/documentation/source/graphicsItems/gradientlegend.rst @@ -0,0 +1,8 @@ +GradientLegend +============== + +.. autoclass:: pyqtgraph.GradientLegend + :members: + + .. automethod:: pyqtgraph.GradientLegend.__init__ + diff --git a/documentation/source/graphicsItems/graphicslayout.rst b/documentation/source/graphicsItems/graphicslayout.rst new file mode 100644 index 00000000..f45dfd87 --- /dev/null +++ b/documentation/source/graphicsItems/graphicslayout.rst @@ -0,0 +1,8 @@ +GraphicsLayout +============== + +.. autoclass:: pyqtgraph.GraphicsLayout + :members: + + .. automethod:: pyqtgraph.GraphicsLayout.__init__ + diff --git a/documentation/source/graphicsItems/graphicsobject.rst b/documentation/source/graphicsItems/graphicsobject.rst new file mode 100644 index 00000000..736d941e --- /dev/null +++ b/documentation/source/graphicsItems/graphicsobject.rst @@ -0,0 +1,8 @@ +GraphicsObject +============== + +.. autoclass:: pyqtgraph.GraphicsObject + :members: + + .. automethod:: pyqtgraph.GraphicsObject.__init__ + diff --git a/documentation/source/graphicsItems/graphicswidget.rst b/documentation/source/graphicsItems/graphicswidget.rst new file mode 100644 index 00000000..7cf23bbe --- /dev/null +++ b/documentation/source/graphicsItems/graphicswidget.rst @@ -0,0 +1,8 @@ +GraphicsWidget +============== + +.. autoclass:: pyqtgraph.GraphicsWidget + :members: + + .. automethod:: pyqtgraph.GraphicsWidget.__init__ + diff --git a/documentation/source/graphicsItems/griditem.rst b/documentation/source/graphicsItems/griditem.rst new file mode 100644 index 00000000..aa932766 --- /dev/null +++ b/documentation/source/graphicsItems/griditem.rst @@ -0,0 +1,8 @@ +GridItem +======== + +.. autoclass:: pyqtgraph.GridItem + :members: + + .. automethod:: pyqtgraph.GridItem.__init__ + diff --git a/documentation/source/graphicsItems/histogramlutitem.rst b/documentation/source/graphicsItems/histogramlutitem.rst new file mode 100644 index 00000000..db0e18cb --- /dev/null +++ b/documentation/source/graphicsItems/histogramlutitem.rst @@ -0,0 +1,8 @@ +HistogramLUTItem +================ + +.. autoclass:: pyqtgraph.HistogramLUTItem + :members: + + .. automethod:: pyqtgraph.HistogramLUTItem.__init__ + diff --git a/documentation/source/graphicsItems/imageitem.rst b/documentation/source/graphicsItems/imageitem.rst new file mode 100644 index 00000000..49a981dc --- /dev/null +++ b/documentation/source/graphicsItems/imageitem.rst @@ -0,0 +1,8 @@ +ImageItem +========= + +.. autoclass:: pyqtgraph.ImageItem + :members: + + .. automethod:: pyqtgraph.ImageItem.__init__ + diff --git a/documentation/source/graphicsItems/index.rst b/documentation/source/graphicsItems/index.rst new file mode 100644 index 00000000..46f5a938 --- /dev/null +++ b/documentation/source/graphicsItems/index.rst @@ -0,0 +1,37 @@ +Pyqtgraph's Graphics Items +========================== + +Since pyqtgraph relies on Qt's GraphicsView framework, most of its graphics functionality is implemented as QGraphicsItem subclasses. This has two important consequences: 1) virtually anything you want to draw can be easily accomplished using the functionality provided by Qt. 2) Many of pyqtgraph's GraphicsItem classes can be used in any normal QGraphicsScene. + + +Contents: + +.. toctree:: + :maxdepth: 2 + + plotdataitem + plotcurveitem + scatterplotitem + plotitem + imageitem + viewbox + linearregionitem + infiniteline + roi + graphicslayout + axisitem + arrowitem + curvepoint + curvearrow + griditem + scalebar + labelitem + vtickgroup + gradienteditoritem + histogramlutitem + gradientlegend + buttonitem + graphicsobject + graphicswidget + uigraphicsitem + diff --git a/documentation/source/graphicsItems/infiniteline.rst b/documentation/source/graphicsItems/infiniteline.rst new file mode 100644 index 00000000..e95987bc --- /dev/null +++ b/documentation/source/graphicsItems/infiniteline.rst @@ -0,0 +1,8 @@ +InfiniteLine +============ + +.. autoclass:: pyqtgraph.InfiniteLine + :members: + + .. automethod:: pyqtgraph.InfiniteLine.__init__ + diff --git a/documentation/source/graphicsItems/labelitem.rst b/documentation/source/graphicsItems/labelitem.rst new file mode 100644 index 00000000..ca420d76 --- /dev/null +++ b/documentation/source/graphicsItems/labelitem.rst @@ -0,0 +1,8 @@ +LabelItem +========= + +.. autoclass:: pyqtgraph.LabelItem + :members: + + .. automethod:: pyqtgraph.LabelItem.__init__ + diff --git a/documentation/source/graphicsItems/linearregionitem.rst b/documentation/source/graphicsItems/linearregionitem.rst new file mode 100644 index 00000000..9bcb534c --- /dev/null +++ b/documentation/source/graphicsItems/linearregionitem.rst @@ -0,0 +1,8 @@ +LinearRegionItem +================ + +.. autoclass:: pyqtgraph.LinearRegionItem + :members: + + .. automethod:: pyqtgraph.LinearRegionItem.__init__ + diff --git a/documentation/source/graphicsItems/make b/documentation/source/graphicsItems/make new file mode 100644 index 00000000..2a990405 --- /dev/null +++ b/documentation/source/graphicsItems/make @@ -0,0 +1,37 @@ +files = """ArrowItem +AxisItem +ButtonItem +CurvePoint +GradientEditorItem +GradientLegend +GraphicsLayout +GraphicsObject +GraphicsWidget +GridItem +HistogramLUTItem +ImageItem +InfiniteLine +LabelItem +LinearRegionItem +PlotCurveItem +PlotDataItem +ROI +ScaleBar +ScatterPlotItem +UIGraphicsItem +ViewBox +VTickGroup""".split('\n') + +for f in files: + print f + fh = open(f.lower()+'.rst', 'w') + fh.write( +"""%s +%s + +.. autoclass:: pyqtgraph.%s + :members: + + .. automethod:: pyqtgraph.%s.__init__ + +""" % (f, '='*len(f), f, f)) diff --git a/documentation/source/graphicsItems/plotcurveitem.rst b/documentation/source/graphicsItems/plotcurveitem.rst new file mode 100644 index 00000000..f0b2171d --- /dev/null +++ b/documentation/source/graphicsItems/plotcurveitem.rst @@ -0,0 +1,8 @@ +PlotCurveItem +============= + +.. autoclass:: pyqtgraph.PlotCurveItem + :members: + + .. automethod:: pyqtgraph.PlotCurveItem.__init__ + diff --git a/documentation/source/graphicsItems/plotdataitem.rst b/documentation/source/graphicsItems/plotdataitem.rst new file mode 100644 index 00000000..275084e9 --- /dev/null +++ b/documentation/source/graphicsItems/plotdataitem.rst @@ -0,0 +1,8 @@ +PlotDataItem +============ + +.. autoclass:: pyqtgraph.PlotDataItem + :members: + + .. automethod:: pyqtgraph.PlotDataItem.__init__ + diff --git a/documentation/source/graphicsItems/plotitem.rst b/documentation/source/graphicsItems/plotitem.rst new file mode 100644 index 00000000..cbf5f9f4 --- /dev/null +++ b/documentation/source/graphicsItems/plotitem.rst @@ -0,0 +1,7 @@ +PlotItem +======== + +.. autoclass:: pyqtgraph.PlotItem + :members: + + .. automethod:: pyqtgraph.PlotItem.__init__ diff --git a/documentation/source/graphicsItems/roi.rst b/documentation/source/graphicsItems/roi.rst new file mode 100644 index 00000000..22945ade --- /dev/null +++ b/documentation/source/graphicsItems/roi.rst @@ -0,0 +1,8 @@ +ROI +=== + +.. autoclass:: pyqtgraph.ROI + :members: + + .. automethod:: pyqtgraph.ROI.__init__ + diff --git a/documentation/source/graphicsItems/scalebar.rst b/documentation/source/graphicsItems/scalebar.rst new file mode 100644 index 00000000..2ab33967 --- /dev/null +++ b/documentation/source/graphicsItems/scalebar.rst @@ -0,0 +1,8 @@ +ScaleBar +======== + +.. autoclass:: pyqtgraph.ScaleBar + :members: + + .. automethod:: pyqtgraph.ScaleBar.__init__ + diff --git a/documentation/source/graphicsItems/scatterplotitem.rst b/documentation/source/graphicsItems/scatterplotitem.rst new file mode 100644 index 00000000..be2c874b --- /dev/null +++ b/documentation/source/graphicsItems/scatterplotitem.rst @@ -0,0 +1,8 @@ +ScatterPlotItem +=============== + +.. autoclass:: pyqtgraph.ScatterPlotItem + :members: + + .. automethod:: pyqtgraph.ScatterPlotItem.__init__ + diff --git a/documentation/source/graphicsItems/uigraphicsitem.rst b/documentation/source/graphicsItems/uigraphicsitem.rst new file mode 100644 index 00000000..4f0b9933 --- /dev/null +++ b/documentation/source/graphicsItems/uigraphicsitem.rst @@ -0,0 +1,8 @@ +UIGraphicsItem +============== + +.. autoclass:: pyqtgraph.UIGraphicsItem + :members: + + .. automethod:: pyqtgraph.UIGraphicsItem.__init__ + diff --git a/documentation/source/graphicsItems/viewbox.rst b/documentation/source/graphicsItems/viewbox.rst new file mode 100644 index 00000000..3593d295 --- /dev/null +++ b/documentation/source/graphicsItems/viewbox.rst @@ -0,0 +1,8 @@ +ViewBox +======= + +.. autoclass:: pyqtgraph.ViewBox + :members: + + .. automethod:: pyqtgraph.ViewBox.__init__ + diff --git a/documentation/source/graphicsItems/vtickgroup.rst b/documentation/source/graphicsItems/vtickgroup.rst new file mode 100644 index 00000000..342705de --- /dev/null +++ b/documentation/source/graphicsItems/vtickgroup.rst @@ -0,0 +1,8 @@ +VTickGroup +========== + +.. autoclass:: pyqtgraph.VTickGroup + :members: + + .. automethod:: pyqtgraph.VTickGroup.__init__ + diff --git a/documentation/source/graphicswindow.rst b/documentation/source/graphicswindow.rst new file mode 100644 index 00000000..3d5641c3 --- /dev/null +++ b/documentation/source/graphicswindow.rst @@ -0,0 +1,8 @@ +Basic display widgets +===================== + + - GraphicsWindow + - GraphicsView + - GraphicsLayoutItem + - ViewBox + diff --git a/documentation/source/how_to_use.rst b/documentation/source/how_to_use.rst new file mode 100644 index 00000000..74e901d0 --- /dev/null +++ b/documentation/source/how_to_use.rst @@ -0,0 +1,47 @@ +How to use pyqtgraph +==================== + +There are a few suggested ways to use pyqtgraph: + +* From the interactive shell (python -i, ipython, etc) +* Displaying pop-up windows from an application +* Embedding widgets in a PyQt application + + + +Command-line use +---------------- + +Pyqtgraph makes it very easy to visualize data from the command line. Observe:: + + import pyqtgraph as pg + pg.plot(data) # data can be a list of values or a numpy array + +The example above would open a window displaying a line plot of the data given. I don't think it could reasonably be any simpler than that. The call to pg.plot returns a handle to the plot widget that is created, allowing more data to be added to the same window. + +Further examples:: + + pw = pg.plot(xVals, yVals, pen='r') # plot x vs y in red + pw.plot(xVals, yVals2, pen='b') + + win = pg.GraphicsWindow() # Automatically generates grids with multiple items + win.addPlot(data1, row=0, col=0) + win.addPlot(data2, row=0, col=1) + win.addPlot(data3, row=1, col=0, colspan=2) + + pg.show(imageData) # imageData must be a numpy array with 2 to 4 dimensions + +We're only scratching the surface here--these functions accept many different data formats and options for customizing the appearance of your data. + + +Displaying windows from within an application +--------------------------------------------- + +While I consider this approach somewhat lazy, it is often the case that 'lazy' is indistinguishable from 'highly efficient'. The approach here is simply to use the very same functions that would be used on the command line, but from within an existing application. I often use this when I simply want to get a immediate feedback about the state of data in my application without taking the time to build a user interface for it. + + +Embedding widgets inside PyQt applications +------------------------------------------ + +For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: PlotWidget, ImageView, GraphicsView, GraphicsLayoutWidget. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality. + diff --git a/documentation/source/images.rst b/documentation/source/images.rst new file mode 100644 index 00000000..461a9cb7 --- /dev/null +++ b/documentation/source/images.rst @@ -0,0 +1,26 @@ +Displaying images and video +=========================== + +Pyqtgraph displays 2D numpy arrays as images and provides tools for determining how to translate between the numpy data type and RGB values on the screen. If you want to display data from common image and video file formats, you will need to load the data first using another library (PIL works well for images and built-in numpy conversion). + +The easiest way to display 2D or 3D data is using the :func:`pyqtgraph.image` function:: + + import pyqtgraph as pg + pg.image(imageData) + +This function will accept any floating-point or integer data types and displays a single :class:`~pyqtgraph.ImageView` widget containing your data. This widget includes controls for determining how the image data will be converted to 32-bit RGBa values. Conversion happens in two steps (both are optional): + +1. Scale and offset the data (by selecting the dark/light levels on the displayed histogram) +2. Convert the data to color using a lookup table (determined by the colors shown in the gradient editor) + +If the data is 3D (time, x, y), then a time axis will be shown with a slider that can set the currently displayed frame. (if the axes in your data are ordered differently, use numpy.transpose to rearrange them) + +There are a few other methods for displaying images as well: + +* The :class:`~pyqtgraph.ImageView` class can also be instantiated directly and embedded in Qt applications. +* Instances of :class:`~pyqtgraph.ImageItem` can be used inside a GraphicsView. +* For higher performance, use :class:`~pyqtgraph.RawImageWidget`. + +Any of these classes are acceptable for displaying video by calling setImage() to display a new frame. To increase performance, the image processing system uses scipy.weave to produce compiled libraries. If your computer has a compiler available, weave will automatically attempt to build the libraries it needs on demand. If this fails, then the slower pure-python methods will be used instead. + +For more information, see the classes listed above and the 'VideoSpeedTest', 'ImageItem', 'ImageView', and 'HistogramLUT' :ref:`examples`. \ No newline at end of file diff --git a/documentation/source/images/plottingClasses.png b/documentation/source/images/plottingClasses.png new file mode 100644 index 0000000000000000000000000000000000000000..7c8325a5bbe0809aa3b40ac3a8f76972b4d802a9 GIT binary patch literal 68667 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 literal 0 HcmV?d00001 diff --git a/documentation/source/images/plottingClasses.svg b/documentation/source/images/plottingClasses.svg new file mode 100644 index 00000000..393d16d7 --- /dev/null +++ b/documentation/source/images/plottingClasses.svg @@ -0,0 +1,580 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + PlotWidget(GraphicsView) + + + + PlotItem(GraphicsItem) + + + + ViewBox(GraphicsItem) + + + + + AxisItem(GraphicsItem) + + + + AxisItem(GraphicsItem) + + + + AxisItem(GraphicsItem) + + + + AxisItem(GraphicsItem) + + + + Title - LabelItem(GraphicsItem) + + PlotDataItem(GraphicsItem) + + + + + + GraphicsLayoutWidget(GraphicsView) + + + + GraphicsLayoutItem(GraphicsItem) + + + + PlotItem + + + + + + + + + + + + + + + + + + + + + + + + + ViewBox + + + + + PlotItem + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/source/index.rst b/documentation/source/index.rst new file mode 100644 index 00000000..aa6753ef --- /dev/null +++ b/documentation/source/index.rst @@ -0,0 +1,32 @@ +.. pyqtgraph documentation master file, created by + sphinx-quickstart on Fri Nov 18 19:33:12 2011. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to the documentation for pyqtgraph 1.8 +============================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + introduction + how_to_use + plotting + images + style + region_of_interest + graphicswindow + parametertree + internals + apireference + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/documentation/source/internals.rst b/documentation/source/internals.rst new file mode 100644 index 00000000..3f25376d --- /dev/null +++ b/documentation/source/internals.rst @@ -0,0 +1,9 @@ +Internals - Extensions to Qt's GraphicsView +================================ + +* GraphicsView +* GraphicsScene (mouse events) +* GraphicsObject +* GraphicsWidget +* ViewBox + diff --git a/documentation/source/introduction.rst b/documentation/source/introduction.rst new file mode 100644 index 00000000..c5c1dfab --- /dev/null +++ b/documentation/source/introduction.rst @@ -0,0 +1,51 @@ +Introduction +============ + + + +What is pyqtgraph? +------------------ + +Pyqtgraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer). + +Pyqtgraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt's GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort. + +It is known to run on Linux, Windows, and OSX + + +What can it do? +--------------- + +Amongst the core features of pyqtgraph are: + +* Basic data visualization primitives: Images, line and scatter plots +* Fast enough for realtime update of video/plot data +* Interactive scaling/panning, averaging, FFTs, SVG/PNG export +* Widgets for marking/selecting plot regions +* Widgets for marking/selecting image region-of-interest and automatically slicing multi-dimensional image data +* Framework for building customized image region-of-interest widgets +* Docking system that replaces/complements Qt's dock system to allow more complex (and more predictable) docking arrangements +* ParameterTree widget for rapid prototyping of dynamic interfaces (Similar to the property trees in Qt Designer and many other applications) + + +.. _examples: + +Examples +-------- + +Pyqtgraph includes an extensive set of examples that can be accessed by running:: + + import pyqtgraph.examples + pyqtgraph.examples.run() + +This will start a launcher with a list of available examples. Select an item from the list to view its source code and double-click an item to run the example. + + +How does it compare to... +------------------------- + +* matplotlib: For plotting and making publication-quality graphics, matplotlib is far more mature than pyqtgraph. However, matplotlib is also much slower and not suitable for applications requiring realtime update of plots/video or rapid interactivity. It also does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. + +* pyqwt5: pyqwt is generally more mature than pyqtgraph for plotting and is about as fast. The major differences are 1) pyqtgraph is written in pure python, so it is somewhat more portable than pyqwt, which often lags behind pyqt in development (and can be a pain to install on some platforms) and 2) like matplotlib, pyqwt does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. + +(My experience with these libraries is somewhat outdated; please correct me if I am wrong here) diff --git a/documentation/source/parametertree.rst b/documentation/source/parametertree.rst new file mode 100644 index 00000000..de699492 --- /dev/null +++ b/documentation/source/parametertree.rst @@ -0,0 +1,7 @@ +Rapid GUI prototyping +===================== + + - parametertree + - dockarea + - flowchart + - canvas diff --git a/documentation/source/plotting.rst b/documentation/source/plotting.rst new file mode 100644 index 00000000..ee9ed6dc --- /dev/null +++ b/documentation/source/plotting.rst @@ -0,0 +1,73 @@ +Plotting in pyqtgraph +===================== + +There are a few basic ways to plot data in pyqtgraph: + +================================================================ ================================================== +:func:`pyqtgraph.plot` Create a new plot window showing your data +:func:`PlotWidget.plot() ` Add a new set of data to an existing plot widget +:func:`PlotItem.plot() ` Add a new set of data to an existing plot widget +:func:`GraphicsWindow.addPlot() ` Add a new plot to a grid of plots +================================================================ ================================================== + +All of these will accept the same basic arguments which control how the plot data is interpreted and displayed: + +* x - Optional X data; if not specified, then a range of integers will be generated automatically. +* y - Y data. +* pen - The pen to use when drawing plot lines, or None to disable lines. +* symbol - A string describing the shape of symbols to use for each point. Optionally, this may also be a sequence of strings with a different symbol for each point. +* symbolPen - The pen (or sequence of pens) to use when drawing the symbol outline. +* symbolBrush - The brush (or sequence of brushes) to use when filling the symbol. +* fillLevel - Fills the area under the plot curve to this Y-value. +* brush - The brush to use when filling under the curve. + +See the 'plotting' :ref:`example ` for a demonstration of these arguments. + +All of the above functions also return handles to the objects that are created, allowing the plots and data to be further modified. + +Organization of Plotting Classes +-------------------------------- + +There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. Pyqtgraph is based heavily on Qt's GraphicsView framework--if you are not already familiar with this, it's worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView. + +* Data Classes (all subclasses of QGraphicsItem) + * PlotCurveItem - Displays a plot line given x,y data + * ScatterPlotItem - Displays points given x,y data + * :class:`PlotDataItem ` - Combines PlotCurveItem and ScatterPlotItem. The plotting functions discussed above create objects of this type. +* Container Classes (subclasses of QGraphicsItem; contain other QGraphicsItem objects and must be viewed from within a GraphicsView) + * PlotItem - Contains a ViewBox for displaying data as well as AxisItems and labels for displaying the axes and title. This is a QGraphicsItem subclass and thus may only be used from within a GraphicsView + * GraphicsLayoutItem - QGraphicsItem subclass which displays a grid of items. This is used to display multiple PlotItems together. + * ViewBox - A QGraphicsItem subclass for displaying data. The user may scale/pan the contents of a ViewBox using the mouse. Typically all PlotData/PlotCurve/ScatterPlotItems are displayed from within a ViewBox. + * AxisItem - Displays axis values, ticks, and labels. Most commonly used with PlotItem. +* Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs) + * PlotWidget - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. + * GraphicsLayoutWidget - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget. + +.. image:: images/plottingClasses.png + + +Examples +-------- + +See the 'plotting' and 'PlotWidget' :ref:`examples included with pyqtgraph ` for more information. + +Show x,y data as scatter plot:: + + import pyqtgraph as pg + import numpy as np + x = np.random.normal(size=1000) + y = np.random.normal(size=1000) + pg.plot(x, y, pen=None, symbol='o') ## setting pen=None disables line drawing + +Create/show a plot widget, display three data curves:: + + import pyqtgraph as pg + import numpy as np + x = np.arange(1000) + y = np.random.normal(size=(3, 1000)) + plotWidget = pg.plot(title="Three plot curves") + for i in range(3): + plotWidget.plot(x, y[i], pen=(i,3)) ## setting pen=(i,3) automaticaly creates three different-colored pens + + + diff --git a/documentation/source/region_of_interest.rst b/documentation/source/region_of_interest.rst new file mode 100644 index 00000000..24799cb7 --- /dev/null +++ b/documentation/source/region_of_interest.rst @@ -0,0 +1,19 @@ +Region-of-interest controls +=========================== + +Slicing Multidimensional Data +----------------------------- + +Linear Selection and Marking +---------------------------- + +2D Selection and Marking +------------------------ + + + + +- translate / rotate / scale +- highly configurable control handles +- automated data slicing +- linearregion, infiniteline diff --git a/documentation/source/style.rst b/documentation/source/style.rst new file mode 100644 index 00000000..fc172420 --- /dev/null +++ b/documentation/source/style.rst @@ -0,0 +1,17 @@ +Line, Fill, and Color +===================== + +Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color. + +For these function arguments, the following values may be used: + +* single-character string representing color (b, g, r, c, m, y, k, w) +* (r, g, b) or (r, g, b, a) tuple +* single greyscale value (0.0 - 1.0) +* (index, maximum) tuple for automatically iterating through colors (see functions.intColor) +* QColor +* QPen / QBrush where appropriate + +Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes. + +Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt's QColor class diff --git a/documentation/source/widgets/checktable.rst b/documentation/source/widgets/checktable.rst new file mode 100644 index 00000000..5301a4e9 --- /dev/null +++ b/documentation/source/widgets/checktable.rst @@ -0,0 +1,8 @@ +CheckTable +========== + +.. autoclass:: pyqtgraph.CheckTable + :members: + + .. automethod:: pyqtgraph.CheckTable.__init__ + diff --git a/documentation/source/widgets/colorbutton.rst b/documentation/source/widgets/colorbutton.rst new file mode 100644 index 00000000..690239d8 --- /dev/null +++ b/documentation/source/widgets/colorbutton.rst @@ -0,0 +1,8 @@ +ColorButton +=========== + +.. autoclass:: pyqtgraph.ColorButton + :members: + + .. automethod:: pyqtgraph.ColorButton.__init__ + diff --git a/documentation/source/widgets/datatreewidget.rst b/documentation/source/widgets/datatreewidget.rst new file mode 100644 index 00000000..f6bbdbaf --- /dev/null +++ b/documentation/source/widgets/datatreewidget.rst @@ -0,0 +1,8 @@ +DataTreeWidget +============== + +.. autoclass:: pyqtgraph.DataTreeWidget + :members: + + .. automethod:: pyqtgraph.DataTreeWidget.__init__ + diff --git a/documentation/source/widgets/dockarea.rst b/documentation/source/widgets/dockarea.rst new file mode 100644 index 00000000..09a6acca --- /dev/null +++ b/documentation/source/widgets/dockarea.rst @@ -0,0 +1,5 @@ +dockarea module +=============== + +.. automodule:: pyqtgraph.dockarea + :members: diff --git a/documentation/source/widgets/filedialog.rst b/documentation/source/widgets/filedialog.rst new file mode 100644 index 00000000..bf2f9c07 --- /dev/null +++ b/documentation/source/widgets/filedialog.rst @@ -0,0 +1,8 @@ +FileDialog +========== + +.. autoclass:: pyqtgraph.FileDialog + :members: + + .. automethod:: pyqtgraph.FileDialog.__init__ + diff --git a/documentation/source/widgets/gradientwidget.rst b/documentation/source/widgets/gradientwidget.rst new file mode 100644 index 00000000..a2587503 --- /dev/null +++ b/documentation/source/widgets/gradientwidget.rst @@ -0,0 +1,8 @@ +GradientWidget +============== + +.. autoclass:: pyqtgraph.GradientWidget + :members: + + .. automethod:: pyqtgraph.GradientWidget.__init__ + diff --git a/documentation/source/widgets/graphicslayoutwidget.rst b/documentation/source/widgets/graphicslayoutwidget.rst new file mode 100644 index 00000000..5f885f07 --- /dev/null +++ b/documentation/source/widgets/graphicslayoutwidget.rst @@ -0,0 +1,8 @@ +GraphicsLayoutWidget +==================== + +.. autoclass:: pyqtgraph.GraphicsLayoutWidget + :members: + + .. automethod:: pyqtgraph.GraphicsLayoutWidget.__init__ + diff --git a/documentation/source/widgets/graphicsview.rst b/documentation/source/widgets/graphicsview.rst new file mode 100644 index 00000000..ac7ae3bf --- /dev/null +++ b/documentation/source/widgets/graphicsview.rst @@ -0,0 +1,8 @@ +GraphicsView +============ + +.. autoclass:: pyqtgraph.GraphicsView + :members: + + .. automethod:: pyqtgraph.GraphicsView.__init__ + diff --git a/documentation/source/widgets/histogramlutwidget.rst b/documentation/source/widgets/histogramlutwidget.rst new file mode 100644 index 00000000..9d8f3b20 --- /dev/null +++ b/documentation/source/widgets/histogramlutwidget.rst @@ -0,0 +1,8 @@ +HistogramLUTWidget +================== + +.. autoclass:: pyqtgraph.HistogramLUTWidget + :members: + + .. automethod:: pyqtgraph.HistogramLUTWidget.__init__ + diff --git a/documentation/source/widgets/imageview.rst b/documentation/source/widgets/imageview.rst new file mode 100644 index 00000000..1eadabbf --- /dev/null +++ b/documentation/source/widgets/imageview.rst @@ -0,0 +1,8 @@ +ImageView +========= + +.. autoclass:: pyqtgraph.ImageView + :members: + + .. automethod:: pyqtgraph.ImageView.__init__ + diff --git a/documentation/source/widgets/index.rst b/documentation/source/widgets/index.rst new file mode 100644 index 00000000..1beaf1ec --- /dev/null +++ b/documentation/source/widgets/index.rst @@ -0,0 +1,31 @@ +Pyqtgraph's Widgets +=================== + +Pyqtgraph provides several QWidget subclasses which are useful for building user interfaces. These widgets can generally be used in any Qt application and provide functionality that is frequently useful in science and engineering applications. + +Contents: + +.. toctree:: + :maxdepth: 2 + + plotwidget + imageview + dockarea + spinbox + gradientwidget + histogramlutwidget + parametertree + graphicsview + rawimagewidget + datatreewidget + tablewidget + treewidget + checktable + colorbutton + graphicslayoutwidget + progressdialog + filedialog + joystickbutton + multiplotwidget + verticallabel + diff --git a/documentation/source/widgets/joystickbutton.rst b/documentation/source/widgets/joystickbutton.rst new file mode 100644 index 00000000..4d21e16f --- /dev/null +++ b/documentation/source/widgets/joystickbutton.rst @@ -0,0 +1,8 @@ +JoystickButton +============== + +.. autoclass:: pyqtgraph.JoystickButton + :members: + + .. automethod:: pyqtgraph.JoystickButton.__init__ + diff --git a/documentation/source/widgets/make b/documentation/source/widgets/make new file mode 100644 index 00000000..40d0e126 --- /dev/null +++ b/documentation/source/widgets/make @@ -0,0 +1,31 @@ +files = """CheckTable +ColorButton +DataTreeWidget +FileDialog +GradientWidget +GraphicsLayoutWidget +GraphicsView +HistogramLUTWidget +JoystickButton +MultiPlotWidget +PlotWidget +ProgressDialog +RawImageWidget +SpinBox +TableWidget +TreeWidget +VerticalLabel""".split('\n') + +for f in files: + print f + fh = open(f.lower()+'.rst', 'w') + fh.write( +"""%s +%s + +.. autoclass:: pyqtgraph.%s + :members: + + .. automethod:: pyqtgraph.%s.__init__ + +""" % (f, '='*len(f), f, f)) diff --git a/documentation/source/widgets/multiplotwidget.rst b/documentation/source/widgets/multiplotwidget.rst new file mode 100644 index 00000000..46986db0 --- /dev/null +++ b/documentation/source/widgets/multiplotwidget.rst @@ -0,0 +1,8 @@ +MultiPlotWidget +=============== + +.. autoclass:: pyqtgraph.MultiPlotWidget + :members: + + .. automethod:: pyqtgraph.MultiPlotWidget.__init__ + diff --git a/documentation/source/widgets/parametertree.rst b/documentation/source/widgets/parametertree.rst new file mode 100644 index 00000000..565b930b --- /dev/null +++ b/documentation/source/widgets/parametertree.rst @@ -0,0 +1,5 @@ +parametertree module +==================== + +.. automodule:: pyqtgraph.parametertree + :members: diff --git a/documentation/source/widgets/plotwidget.rst b/documentation/source/widgets/plotwidget.rst new file mode 100644 index 00000000..cbded80d --- /dev/null +++ b/documentation/source/widgets/plotwidget.rst @@ -0,0 +1,8 @@ +PlotWidget +========== + +.. autoclass:: pyqtgraph.PlotWidget + :members: + + .. automethod:: pyqtgraph.PlotWidget.__init__ + diff --git a/documentation/source/widgets/progressdialog.rst b/documentation/source/widgets/progressdialog.rst new file mode 100644 index 00000000..fff04cb3 --- /dev/null +++ b/documentation/source/widgets/progressdialog.rst @@ -0,0 +1,8 @@ +ProgressDialog +============== + +.. autoclass:: pyqtgraph.ProgressDialog + :members: + + .. automethod:: pyqtgraph.ProgressDialog.__init__ + diff --git a/documentation/source/widgets/rawimagewidget.rst b/documentation/source/widgets/rawimagewidget.rst new file mode 100644 index 00000000..29fda791 --- /dev/null +++ b/documentation/source/widgets/rawimagewidget.rst @@ -0,0 +1,8 @@ +RawImageWidget +============== + +.. autoclass:: pyqtgraph.RawImageWidget + :members: + + .. automethod:: pyqtgraph.RawImageWidget.__init__ + diff --git a/documentation/source/widgets/spinbox.rst b/documentation/source/widgets/spinbox.rst new file mode 100644 index 00000000..33da1f4c --- /dev/null +++ b/documentation/source/widgets/spinbox.rst @@ -0,0 +1,8 @@ +SpinBox +======= + +.. autoclass:: pyqtgraph.SpinBox + :members: + + .. automethod:: pyqtgraph.SpinBox.__init__ + diff --git a/documentation/source/widgets/tablewidget.rst b/documentation/source/widgets/tablewidget.rst new file mode 100644 index 00000000..283b540b --- /dev/null +++ b/documentation/source/widgets/tablewidget.rst @@ -0,0 +1,8 @@ +TableWidget +=========== + +.. autoclass:: pyqtgraph.TableWidget + :members: + + .. automethod:: pyqtgraph.TableWidget.__init__ + diff --git a/documentation/source/widgets/treewidget.rst b/documentation/source/widgets/treewidget.rst new file mode 100644 index 00000000..00f9fa28 --- /dev/null +++ b/documentation/source/widgets/treewidget.rst @@ -0,0 +1,8 @@ +TreeWidget +========== + +.. autoclass:: pyqtgraph.TreeWidget + :members: + + .. automethod:: pyqtgraph.TreeWidget.__init__ + diff --git a/documentation/source/widgets/verticallabel.rst b/documentation/source/widgets/verticallabel.rst new file mode 100644 index 00000000..4f627437 --- /dev/null +++ b/documentation/source/widgets/verticallabel.rst @@ -0,0 +1,8 @@ +VerticalLabel +============= + +.. autoclass:: pyqtgraph.VerticalLabel + :members: + + .. automethod:: pyqtgraph.VerticalLabel.__init__ + diff --git a/examples/test_Arrow.py b/examples/Arrow.py similarity index 77% rename from examples/test_Arrow.py rename to examples/Arrow.py index 7d0c4aad..f9384008 100755 --- a/examples/test_Arrow.py +++ b/examples/Arrow.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) +## Add path to library (just for examples; you do not need this) import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - import numpy as np from PyQt4 import QtGui, QtCore import pyqtgraph as pg @@ -15,7 +14,7 @@ mw.resize(800,800) p = pg.PlotWidget() mw.setCentralWidget(p) -c = p.plot(x=np.sin(np.linspace(0, 2*np.pi, 100)), y=np.cos(np.linspace(0, 2*np.pi, 100))) +c = p.plot(x=np.sin(np.linspace(0, 2*np.pi, 1000)), y=np.cos(np.linspace(0, 6*np.pi, 1000))) a = pg.CurveArrow(c) p.addItem(a) diff --git a/examples/CLIexample.py b/examples/CLIexample.py new file mode 100644 index 00000000..f2def91a --- /dev/null +++ b/examples/CLIexample.py @@ -0,0 +1,22 @@ +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + + +from PyQt4 import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +app = QtGui.QApplication([]) + + +data = np.random.normal(size=1000) +pg.plot(data, title="Simplest possible plotting example") + +data = np.random.normal(size=(500,500)) +pg.show(data, title="Simplest possible image example") + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/DataSlicing.py b/examples/DataSlicing.py new file mode 100644 index 00000000..32b9c584 --- /dev/null +++ b/examples/DataSlicing.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + + +import numpy as np +import scipy +from PyQt4 import QtCore, QtGui +import pyqtgraph as pg + +app = QtGui.QApplication([]) + +## Create window with two ImageView widgets +win = QtGui.QMainWindow() +win.resize(800,800) +cw = QtGui.QWidget() +win.setCentralWidget(cw) +l = QtGui.QGridLayout() +cw.setLayout(l) +imv1 = pg.ImageView() +imv2 = pg.ImageView() +l.addWidget(imv1, 0, 0) +l.addWidget(imv2, 1, 0) +win.show() + +roi = pg.LineSegmentROI([[10, 64], [120,64]], pen='r') +imv1.addItem(roi) + +x1 = np.linspace(-30, 10, 128)[:, np.newaxis, np.newaxis] +x2 = np.linspace(-20, 20, 128)[:, np.newaxis, np.newaxis] +y = np.linspace(-30, 10, 128)[np.newaxis, :, np.newaxis] +z = np.linspace(-20, 20, 128)[np.newaxis, np.newaxis, :] +d1 = np.sqrt(x1**2 + y**2 + z**2) +d2 = 2*np.sqrt(x1[::-1]**2 + y**2 + z**2) +d3 = 4*np.sqrt(x2**2 + y[:,::-1]**2 + z**2) +data = (np.sin(d1) / d1**2) + (np.sin(d2) / d2**2) + (np.sin(d3) / d3**2) + +def update(): + global data, imv1, imv2 + d2 = roi.getArrayRegion(data, imv1.imageItem, axes=(1,2)) + imv2.setImage(d2) + +roi.sigRegionChanged.connect(update) + + +## Display the data +imv1.setImage(data) +imv1.setHistogramRange(data.min(), data.max()) + +update() + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_draw.py b/examples/Draw.py old mode 100755 new mode 100644 similarity index 80% rename from examples/test_draw.py rename to examples/Draw.py index b40932ba..83736cc4 --- a/examples/test_draw.py +++ b/examples/Draw.py @@ -20,7 +20,6 @@ win.show() ## Allow mouse scale/pan view.enableMouse() - ## ..But lock the aspect ratio view.setAspectLocked(True) @@ -31,8 +30,14 @@ view.scene().addItem(img) ## Set initial view bounds view.setRange(QtCore.QRectF(0, 0, 200, 200)) -img.setDrawKernel(1) -img.setLevels(10,0) +## start drawing with 3x3 brush +kern = np.array([ + [0.0, 0.5, 0.0], + [0.5, 1.0, 0.5], + [0.0, 0.5, 0.0] +]) +img.setDrawKernel(kern, mask=kern, center=(1,1), mode='add') +img.setLevels([0, 10]) ## Start Qt event loop unless running in interactive mode. if sys.flags.interactive != 1: diff --git a/examples/Flowchart.py b/examples/Flowchart.py new file mode 100644 index 00000000..749fd3b6 --- /dev/null +++ b/examples/Flowchart.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import sys, os + +## Make sure pyqtgraph is importable +p = os.path.dirname(os.path.abspath(__file__)) +p = os.path.join(p, '..', '..') +sys.path.insert(0, p) + + +from pyqtgraph.flowchart import Flowchart +from pyqtgraph.Qt import QtGui + +#import pyqtgraph.flowchart as f + +app = QtGui.QApplication([]) + +#TETRACYCLINE = True + +fc = Flowchart(terminals={ + 'dataIn': {'io': 'in'}, + 'dataOut': {'io': 'out'} +}) +w = fc.widget() +w.resize(400,200) +w.show() + +n1 = fc.createNode('Add') +n2 = fc.createNode('Subtract') +n3 = fc.createNode('Abs') +n4 = fc.createNode('Add') + +fc.connectTerminals(fc.dataIn, n1.A) +fc.connectTerminals(fc.dataIn, n1.B) +fc.connectTerminals(fc.dataIn, n2.A) +fc.connectTerminals(n1.Out, n4.A) +fc.connectTerminals(n1.Out, n2.B) +fc.connectTerminals(n2.Out, n3.In) +fc.connectTerminals(n3.Out, n4.B) +fc.connectTerminals(n4.Out, fc.dataOut) + + +def process(**kargs): + return fc.process(**kargs) + + +print process(dataIn=7) + +fc.setInput(dataIn=3) + +s = fc.saveState() +fc.clear() + +fc.restoreState(s) + +fc.setInput(dataIn=3) + +#f.NodeMod.TETRACYCLINE = False + +if sys.flags.interactive == 0: + app.exec_() + diff --git a/examples/GradientEditor.py b/examples/GradientEditor.py new file mode 100644 index 00000000..f22479db --- /dev/null +++ b/examples/GradientEditor.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +import numpy as np +from PyQt4 import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) +mw = pg.GraphicsView() +mw.resize(800,800) +mw.show() + +#ts = pg.TickSliderItem() +#mw.setCentralItem(ts) +#ts.addTick(0.5, 'r') +#ts.addTick(0.9, 'b') + +ge = pg.GradientEditorItem() +mw.setCentralItem(ge) + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/GraphicsLayout.py b/examples/GraphicsLayout.py new file mode 100755 index 00000000..940d450f --- /dev/null +++ b/examples/GraphicsLayout.py @@ -0,0 +1,46 @@ +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from PyQt4 import QtGui, QtCore +import pyqtgraph as pg +import user + +app = QtGui.QApplication([]) +view = pg.GraphicsView() +l = pg.GraphicsLayout(border=pg.mkPen(0, 0, 255)) +view.setCentralItem(l) +view.show() + +## Add 3 plots into the first row (automatic position) +p1 = l.addPlot() +p2 = l.addPlot() +p3 = l.addPlot() + +## Add a viewbox into the second row (automatic position) +l.nextRow() +vb = l.addViewBox(colspan=3) + +## Add 2 more plots into the third row (manual position) +p4 = l.addPlot(row=2, col=0) +p5 = l.addPlot(row=2, col=1, colspan=2) + + + +## show some content +p1.plot([1,3,2,4,3,5]) +p2.plot([1,3,2,4,3,5]) +p3.plot([1,3,2,4,3,5]) +p4.plot([1,3,2,4,3,5]) +p5.plot([1,3,2,4,3,5]) + +b = QtGui.QGraphicsRectItem(0, 0, 1, 1) +b.setPen(pg.mkPen(255,255,0)) +vb.addItem(b) +vb.setRange(QtCore.QRectF(-1, -1, 3, 3)) + + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/GraphicsScene.py b/examples/GraphicsScene.py new file mode 100644 index 00000000..9720f65b --- /dev/null +++ b/examples/GraphicsScene.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from PyQt4 import QtCore, QtGui +import pyqtgraph as pg +from pyqtgraph.GraphicsScene import GraphicsScene + +app = QtGui.QApplication([]) +win = pg.GraphicsView() +win.show() + + +class Obj(QtGui.QGraphicsObject): + def __init__(self): + QtGui.QGraphicsObject.__init__(self) + GraphicsScene.registerObject(self) + + def paint(self, p, *args): + p.setPen(pg.mkPen(200,200,200)) + p.drawRect(self.boundingRect()) + + def boundingRect(self): + return QtCore.QRectF(0, 0, 20, 20) + + def mouseClickEvent(self, ev): + if ev.double(): + print "double click" + else: + print "click" + ev.accept() + + #def mouseDragEvent(self, ev): + #print "drag" + #ev.accept() + #self.setPos(self.pos() + ev.pos()-ev.lastPos()) + + + +vb = pg.ViewBox() +win.setCentralItem(vb) + +obj = Obj() +vb.addItem(obj) + +obj2 = Obj() +win.addItem(obj2) + +def clicked(): + print "button click" +btn = QtGui.QPushButton("BTN") +btn.clicked.connect(clicked) +prox = QtGui.QGraphicsProxyWidget() +prox.setWidget(btn) +prox.setPos(100,0) +vb.addItem(prox) + +g = pg.GridItem() +vb.addItem(g) + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/HistogramLUT.py b/examples/HistogramLUT.py new file mode 100644 index 00000000..114da050 --- /dev/null +++ b/examples/HistogramLUT.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +import numpy as np +import scipy.ndimage as ndi +from PyQt4 import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) +win = QtGui.QMainWindow() +win.resize(800,600) +win.show() + +cw = QtGui.QWidget() +win.setCentralWidget(cw) + +l = QtGui.QGridLayout() +cw.setLayout(l) +l.setSpacing(0) + +v = pg.GraphicsView() +vb = pg.ViewBox() +vb.setAspectLocked() +v.setCentralItem(vb) +l.addWidget(v, 0, 0) + +w = pg.HistogramLUTWidget() +l.addWidget(w, 0, 1) + +data = ndi.gaussian_filter(np.random.normal(size=(256, 256)), (20, 20)) +for i in range(32): + for j in range(32): + data[i*8, j*8] += .1 +img = pg.ImageItem(data) +#data2 = np.zeros((2,) + data.shape + (2,)) +#data2[0,:,:,0] = data ## make non-contiguous array for testing purposes +#img = pg.ImageItem(data2[0,:,:,0]) +vb.addItem(img) +vb.autoRange() + +w.setImageItem(img) + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_ImageItem.py b/examples/ImageItem.py old mode 100755 new mode 100644 similarity index 53% rename from examples/test_ImageItem.py rename to examples/ImageItem.py index f48f0f51..6697e93c --- a/examples/test_ImageItem.py +++ b/examples/ImageItem.py @@ -2,7 +2,7 @@ ## Add path to library (just for examples; you do not need this) import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - +import ptime from PyQt4 import QtCore, QtGui import numpy as np @@ -14,55 +14,48 @@ app = QtGui.QApplication([]) win = QtGui.QMainWindow() win.resize(800,800) view = pg.GraphicsView() -#view.useOpenGL(True) win.setCentralWidget(view) win.show() -## Allow mouse scale/pan +## Allow mouse scale/pan. Normally we use a ViewBox for this, but +## for simple examples this is easier. view.enableMouse() -## ..But lock the aspect ratio +## lock the aspect ratio so pixels are always square view.setAspectLocked(True) ## Create image item -img = pg.ImageItem() +img = pg.ImageItem(border='w') view.scene().addItem(img) ## Set initial view bounds -view.setRange(QtCore.QRectF(0, 0, 200, 200)) +view.setRange(QtCore.QRectF(0, 0, 600, 600)) ## Create random image -data = np.random.normal(size=(50, 200, 200)) +data = np.random.normal(size=(15, 600, 600), loc=1024, scale=64).astype(np.uint16) i = 0 +updateTime = ptime.time() +fps = 0 + def updateData(): - global img, data, i + global img, data, i, updateTime, fps ## Display the data - img.updateImage(data[i]) + img.setImage(data[i]) i = (i+1) % data.shape[0] - QtCore.QTimer.singleShot(20, updateData) + QtCore.QTimer.singleShot(1, updateData) + now = ptime.time() + fps2 = 1.0 / (now-updateTime) + updateTime = now + fps = fps * 0.9 + fps2 * 0.1 + + #print "%0.1f fps" % fps -# update image data every 20ms (or so) -#t = QtCore.QTimer() -#t.timeout.connect(updateData) -#t.start(20) updateData() - -def doWork(): - while True: - x = '.'.join(['%f'%i for i in range(100)]) ## some work for the thread to do - if time is None: ## main thread has started cleaning up, bail out now - break - time.sleep(1e-3) - -import thread -thread.start_new_thread(doWork, ()) - - ## Start Qt event loop unless running in interactive mode. if sys.flags.interactive != 1: app.exec_() diff --git a/examples/test_ImageView.py b/examples/ImageView.py old mode 100755 new mode 100644 similarity index 100% rename from examples/test_ImageView.py rename to examples/ImageView.py diff --git a/examples/test_MultiPlotWidget.py b/examples/MultiPlotWidget.py old mode 100755 new mode 100644 similarity index 94% rename from examples/test_MultiPlotWidget.py rename to examples/MultiPlotWidget.py index 4c72275b..9e5878a2 --- a/examples/test_MultiPlotWidget.py +++ b/examples/MultiPlotWidget.py @@ -9,7 +9,7 @@ from scipy import random from numpy import linspace from PyQt4 import QtGui, QtCore import pyqtgraph as pg -from pyqtgraph.MultiPlotWidget import MultiPlotWidget +from pyqtgraph import MultiPlotWidget try: from metaarray import * except: diff --git a/examples/PlotSpeedTest.py b/examples/PlotSpeedTest.py new file mode 100644 index 00000000..3010270b --- /dev/null +++ b/examples/PlotSpeedTest.py @@ -0,0 +1,46 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os, time +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + + +from PyQt4 import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +p = pg.plot() + +curve = p.plot() +data = np.random.normal(size=(10,50000)) +ptr = 0 +lastTime = time.time() +fps = None +def update(): + global curve, data, ptr, p, lastTime, fps + curve.setData(data[ptr%10]) + ptr += 1 + now = time.time() + dt = now - lastTime + lastTime = now + if fps is None: + fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + fps = fps * (1-s) + (1.0/dt) * s + p.setTitle('%0.2f fps' % fps) + app.processEvents() ## force complete redraw for every plot +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_PlotWidget.py b/examples/PlotWidget.py old mode 100755 new mode 100644 similarity index 76% rename from examples/test_PlotWidget.py rename to examples/PlotWidget.py index 2b2ef496..cecbb58e --- a/examples/test_PlotWidget.py +++ b/examples/PlotWidget.py @@ -32,10 +32,14 @@ p1 = pw.plot() p1.setPen((200,200,100)) ## Add in some extra graphics -rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, 1, 1)) +rect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, 0, 1, 5e-11)) rect.setPen(QtGui.QPen(QtGui.QColor(100, 200, 100))) pw.addItem(rect) +pw.setLabel('left', 'Value', units='V') +pw.setLabel('bottom', 'Time', units='s') +pw.setXRange(0, 2) +pw.setYRange(0, 1e-10) def rand(n): data = np.random.random(n) @@ -49,12 +53,13 @@ def rand(n): def updateData(): yd, xd = rand(10000) - p1.updateData(yd, x=xd) + p1.setData(y=yd, x=xd) ## Start a timer to rapidly update the plot in pw t = QtCore.QTimer() t.timeout.connect(updateData) t.start(50) +#updateData() ## Multiple parameterized plots--we can autogenerate averages for these. for i in range(0, 5): @@ -63,10 +68,19 @@ for i in range(0, 5): pw2.plot(y=yd*(j+1), x=xd, params={'iter': i, 'val': j}) ## Test large numbers -curve = pw3.plot(np.random.normal(size=100)*1e6) +curve = pw3.plot(np.random.normal(size=100)*1e0, clickable=True) curve.setPen('w') ## white pen curve.setShadowPen(pg.mkPen((70,70,30), width=6, cosmetic=True)) +def clicked(): + print "curve clicked" +curve.sigClicked.connect(clicked) + +lr = pg.LinearRegionItem([1, 30], bounds=[0,100], movable=True) +pw3.addItem(lr) +line = pg.InfiniteLine(angle=90, movable=True) +pw3.addItem(line) +line.setBounds([0,200]) ## Start Qt event loop unless running in interactive mode. if sys.flags.interactive != 1: diff --git a/examples/Plotting.py b/examples/Plotting.py new file mode 100644 index 00000000..cfeb4786 --- /dev/null +++ b/examples/Plotting.py @@ -0,0 +1,72 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + + +from PyQt4 import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +win = pg.GraphicsWindow(title="Basic plotting examples") +win.resize(800,600) + + + +p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100)) + +p2 = win.addPlot(title="Multiple curves") +p2.plot(np.random.normal(size=100), pen=(255,0,0)) +p2.plot(np.random.normal(size=100)+5, pen=(0,255,0)) +p2.plot(np.random.normal(size=100)+10, pen=(0,0,255)) + +p3 = win.addPlot(title="Drawing with points") +p3.plot(np.random.normal(size=100), pen=(200,200,200), symbolBrush=(255,0,0), symbolPen='w') + + +win.nextRow() + +p4 = win.addPlot(title="Parametric") +x = np.cos(np.linspace(0, 2*np.pi, 1000)) +y = np.sin(np.linspace(0, 4*np.pi, 1000)) +p4.plot(x, y) + +p5 = win.addPlot(title="Scatter plot with labels") +x = np.random.normal(size=1000) * 1e-5 +y = x*1000 + 0.005 * np.random.normal(size=1000) +p5.plot(x, y, pen=None, symbol='t', symbolPen=None, symbolSize=10, symbolBrush=(100, 100, 255, 50)) +p5.setLabel('left', "Y Axis", units='A') +p5.setLabel('bottom', "Y Axis", units='s') + + +p6 = win.addPlot(title="Updating plot") +curve = p6.plot(pen='y') +data = np.random.normal(size=(10,1000)) +ptr = 0 +def update(): + global curve, data, ptr, p6 + curve.setData(data[ptr%10]) + if ptr == 0: + p6.enableAutoRange('xy', False) + ptr += 1 +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(50) + + +win.nextRow() + +p7 = win.addPlot(title="Filled plot") +y = np.sin(np.linspace(0, 10, 1000)) + np.random.normal(size=1000, scale=0.1) +p7.plot(y, fillLevel=-0.3, brush=(50,50,200,100)) + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/test_ROItypes.py b/examples/ROItypes.py old mode 100755 new mode 100644 similarity index 58% rename from examples/test_ROItypes.py rename to examples/ROItypes.py index f080e0b4..7649baaa --- a/examples/test_ROItypes.py +++ b/examples/ROItypes.py @@ -11,17 +11,22 @@ import pyqtgraph as pg ## create GUI app = QtGui.QApplication([]) -w = QtGui.QMainWindow() -w.resize(800,800) -v = pg.GraphicsView() -#v.invertY(True) ## Images usually have their Y-axis pointing downward + +w = pg.GraphicsWindow(size=(800,800), border=True) + +v = w.addViewBox(colspan=2) + +#w = QtGui.QMainWindow() +#w.resize(800,800) +#v = pg.GraphicsView() +v.invertY(True) ## Images usually have their Y-axis pointing downward v.setAspectLocked(True) -v.enableMouse(True) -v.autoPixelScale = False -w.setCentralWidget(v) -s = v.scene() -v.setRange(QtCore.QRect(-2, -2, 220, 220)) -w.show() +#v.enableMouse(True) +#v.autoPixelScale = False +#w.setCentralWidget(v) +#s = v.scene() +#v.setRange(QtCore.QRectF(-2, -2, 220, 220)) + ## Create image to display arr = np.ones((100, 100), dtype=float) @@ -36,23 +41,35 @@ arr[:, 50] = 10 ## Create image items, add to scene and set position im1 = pg.ImageItem(arr) im2 = pg.ImageItem(arr) -s.addItem(im1) -s.addItem(im2) +v.addItem(im1) +v.addItem(im2) im2.moveBy(110, 20) +v.setRange(QtCore.QRectF(0, 0, 200, 120)) + im3 = pg.ImageItem() -s.addItem(im3) -im3.moveBy(0, 130) +v2 = w.addViewBox(1,0) +v2.addItem(im3) +v2.setRange(QtCore.QRectF(0, 0, 60, 60)) +v2.invertY(True) +v2.setAspectLocked(True) +#im3.moveBy(0, 130) im3.setZValue(10) + im4 = pg.ImageItem() -s.addItem(im4) -im4.moveBy(110, 130) +v3 = w.addViewBox(1,1) +v3.addItem(im4) +v3.setRange(QtCore.QRectF(0, 0, 60, 60)) +v3.invertY(True) +v3.setAspectLocked(True) +#im4.moveBy(110, 130) im4.setZValue(10) ## create the plot -pi1 = pg.PlotItem() -s.addItem(pi1) -pi1.scale(0.5, 0.5) -pi1.setGeometry(0, 170, 300, 100) +pi1 = w.addPlot(2,0, colspan=2) +#pi1 = pg.PlotItem() +#s.addItem(pi1) +#pi1.scale(0.5, 0.5) +#pi1.setGeometry(0, 170, 300, 100) lastRoi = None @@ -62,31 +79,31 @@ def updateRoi(roi): return lastRoi = roi arr1 = roi.getArrayRegion(im1.image, img=im1) - im3.updateImage(arr1, autoRange=True) + im3.setImage(arr1) arr2 = roi.getArrayRegion(im2.image, img=im2) - im4.updateImage(arr2, autoRange=True) + im4.setImage(arr2) updateRoiPlot(roi, arr1) def updateRoiPlot(roi, data=None): if data is None: data = roi.getArrayRegion(im1.image, img=im1) if data is not None: - roi.curve.updateData(data.mean(axis=1)) + roi.curve.setData(data.mean(axis=1)) ## Create a variety of different ROI types rois = [] -rois.append(pg.widgets.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9))) -rois.append(pg.widgets.LineROI([0, 0], [20, 20], width=5, pen=(1,9))) -rois.append(pg.widgets.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) -rois.append(pg.widgets.EllipseROI([110, 10], [30, 20], pen=(3,9))) -rois.append(pg.widgets.CircleROI([110, 50], [20, 20], pen=(4,9))) -rois.append(pg.widgets.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) +rois.append(pg.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9))) +rois.append(pg.LineROI([0, 0], [20, 20], width=5, pen=(1,9))) +rois.append(pg.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) +rois.append(pg.EllipseROI([110, 10], [30, 20], pen=(3,9))) +rois.append(pg.CircleROI([110, 50], [20, 20], pen=(4,9))) +rois.append(pg.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) #rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0))) ## Add each ROI to the scene and link its data to a plot curve with the same color for r in rois: - s.addItem(r) + v.addItem(r) c = pi1.plot(pen=r.pen) r.curve = c r.sigRegionChanged.connect(updateRoi) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py new file mode 100755 index 00000000..da9d4750 --- /dev/null +++ b/examples/ScatterPlot.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +import sys, os +## Add path to library (just for examples; you do not need this) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from PyQt4 import QtGui, QtCore +import pyqtgraph as pg +import numpy as np + +app = QtGui.QApplication([]) +mw = QtGui.QMainWindow() +mw.resize(800,800) +view = pg.GraphicsLayoutWidget() ## GraphicsView with GraphicsLayout inserted by default +mw.setCentralWidget(view) +mw.show() + +## create four areas to add plots +w1 = view.addPlot() +w2 = view.addViewBox() +w2.setAspectLocked(True) +view.nextRow() +w3 = view.addPlot() +w4 = view.addPlot() +print "Generating data, this takes a few seconds..." + +## There are a few different ways we can draw scatter plots; each is optimized for different types of data: + + +## 1) All spots identical and transform-invariant (top-left plot). +## In this case we can get a huge performance boost by pre-rendering the spot +## image and just drawing that image repeatedly. (use identical=True in the constructor) +## (An even faster approach might be to use QPainter.drawPixmapFragments) + +n = 300 +s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 20), identical=True) +pos = np.random.normal(size=(2,n), scale=1e-5) +spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}] +s1.addPoints(spots) +w1.addItem(s1) + +## This plot is clickable +def clicked(plot, points): + print "clicked points", points +s1.sigClicked.connect(clicked) + + + +## 2) Spots are transform-invariant, but not identical (top-right plot). +## In this case, drawing is as fast as 1), but there is more startup overhead +## and memory usage since each spot generates its own pre-rendered image. + +s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) +pos = np.random.normal(size=(2,n), scale=1e-5) +spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'style': i%5, 'size': 5+i/10.} for i in xrange(n)] +s2.addPoints(spots) +w2.addItem(s2) +w2.setRange(s2.boundingRect()) +s2.sigClicked.connect(clicked) + + +## 3) Spots are not transform-invariant, not identical (bottom-left). +## This is the slowest case, since all spots must be completely re-drawn +## every time because their apparent transformation may have changed. + +s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to transform with the view +spots3 = [] +for i in range(10): + for j in range(10): + spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'brush':pg.intColor(i*10+j, 100)}) +s3.addPoints(spots3) +w3.addItem(s3) +s3.sigClicked.connect(clicked) + + +## Coming: use qpainter.drawpixmapfragments for scatterplots which do not require mouse interaction + +s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 20), identical=True) +pos = np.random.normal(size=(2,10000), scale=1e-9) +s4.addPoints(x=pos[0], y=pos[1]) +w4.addItem(s4) + + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() + diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py new file mode 100644 index 00000000..4a1d5fb4 --- /dev/null +++ b/examples/VideoSpeedTest.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os, time +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + + +from PyQt4 import QtGui, QtCore +import numpy as np +import pyqtgraph as pg +from pyqtgraph import RawImageWidget +import scipy.ndimage as ndi +import pyqtgraph.ptime as ptime +import VideoTemplate + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +win = QtGui.QMainWindow() +ui = VideoTemplate.Ui_MainWindow() +ui.setupUi(win) +win.show() +ui.maxSpin1.setOpts(value=255, step=1) +ui.minSpin1.setOpts(value=0, step=1) + +vb = pg.ViewBox() +ui.graphicsView.setCentralItem(vb) +vb.setAspectLocked() +img = pg.ImageItem() +vb.addItem(img) +vb.setRange(QtCore.QRectF(0, 0, 512, 512)) + +LUT = None +def updateLUT(): + global LUT, ui + dtype = ui.dtypeCombo.currentText() + if dtype == 'uint8': + n = 256 + else: + n = 4096 + LUT = ui.gradient.getLookupTable(n, alpha=ui.alphaCheck.isChecked()) +ui.gradient.sigGradientChanged.connect(updateLUT) +updateLUT() + +ui.alphaCheck.toggled.connect(updateLUT) + +def updateScale(): + global ui + spins = [ui.minSpin1, ui.maxSpin1, ui.minSpin2, ui.maxSpin2, ui.minSpin3, ui.maxSpin3] + if ui.rgbCheck.isChecked(): + for s in spins[2:]: + s.setEnabled(True) + else: + for s in spins[2:]: + s.setEnabled(False) +ui.rgbCheck.toggled.connect(updateScale) + +cache = {} +def mkData(): + global data, cache, ui + dtype = ui.dtypeCombo.currentText() + if dtype not in cache: + if dtype == 'uint8': + dt = np.uint8 + loc = 128 + scale = 64 + mx = 255 + elif dtype == 'uint16': + dt = np.uint16 + loc = 4096 + scale = 1024 + mx = 2**16 + elif dtype == 'float': + dt = np.float + loc = 1.0 + scale = 0.1 + + data = np.random.normal(size=(20,512,512), loc=loc, scale=scale) + data = ndi.gaussian_filter(data, (0, 3, 3)) + if dtype != 'float': + data = np.clip(data, 0, mx) + data = data.astype(dt) + cache[dtype] = data + + data = cache[dtype] + updateLUT() +mkData() +ui.dtypeCombo.currentIndexChanged.connect(mkData) + + +ptr = 0 +lastTime = ptime.time() +fps = None +def update(): + global ui, ptr, lastTime, fps, LUT, img + if ui.lutCheck.isChecked(): + useLut = LUT + else: + useLut = None + + if ui.scaleCheck.isChecked(): + if ui.rgbCheck.isChecked(): + useScale = [ + [ui.minSpin1.value(), ui.maxSpin1.value()], + [ui.minSpin2.value(), ui.maxSpin2.value()], + [ui.minSpin3.value(), ui.maxSpin3.value()]] + else: + useScale = [ui.minSpin1.value(), ui.maxSpin1.value()] + else: + useScale = None + + if ui.rawRadio.isChecked(): + ui.rawImg.setImage(data[ptr%data.shape[0]], lut=useLut, levels=useScale) + else: + img.setImage(data[ptr%data.shape[0]], autoLevels=False, levels=useScale, lut=useLut) + #img.setImage(data[ptr%data.shape[0]], autoRange=False) + + ptr += 1 + now = ptime.time() + dt = now - lastTime + lastTime = now + if fps is None: + fps = 1.0/dt + else: + s = np.clip(dt*3., 0, 1) + fps = fps * (1-s) + (1.0/dt) * s + ui.fpsLabel.setText('%0.2f fps' % fps) + app.processEvents() ## force complete redraw for every plot +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(0) + + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/VideoTemplate.py b/examples/VideoTemplate.py new file mode 100644 index 00000000..d4f712d5 --- /dev/null +++ b/examples/VideoTemplate.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'VideoTemplate.ui' +# +# Created: Sun Jan 8 19:22:32 2012 +# by: PyQt4 UI code generator 4.8.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName(_fromUtf8("MainWindow")) + MainWindow.resize(985, 674) + self.centralwidget = QtGui.QWidget(MainWindow) + self.centralwidget.setObjectName(_fromUtf8("centralwidget")) + self.gridLayout_2 = QtGui.QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.gridLayout = QtGui.QGridLayout() + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.rawImg = RawImageWidget(self.centralwidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) + self.rawImg.setSizePolicy(sizePolicy) + self.rawImg.setObjectName(_fromUtf8("rawImg")) + self.gridLayout.addWidget(self.rawImg, 0, 0, 1, 1) + self.graphicsView = GraphicsView(self.centralwidget) + self.graphicsView.setObjectName(_fromUtf8("graphicsView")) + self.gridLayout.addWidget(self.graphicsView, 0, 1, 1, 1) + self.rawRadio = QtGui.QRadioButton(self.centralwidget) + self.rawRadio.setChecked(True) + self.rawRadio.setObjectName(_fromUtf8("rawRadio")) + self.gridLayout.addWidget(self.rawRadio, 1, 0, 1, 1) + self.gfxRadio = QtGui.QRadioButton(self.centralwidget) + self.gfxRadio.setObjectName(_fromUtf8("gfxRadio")) + self.gridLayout.addWidget(self.gfxRadio, 1, 1, 1, 1) + self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) + self.label = QtGui.QLabel(self.centralwidget) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout_2.addWidget(self.label, 2, 0, 1, 1) + self.dtypeCombo = QtGui.QComboBox(self.centralwidget) + self.dtypeCombo.setObjectName(_fromUtf8("dtypeCombo")) + self.dtypeCombo.addItem(_fromUtf8("")) + self.dtypeCombo.addItem(_fromUtf8("")) + self.dtypeCombo.addItem(_fromUtf8("")) + self.gridLayout_2.addWidget(self.dtypeCombo, 2, 2, 1, 1) + self.scaleCheck = QtGui.QCheckBox(self.centralwidget) + self.scaleCheck.setObjectName(_fromUtf8("scaleCheck")) + self.gridLayout_2.addWidget(self.scaleCheck, 3, 0, 1, 1) + self.rgbCheck = QtGui.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName(_fromUtf8("rgbCheck")) + self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.minSpin1 = SpinBox(self.centralwidget) + self.minSpin1.setObjectName(_fromUtf8("minSpin1")) + self.horizontalLayout.addWidget(self.minSpin1) + self.label_2 = QtGui.QLabel(self.centralwidget) + self.label_2.setAlignment(QtCore.Qt.AlignCenter) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.horizontalLayout.addWidget(self.label_2) + self.maxSpin1 = SpinBox(self.centralwidget) + self.maxSpin1.setObjectName(_fromUtf8("maxSpin1")) + self.horizontalLayout.addWidget(self.maxSpin1) + self.gridLayout_2.addLayout(self.horizontalLayout, 3, 2, 1, 1) + self.horizontalLayout_2 = QtGui.QHBoxLayout() + self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) + self.minSpin2 = SpinBox(self.centralwidget) + self.minSpin2.setEnabled(False) + self.minSpin2.setObjectName(_fromUtf8("minSpin2")) + self.horizontalLayout_2.addWidget(self.minSpin2) + self.label_3 = QtGui.QLabel(self.centralwidget) + self.label_3.setAlignment(QtCore.Qt.AlignCenter) + self.label_3.setObjectName(_fromUtf8("label_3")) + self.horizontalLayout_2.addWidget(self.label_3) + self.maxSpin2 = SpinBox(self.centralwidget) + self.maxSpin2.setEnabled(False) + self.maxSpin2.setObjectName(_fromUtf8("maxSpin2")) + self.horizontalLayout_2.addWidget(self.maxSpin2) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 4, 2, 1, 1) + self.horizontalLayout_3 = QtGui.QHBoxLayout() + self.horizontalLayout_3.setObjectName(_fromUtf8("horizontalLayout_3")) + self.minSpin3 = SpinBox(self.centralwidget) + self.minSpin3.setEnabled(False) + self.minSpin3.setObjectName(_fromUtf8("minSpin3")) + self.horizontalLayout_3.addWidget(self.minSpin3) + self.label_4 = QtGui.QLabel(self.centralwidget) + self.label_4.setAlignment(QtCore.Qt.AlignCenter) + self.label_4.setObjectName(_fromUtf8("label_4")) + self.horizontalLayout_3.addWidget(self.label_4) + self.maxSpin3 = SpinBox(self.centralwidget) + self.maxSpin3.setEnabled(False) + self.maxSpin3.setObjectName(_fromUtf8("maxSpin3")) + self.horizontalLayout_3.addWidget(self.maxSpin3) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 5, 2, 1, 1) + self.lutCheck = QtGui.QCheckBox(self.centralwidget) + self.lutCheck.setObjectName(_fromUtf8("lutCheck")) + self.gridLayout_2.addWidget(self.lutCheck, 6, 0, 1, 1) + self.alphaCheck = QtGui.QCheckBox(self.centralwidget) + self.alphaCheck.setObjectName(_fromUtf8("alphaCheck")) + self.gridLayout_2.addWidget(self.alphaCheck, 6, 1, 1, 1) + self.gradient = GradientWidget(self.centralwidget) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) + self.gradient.setSizePolicy(sizePolicy) + self.gradient.setObjectName(_fromUtf8("gradient")) + self.gridLayout_2.addWidget(self.gradient, 6, 2, 1, 2) + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 2, 3, 1, 1) + self.fpsLabel = QtGui.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setPointSize(12) + self.fpsLabel.setFont(font) + self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fpsLabel.setObjectName(_fromUtf8("fpsLabel")) + self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtGui.QApplication.translate("MainWindow", "MainWindow", None, QtGui.QApplication.UnicodeUTF8)) + self.rawRadio.setText(QtGui.QApplication.translate("MainWindow", "RawImageWidget (unscaled; faster)", None, QtGui.QApplication.UnicodeUTF8)) + self.gfxRadio.setText(QtGui.QApplication.translate("MainWindow", "GraphicsView + ImageItem (scaled; slower)", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("MainWindow", "Data type", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(0, QtGui.QApplication.translate("MainWindow", "uint8", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(1, QtGui.QApplication.translate("MainWindow", "uint16", None, QtGui.QApplication.UnicodeUTF8)) + self.dtypeCombo.setItemText(2, QtGui.QApplication.translate("MainWindow", "float", None, QtGui.QApplication.UnicodeUTF8)) + self.scaleCheck.setText(QtGui.QApplication.translate("MainWindow", "Scale Data", None, QtGui.QApplication.UnicodeUTF8)) + self.rgbCheck.setText(QtGui.QApplication.translate("MainWindow", "RGB", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.label_4.setText(QtGui.QApplication.translate("MainWindow", "<--->", None, QtGui.QApplication.UnicodeUTF8)) + self.lutCheck.setText(QtGui.QApplication.translate("MainWindow", "Use Lookup Table", None, QtGui.QApplication.UnicodeUTF8)) + self.alphaCheck.setText(QtGui.QApplication.translate("MainWindow", "alpha", None, QtGui.QApplication.UnicodeUTF8)) + self.fpsLabel.setText(QtGui.QApplication.translate("MainWindow", "FPS", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph import SpinBox, GradientWidget, GraphicsView, RawImageWidget diff --git a/examples/VideoTemplate.ui b/examples/VideoTemplate.ui new file mode 100644 index 00000000..078e7ccf --- /dev/null +++ b/examples/VideoTemplate.ui @@ -0,0 +1,250 @@ + + + MainWindow + + + + 0 + 0 + 985 + 674 + + + + MainWindow + + + + + + + + + + 0 + 0 + + + fpsLabel + + + + + + + + + RawImageWidget (unscaled; faster) + + + true + + + + + + + GraphicsView + ImageItem (scaled; slower) + + + + + + + + + Data type + + + + + + + + uint8 + + + + + uint16 + + + + + float + + + + + + + + Scale Data + + + + + + + RGB + + + + + + + + + + + + <---> + + + Qt::AlignCenter + + + + + + + + + + + + + + false + + + + + + + <---> + + + Qt::AlignCenter + + + + + + + false + + + + + + + + + + + false + + + + + + + <---> + + + Qt::AlignCenter + + + + + + + false + + + + + + + + + Use Lookup Table + + + + + + + alpha + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 12 + + + + FPS + + + Qt::AlignCenter + + + + + + + + + GraphicsView + QGraphicsView +
pyqtgraph
+
+ + RawImageWidget + QWidget +
pyqtgraph
+ 1 +
+ + GradientWidget + QWidget +
pyqtgraph
+ 1 +
+ + SpinBox + QDoubleSpinBox +
pyqtgraph
+
+
+ + +
diff --git a/examples/test_viewBox.py b/examples/ViewBox.py similarity index 83% rename from examples/test_viewBox.py rename to examples/ViewBox.py index 0b8db232..6e30a7e6 100755 --- a/examples/test_viewBox.py +++ b/examples/ViewBox.py @@ -13,25 +13,28 @@ import pyqtgraph as pg app = QtGui.QApplication([]) mw = QtGui.QMainWindow() -cw = QtGui.QWidget() -vl = QtGui.QVBoxLayout() -cw.setLayout(vl) -mw.setCentralWidget(cw) +#cw = QtGui.QWidget() +#vl = QtGui.QVBoxLayout() +#cw.setLayout(vl) +#mw.setCentralWidget(cw) mw.show() mw.resize(800, 600) - -gv = pg.GraphicsView(cw) -gv.enableMouse(False) ## Mouse interaction will be handled by the ViewBox +gv = pg.GraphicsView() +mw.setCentralWidget(gv) +#gv.enableMouse(False) ## Mouse interaction will be handled by the ViewBox l = QtGui.QGraphicsGridLayout() l.setHorizontalSpacing(0) l.setVerticalSpacing(0) +#vl.addWidget(gv) vb = pg.ViewBox() -p1 = pg.PlotCurveItem() +#grid = pg.GridItem() +#vb.addItem(grid) + +p1 = pg.PlotDataItem() vb.addItem(p1) -vl.addWidget(gv) class movableRect(QtGui.QGraphicsRectItem): def __init__(self, *args): @@ -63,9 +66,9 @@ l.addItem(vb, 0, 1) gv.centralWidget.setLayout(l) -xScale = pg.ScaleItem(orientation='bottom', linkView=vb) +xScale = pg.AxisItem(orientation='bottom', linkView=vb) l.addItem(xScale, 1, 1) -yScale = pg.ScaleItem(orientation='left', linkView=vb) +yScale = pg.AxisItem(orientation='left', linkView=vb) l.addItem(yScale, 0, 0) xScale.setLabel(text=u"X Axis", units="s") @@ -82,7 +85,7 @@ def rand(n): def updateData(): yd, xd = rand(10000) - p1.updateData(yd, x=xd) + p1.setData(y=yd, x=xd) yd, xd = rand(10000) updateData() diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 00000000..23b7cd58 --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1 @@ +from __main__ import run diff --git a/examples/__main__.py b/examples/__main__.py new file mode 100644 index 00000000..f8fe4c76 --- /dev/null +++ b/examples/__main__.py @@ -0,0 +1,101 @@ +from PyQt4 import QtCore, QtGui +from exampleLoaderTemplate import Ui_Form +import os, sys +from collections import OrderedDict + +examples = OrderedDict([ + ('Command-line usage', 'CLIexample.py'), + ('Basic Plotting', 'Plotting.py'), + ('GraphicsItems', OrderedDict([ + ('PlotItem', 'PlotItem.py'), + ('ImageItem - video', 'ImageItem.py'), + ('ImageItem - draw', 'Draw.py'), + ('Region-of-Interest', 'ROItypes.py'), + ('GraphicsLayout', 'GraphicsLayout.py'), + ('Scatter Plot', 'ScatterPlot.py'), + ('ViewBox', 'ViewBox.py'), + ('Arrow', 'Arrow.py'), + ])), + ('Widgets', OrderedDict([ + ('PlotWidget', 'PlotWidget.py'), + ('SpinBox', '../widgets/SpinBox.py'), + ('TreeWidget', '../widgets/TreeWidget.py'), + ('DataTreeWidget', '../widgets/DataTreeWidget.py'), + ('GradientWidget', '../widgets/GradientWidget.py'), + ('TableWidget', '../widgets/TableWidget.py'), + ('ColorButton', '../widgets/ColorButton.py'), + ('CheckTable', '../widgets/CheckTable.py'), + ('VerticalLabel', '../widgets/VerticalLabel.py'), + ('JoystickButton', '../widgets/JoystickButton.py'), + ])), + ('ImageView', 'ImageView.py'), + ('GraphicsScene', 'GraphicsScene.py'), + ('Flowcharts', 'Flowchart.py'), + ('ParameterTree', '../parametertree'), + ('Canvas', '../canvas'), + ('MultiPlotWidget', 'MultiPlotWidget.py'), +]) + +path = os.path.abspath(os.path.dirname(__file__)) + +class ExampleLoader(QtGui.QMainWindow): + def __init__(self): + QtGui.QMainWindow.__init__(self) + self.ui = Ui_Form() + self.cw = QtGui.QWidget() + self.setCentralWidget(self.cw) + self.ui.setupUi(self.cw) + + global examples + self.populateTree(self.ui.exampleTree.invisibleRootItem(), examples) + self.ui.exampleTree.expandAll() + + self.resize(900,500) + self.show() + self.ui.splitter.setSizes([150,750]) + self.ui.loadBtn.clicked.connect(self.loadFile) + self.ui.exampleTree.currentItemChanged.connect(self.showFile) + self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile) + + + def populateTree(self, root, examples): + for key, val in examples.iteritems(): + item = QtGui.QTreeWidgetItem([key]) + if isinstance(val, basestring): + item.file = val + else: + self.populateTree(item, val) + root.addChild(item) + + + def currentFile(self): + item = self.ui.exampleTree.currentItem() + if hasattr(item, 'file'): + global path + return os.path.join(path, item.file) + return None + + def loadFile(self): + fn = self.currentFile() + if fn is None: + return + os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn) + + + def showFile(self): + fn = self.currentFile() + if fn is None: + self.ui.codeView.clear() + return + text = open(fn).read() + self.ui.codeView.setPlainText(text) + +def run(): + app = QtGui.QApplication([]) + loader = ExampleLoader() + + if sys.flags.interactive != 1: + app.exec_() + +if __name__ == '__main__': + run() \ No newline at end of file diff --git a/examples/exampleLoaderTemplate.py b/examples/exampleLoaderTemplate.py new file mode 100644 index 00000000..7447e84c --- /dev/null +++ b/examples/exampleLoaderTemplate.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' +# +# Created: Sat Dec 17 23:46:27 2011 +# by: PyQt4 UI code generator 4.8.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(762, 302) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setMargin(0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.splitter = QtGui.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName(_fromUtf8("splitter")) + self.layoutWidget = QtGui.QWidget(self.splitter) + self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) + self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setMargin(0) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.exampleTree = QtGui.QTreeWidget(self.layoutWidget) + self.exampleTree.setObjectName(_fromUtf8("exampleTree")) + self.exampleTree.headerItem().setText(0, _fromUtf8("1")) + self.exampleTree.header().setVisible(False) + self.verticalLayout.addWidget(self.exampleTree) + self.loadBtn = QtGui.QPushButton(self.layoutWidget) + self.loadBtn.setObjectName(_fromUtf8("loadBtn")) + self.verticalLayout.addWidget(self.loadBtn) + self.codeView = QtGui.QTextBrowser(self.splitter) + font = QtGui.QFont() + font.setFamily(_fromUtf8("Monospace")) + font.setPointSize(10) + self.codeView.setFont(font) + self.codeView.setObjectName(_fromUtf8("codeView")) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/examples/exampleLoaderTemplate.ui b/examples/exampleLoaderTemplate.ui new file mode 100644 index 00000000..b1941447 --- /dev/null +++ b/examples/exampleLoaderTemplate.ui @@ -0,0 +1,65 @@ + + + Form + + + + 0 + 0 + 762 + 302 + + + + Form + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + + + + false + + + + 1 + + + + + + + + Load Example + + + + + + + + + Monospace + 10 + + + + + + + + + + diff --git a/examples/test_scatterPlot.py b/examples/test_scatterPlot.py deleted file mode 100755 index e8d91eea..00000000 --- a/examples/test_scatterPlot.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -import sys, os -## Add path to library (just for examples; you do not need this) -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - -from PyQt4 import QtGui, QtCore -import pyqtgraph as pg -import numpy as np - -#QtGui.QApplication.setGraphicsSystem('raster') -app = QtGui.QApplication([]) - -mw = QtGui.QMainWindow() -mw.resize(800,800) -cw = QtGui.QWidget() -layout = QtGui.QGridLayout() -cw.setLayout(layout) -mw.setCentralWidget(cw) - -w1 = pg.PlotWidget() -layout.addWidget(w1, 0,0) - -w2 = pg.PlotWidget() -layout.addWidget(w2, 1,0) - -w3 = pg.GraphicsView() -w3.enableMouse() -w3.aspectLocked = True -layout.addWidget(w3, 0,1) - -w4 = pg.PlotWidget() -#vb = pg.ViewBox() -#w4.setCentralItem(vb) -layout.addWidget(w4, 1,1) - -mw.show() - - -n = 3000 -s1 = pg.ScatterPlotItem(size=10, pen=QtGui.QPen(QtCore.Qt.NoPen), brush=QtGui.QBrush(QtGui.QColor(255, 255, 255, 20))) -pos = np.random.normal(size=(2,n), scale=1e-5) -spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}] -s1.addPoints(spots) -w1.addDataItem(s1) - -def clicked(plot, points): - print "clicked points", points - -s1.sigClicked.connect(clicked) - - -s2 = pg.ScatterPlotItem(pxMode=False) -spots2 = [] -for i in range(10): - for j in range(10): - spots2.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'brush':pg.intColor(i*10+j, 100)}) -s2.addPoints(spots2) -w2.addDataItem(s2) - -s2.sigClicked.connect(clicked) - - -s3 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) -pos = np.random.normal(size=(2,3000), scale=1e-5) -spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, 3000)} for i in range(3000)] -s3.addPoints(spots) -w3.addItem(s3) -w3.setRange(s3.boundingRect()) -s3.sigClicked.connect(clicked) - - -s4 = pg.ScatterPlotItem(identical=True, size=10, pen=QtGui.QPen(QtCore.Qt.NoPen), brush=QtGui.QBrush(QtGui.QColor(255, 255, 255, 20))) -#pos = np.random.normal(size=(2,n), scale=1e-5) -#spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}] -s4.addPoints(spots) -w4.addDataItem(s4) - - -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: - app.exec_() - diff --git a/flowchart/Flowchart.py b/flowchart/Flowchart.py new file mode 100644 index 00000000..acbdb27e --- /dev/null +++ b/flowchart/Flowchart.py @@ -0,0 +1,920 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +#from PyQt4 import QtCore, QtGui +#from PySide import QtCore, QtGui +#import Node as NodeMod +from Node import * +#import functions +from collections import OrderedDict +from pyqtgraph.widgets.TreeWidget import * +#from .. DataTreeWidget import * +import FlowchartTemplate +import FlowchartCtrlTemplate +from Terminal import Terminal +from numpy import ndarray +import library +from debug import printExc +import configfile +import pyqtgraph.dockarea as dockarea +import pyqtgraph as pg +import FlowchartGraphicsView + +def strDict(d): + return dict([(str(k), v) for k, v in d.iteritems()]) + + +def toposort(deps, nodes=None, seen=None, stack=None, depth=0): + """Topological sort. Arguments are: + deps dictionary describing dependencies where a:[b,c] means "a depends on b and c" + nodes optional, specifies list of starting nodes (these should be the nodes + which are not depended on by any other nodes) + """ + + if nodes is None: + ## run through deps to find nodes that are not depended upon + rem = set() + for dep in deps.itervalues(): + rem |= set(dep) + nodes = set(deps.keys()) - rem + if seen is None: + seen = set() + stack = [] + sorted = [] + #print " "*depth, "Starting from", nodes + for n in nodes: + if n in stack: + raise Exception("Cyclic dependency detected", stack + [n]) + if n in seen: + continue + seen.add(n) + #print " "*depth, " descending into", n, deps[n] + sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1)) + #print " "*depth, " Added", n + sorted.append(n) + #print " "*depth, " ", sorted + return sorted + + +class Flowchart(Node): + + sigFileLoaded = QtCore.Signal(object) + sigFileSaved = QtCore.Signal(object) + + + #sigOutputChanged = QtCore.Signal() ## inherited from Node + sigChartLoaded = QtCore.Signal() + sigStateChanged = QtCore.Signal() + + def __init__(self, terminals=None, name=None, filePath=None): + if name is None: + name = "Flowchart" + if terminals is None: + terminals = {} + self.filePath = filePath + Node.__init__(self, name) ## create node without terminals; we'll add these later + + self.inputWasSet = False ## flag allows detection of changes in the absence of input change. + self._nodes = {} + self.nextZVal = 10 + #self.connects = [] + #self._chartGraphicsItem = FlowchartGraphicsItem(self) + self._widget = None + self._scene = None + self.processing = False ## flag that prevents recursive node updates + + self.widget() + + self.inputNode = Node('Input', allowRemove=False) + self.outputNode = Node('Output', allowRemove=False) + self.addNode(self.inputNode, 'Input', [-150, 0]) + self.addNode(self.outputNode, 'Output', [300, 0]) + + self.outputNode.sigOutputChanged.connect(self.outputChanged) + self.outputNode.sigTerminalRenamed.connect(self.internalTerminalRenamed) + self.inputNode.sigTerminalRenamed.connect(self.internalTerminalRenamed) + + self.viewBox.autoRange(padding = 0.04) + + for name, opts in terminals.iteritems(): + self.addTerminal(name, **opts) + + def setInput(self, **args): + #print "setInput", args + #Node.setInput(self, **args) + #print " ....." + self.inputWasSet = True + self.inputNode.setOutput(**args) + + def outputChanged(self): + self.widget().outputChanged(self.outputNode.inputValues()) + self.sigOutputChanged.emit(self) + + def output(self): + return self.outputNode.inputValues() + + def nodes(self): + return self._nodes + + def addTerminal(self, name, **opts): + term = Node.addTerminal(self, name, **opts) + name = term.name() + if opts['io'] == 'in': ## inputs to the flowchart become outputs on the input node + opts['io'] = 'out' + opts['multi'] = False + term2 = self.inputNode.addTerminal(name, **opts) + else: + opts['io'] = 'in' + #opts['multi'] = False + term2 = self.outputNode.addTerminal(name, **opts) + return term + + def removeTerminal(self, name): + #print "remove:", name + term = self[name] + inTerm = self.internalTerminal(term) + Node.removeTerminal(self, name) + inTerm.node().removeTerminal(inTerm.name()) + + def internalTerminalRenamed(self, term, oldName): + self[oldName].rename(term.name()) + + def terminalRenamed(self, term, oldName): + newName = term.name() + #print "flowchart rename", newName, oldName + #print self.terminals + Node.terminalRenamed(self, self[oldName], oldName) + #print self.terminals + for n in [self.inputNode, self.outputNode]: + if oldName in n.terminals: + n[oldName].rename(newName) + + def createNode(self, nodeType, name=None, pos=None): + if name is None: + n = 0 + while True: + name = "%s.%d" % (nodeType, n) + if name not in self._nodes: + break + n += 1 + + node = library.getNodeType(nodeType)(name) + self.addNode(node, name, pos) + return node + + def addNode(self, node, name, pos=None): + if pos is None: + pos = [0, 0] + if type(pos) in [QtCore.QPoint, QtCore.QPointF]: + pos = [pos.x(), pos.y()] + item = node.graphicsItem() + item.setZValue(self.nextZVal*2) + self.nextZVal += 1 + #item.setParentItem(self.chartGraphicsItem()) + self.viewBox.addItem(item) + #item.setPos(pos2.x(), pos2.y()) + item.moveBy(*pos) + self._nodes[name] = node + self.widget().addNode(node) + #QtCore.QObject.connect(node, QtCore.SIGNAL('closed'), self.nodeClosed) + node.sigClosed.connect(self.nodeClosed) + #QtCore.QObject.connect(node, QtCore.SIGNAL('renamed'), self.nodeRenamed) + node.sigRenamed.connect(self.nodeRenamed) + #QtCore.QObject.connect(node, QtCore.SIGNAL('outputChanged'), self.nodeOutputChanged) + node.sigOutputChanged.connect(self.nodeOutputChanged) + + def removeNode(self, node): + node.close() + + def nodeClosed(self, node): + del self._nodes[node.name()] + self.widget().removeNode(node) + #QtCore.QObject.disconnect(node, QtCore.SIGNAL('closed'), self.nodeClosed) + try: + node.sigClosed.disconnect(self.nodeClosed) + except TypeError: + pass + #QtCore.QObject.disconnect(node, QtCore.SIGNAL('renamed'), self.nodeRenamed) + try: + node.sigRenamed.disconnect(self.nodeRenamed) + except TypeError: + pass + #QtCore.QObject.disconnect(node, QtCore.SIGNAL('outputChanged'), self.nodeOutputChanged) + try: + node.sigOutputChanged.disconnect(self.nodeOutputChanged) + except TypeError: + pass + + def nodeRenamed(self, node, oldName): + del self._nodes[oldName] + self._nodes[node.name()] = node + self.widget().nodeRenamed(node, oldName) + + def arrangeNodes(self): + pass + + def internalTerminal(self, term): + """If the terminal belongs to the external Node, return the corresponding internal terminal""" + if term.node() is self: + if term.isInput(): + return self.inputNode[term.name()] + else: + return self.outputNode[term.name()] + else: + return term + + def connectTerminals(self, term1, term2): + """Connect two terminals together within this flowchart.""" + term1 = self.internalTerminal(term1) + term2 = self.internalTerminal(term2) + term1.connectTo(term2) + + + def process(self, **args): + """ + Process data through the flowchart, returning the output. + Keyword arguments must be the names of input terminals + + """ + data = {} ## Stores terminal:value pairs + + ## determine order of operations + ## order should look like [('p', node1), ('p', node2), ('d', terminal1), ...] + ## Each tuple specifies either (p)rocess this node or (d)elete the result from this terminal + order = self.processOrder() + #print "ORDER:", order + + ## Record inputs given to process() + for n, t in self.inputNode.outputs().iteritems(): + if n not in args: + raise Exception("Parameter %s required to process this chart." % n) + data[t] = args[n] + + ret = {} + + ## process all in order + for c, arg in order: + + if c == 'p': ## Process a single node + #print "===> process:", arg + node = arg + if node is self.inputNode: + continue ## input node has already been processed. + + + ## get input and output terminals for this node + outs = node.outputs().values() + ins = node.inputs().values() + + ## construct input value dictionary + args = {} + for inp in ins: + inputs = inp.inputTerminals() + if len(inputs) == 0: + continue + if inp.isMultiValue(): ## multi-input terminals require a dict of all inputs + args[inp.name()] = dict([(i, data[i]) for i in inputs]) + else: ## single-inputs terminals only need the single input value available + args[inp.name()] = data[inputs[0]] + + if node is self.outputNode: + ret = args ## we now have the return value, but must keep processing in case there are other endpoint nodes in the chart + else: + try: + if node.isBypassed(): + result = node.processBypassed(args) + else: + result = node.process(display=False, **args) + except: + print "Error processing node %s. Args are: %s" % (str(node), str(args)) + raise + for out in outs: + #print " Output:", out, out.name() + #print out.name() + try: + data[out] = result[out.name()] + except: + print out, out.name() + raise + elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory) + #print "===> delete", arg + if arg in data: + del data[arg] + + return ret + + def processOrder(self): + """Return the order of operations required to process this chart. + The order returned should look like [('p', node1), ('p', node2), ('d', terminal1), ...] + where each tuple specifies either (p)rocess this node or (d)elete the result from this terminal + """ + + ## first collect list of nodes/terminals and their dependencies + deps = {} + tdeps = {} ## {terminal: [nodes that depend on terminal]} + for name, node in self._nodes.iteritems(): + deps[node] = node.dependentNodes() + for t in node.outputs().itervalues(): + tdeps[t] = t.dependentNodes() + + #print "DEPS:", deps + ## determine correct node-processing order + #deps[self] = [] + order = toposort(deps) + #print "ORDER1:", order + + ## construct list of operations + ops = [('p', n) for n in order] + + ## determine when it is safe to delete terminal values + dels = [] + for t, nodes in tdeps.iteritems(): + lastInd = 0 + lastNode = None + for n in nodes: ## determine which node is the last to be processed according to order + if n is self: + lastInd = None + break + else: + try: + ind = order.index(n) + except ValueError: + continue + if lastNode is None or ind > lastInd: + lastNode = n + lastInd = ind + #tdeps[t] = lastNode + if lastInd is not None: + dels.append((lastInd+1, t)) + dels.sort(lambda a,b: cmp(b[0], a[0])) + for i, t in dels: + ops.insert(i, ('d', t)) + + return ops + + + def nodeOutputChanged(self, startNode): + """Triggered when a node's output values have changed. (NOT called during process()) + Propagates new data forward through network.""" + ## first collect list of nodes/terminals and their dependencies + + if self.processing: + return + self.processing = True + try: + deps = {} + for name, node in self._nodes.iteritems(): + deps[node] = [] + for t in node.outputs().itervalues(): + deps[node].extend(t.dependentNodes()) + + ## determine order of updates + order = toposort(deps, nodes=[startNode]) + order.reverse() + + ## keep track of terminals that have been updated + terms = set(startNode.outputs().values()) + + #print "======= Updating", startNode + #print "Order:", order + for node in order[1:]: + #print "Processing node", node + for term in node.inputs().values(): + #print " checking terminal", term + deps = term.connections().keys() + update = False + for d in deps: + if d in terms: + #print " ..input", d, "changed" + update = True + term.inputChanged(d, process=False) + if update: + #print " processing.." + node.update() + terms |= set(node.outputs().values()) + + finally: + self.processing = False + if self.inputWasSet: + self.inputWasSet = False + else: + self.sigStateChanged.emit() + + + + def chartGraphicsItem(self): + """Return the graphicsItem which displays the internals of this flowchart. + (graphicsItem() still returns the external-view item)""" + #return self._chartGraphicsItem + return self.viewBox + + def widget(self): + if self._widget is None: + self._widget = FlowchartCtrlWidget(self) + self.scene = self._widget.scene() + self.viewBox = self._widget.viewBox() + #self._scene = QtGui.QGraphicsScene() + #self._widget.setScene(self._scene) + #self.scene.addItem(self.chartGraphicsItem()) + + #ci = self.chartGraphicsItem() + #self.viewBox.addItem(ci) + #self.viewBox.autoRange() + return self._widget + + def listConnections(self): + conn = set() + for n in self._nodes.itervalues(): + terms = n.outputs() + for n, t in terms.iteritems(): + for c in t.connections(): + conn.add((t, c)) + return conn + + def saveState(self): + state = Node.saveState(self) + state['nodes'] = [] + state['connects'] = [] + state['terminals'] = self.saveTerminals() + + for name, node in self._nodes.iteritems(): + cls = type(node) + if hasattr(cls, 'nodeName'): + clsName = cls.nodeName + pos = node.graphicsItem().pos() + ns = {'class': clsName, 'name': name, 'pos': (pos.x(), pos.y()), 'state': node.saveState()} + state['nodes'].append(ns) + + conn = self.listConnections() + for a, b in conn: + state['connects'].append((a.node().name(), a.name(), b.node().name(), b.name())) + + state['inputNode'] = self.inputNode.saveState() + state['outputNode'] = self.outputNode.saveState() + + return state + + def restoreState(self, state, clear=False): + self.blockSignals(True) + try: + if clear: + self.clear() + Node.restoreState(self, state) + nodes = state['nodes'] + nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0])) + for n in nodes: + if n['name'] in self._nodes: + self._nodes[n['name']].moveBy(*n['pos']) + continue + try: + node = self.createNode(n['class'], name=n['name']) + node.restoreState(n['state']) + except: + printExc("Error creating node %s: (continuing anyway)" % n['name']) + #node.graphicsItem().moveBy(*n['pos']) + + self.inputNode.restoreState(state.get('inputNode', {})) + self.outputNode.restoreState(state.get('outputNode', {})) + + #self.restoreTerminals(state['terminals']) + for n1, t1, n2, t2 in state['connects']: + try: + self.connectTerminals(self._nodes[n1][t1], self._nodes[n2][t2]) + except: + print self._nodes[n1].terminals + print self._nodes[n2].terminals + printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2)) + + + finally: + self.blockSignals(False) + + self.sigChartLoaded.emit() + self.outputChanged() + #self.sigOutputChanged.emit() + + def loadFile(self, fileName=None, startDir=None): + if fileName is None: + if startDir is None: + startDir = self.filePath + if startDir is None: + startDir = '.' + self.fileDialog = pg.FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") + #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.show() + self.fileDialog.fileSelected.connect(self.loadFile) + return + ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. + #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") + fileName = str(fileName) + state = configfile.readConfigFile(fileName) + self.restoreState(state, clear=True) + self.viewBox.autoRange() + #self.emit(QtCore.SIGNAL('fileLoaded'), fileName) + self.sigFileLoaded.emit(fileName) + + def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): + if fileName is None: + if startDir is None: + startDir = self.filePath + if startDir is None: + startDir = '.' + self.fileDialog = pg.FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") + #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + #self.fileDialog.setDirectory(startDir) + self.fileDialog.show() + self.fileDialog.fileSelected.connect(self.saveFile) + return + #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") + configfile.writeConfigFile(self.saveState(), fileName) + self.sigFileSaved.emit(fileName) + + def clear(self): + for n in self._nodes.values(): + if n is self.inputNode or n is self.outputNode: + continue + n.close() ## calls self.nodeClosed(n) by signal + #self.clearTerminals() + self.widget().clear() + + def clearTerminals(self): + Node.clearTerminals(self) + self.inputNode.clearTerminals() + self.outputNode.clearTerminals() + +#class FlowchartGraphicsItem(QtGui.QGraphicsItem): +class FlowchartGraphicsItem(GraphicsObject): + + def __init__(self, chart): + #print "FlowchartGraphicsItem.__init__" + #QtGui.QGraphicsItem.__init__(self) + GraphicsObject.__init__(self) + self.chart = chart ## chart is an instance of Flowchart() + self.updateTerminals() + + def updateTerminals(self): + #print "FlowchartGraphicsItem.updateTerminals" + self.terminals = {} + bounds = self.boundingRect() + inp = self.chart.inputs() + dy = bounds.height() / (len(inp)+1) + y = dy + for n, t in inp.iteritems(): + item = t.graphicsItem() + self.terminals[n] = item + item.setParentItem(self) + item.setAnchor(bounds.width(), y) + y += dy + out = self.chart.outputs() + dy = bounds.height() / (len(out)+1) + y = dy + for n, t in out.iteritems(): + item = t.graphicsItem() + self.terminals[n] = item + item.setParentItem(self) + item.setAnchor(0, y) + y += dy + + def boundingRect(self): + #print "FlowchartGraphicsItem.boundingRect" + return QtCore.QRectF() + + def paint(self, p, *args): + #print "FlowchartGraphicsItem.paint" + pass + #p.drawRect(self.boundingRect()) + + +class FlowchartCtrlWidget(QtGui.QWidget): + """The widget that contains the list of all the nodes in a flowchart and their controls, as well as buttons for loading/saving flowcharts.""" + + def __init__(self, chart): + self.items = {} + #self.loadDir = loadDir ## where to look initially for chart files + self.currentFileName = None + QtGui.QWidget.__init__(self) + self.chart = chart + self.ui = FlowchartCtrlTemplate.Ui_Form() + self.ui.setupUi(self) + self.ui.ctrlList.setColumnCount(2) + #self.ui.ctrlList.setColumnWidth(0, 200) + self.ui.ctrlList.setColumnWidth(1, 20) + self.ui.ctrlList.setVerticalScrollMode(self.ui.ctrlList.ScrollPerPixel) + self.ui.ctrlList.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + self.chartWidget = FlowchartWidget(chart, self) + #self.chartWidget.viewBox().autoRange() + self.cwWin = QtGui.QMainWindow() + self.cwWin.setWindowTitle('Flowchart') + self.cwWin.setCentralWidget(self.chartWidget) + self.cwWin.resize(1000,800) + + h = self.ui.ctrlList.header() + h.setResizeMode(0, h.Stretch) + + self.ui.ctrlList.itemChanged.connect(self.itemChanged) + self.ui.loadBtn.clicked.connect(self.loadClicked) + self.ui.saveBtn.clicked.connect(self.saveClicked) + self.ui.saveAsBtn.clicked.connect(self.saveAsClicked) + self.ui.showChartBtn.toggled.connect(self.chartToggled) + self.chart.sigFileLoaded.connect(self.setCurrentFile) + self.ui.reloadBtn.clicked.connect(self.reloadClicked) + self.chart.sigFileSaved.connect(self.fileSaved) + + + + #def resizeEvent(self, ev): + #QtGui.QWidget.resizeEvent(self, ev) + #self.ui.ctrlList.setColumnWidth(0, self.ui.ctrlList.viewport().width()-20) + + def chartToggled(self, b): + if b: + self.cwWin.show() + else: + self.cwWin.hide() + + def reloadClicked(self): + try: + self.chartWidget.reloadLibrary() + self.ui.reloadBtn.success("Reloaded.") + except: + self.ui.reloadBtn.success("Error.") + raise + + + def loadClicked(self): + newFile = self.chart.loadFile() + #self.setCurrentFile(newFile) + + def fileSaved(self, fileName): + self.setCurrentFile(fileName) + self.ui.saveBtn.success("Saved.") + + def saveClicked(self): + if self.currentFileName is None: + self.saveAsClicked() + else: + try: + self.chart.saveFile(self.currentFileName) + #self.ui.saveBtn.success("Saved.") + except: + self.ui.saveBtn.failure("Error") + raise + + def saveAsClicked(self): + try: + if self.currentFileName is None: + newFile = self.chart.saveFile() + else: + newFile = self.chart.saveFile(suggestedFileName=self.currentFileName) + #self.ui.saveAsBtn.success("Saved.") + #print "Back to saveAsClicked." + except: + self.ui.saveBtn.failure("Error") + raise + + #self.setCurrentFile(newFile) + + def setCurrentFile(self, fileName): + self.currentFileName = fileName + if fileName is None: + self.ui.fileNameLabel.setText("[ new ]") + else: + self.ui.fileNameLabel.setText("%s" % os.path.split(self.currentFileName)[1]) + self.resizeEvent(None) + + def itemChanged(self, *args): + pass + + def scene(self): + return self.chartWidget.scene() ## returns the GraphicsScene object + + def viewBox(self): + return self.chartWidget.viewBox() + + def nodeRenamed(self, node, oldName): + self.items[node].setText(0, node.name()) + + def addNode(self, node): + ctrl = node.ctrlWidget() + #if ctrl is None: + #return + item = QtGui.QTreeWidgetItem([node.name(), '', '']) + self.ui.ctrlList.addTopLevelItem(item) + byp = QtGui.QPushButton('X') + byp.setCheckable(True) + byp.setFixedWidth(20) + item.bypassBtn = byp + self.ui.ctrlList.setItemWidget(item, 1, byp) + byp.node = node + node.bypassButton = byp + byp.setChecked(node.isBypassed()) + byp.clicked.connect(self.bypassClicked) + + if ctrl is not None: + item2 = QtGui.QTreeWidgetItem() + item.addChild(item2) + self.ui.ctrlList.setItemWidget(item2, 0, ctrl) + + self.items[node] = item + + def removeNode(self, node): + if node in self.items: + item = self.items[node] + #self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked) + try: + item.bypassBtn.clicked.disconnect(self.bypassClicked) + except TypeError: + pass + self.ui.ctrlList.removeTopLevelItem(item) + + def bypassClicked(self): + btn = QtCore.QObject.sender(self) + btn.node.bypass(btn.isChecked()) + + def chartWidget(self): + return self.chartWidget + + def outputChanged(self, data): + pass + #self.ui.outputTree.setData(data, hideRoot=True) + + def clear(self): + self.chartWidget.clear() + + def select(self, node): + item = self.items[node] + self.ui.ctrlList.setCurrentItem(item) + +class FlowchartWidget(dockarea.DockArea): + """Includes the actual graphical flowchart and debugging interface""" + def __init__(self, chart, ctrl): + #QtGui.QWidget.__init__(self) + dockarea.DockArea.__init__(self) + self.chart = chart + self.ctrl = ctrl + self.hoverItem = None + #self.setMinimumWidth(250) + #self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)) + + #self.ui = FlowchartTemplate.Ui_Form() + #self.ui.setupUi(self) + + ## build user interface (it was easier to do it here than via developer) + self.view = FlowchartGraphicsView.FlowchartGraphicsView(self) + self.viewDock = dockarea.Dock('view', size=(1000,600)) + self.viewDock.addWidget(self.view) + self.viewDock.hideTitleBar() + self.addDock(self.viewDock) + + + self.hoverText = QtGui.QTextEdit() + self.hoverText.setReadOnly(True) + self.hoverDock = dockarea.Dock('Hover Info', size=(1000,20)) + self.hoverDock.addWidget(self.hoverText) + self.addDock(self.hoverDock, 'bottom') + + self.selInfo = QtGui.QWidget() + self.selInfoLayout = QtGui.QGridLayout() + self.selInfo.setLayout(self.selInfoLayout) + self.selDescLabel = QtGui.QLabel() + self.selNameLabel = QtGui.QLabel() + self.selDescLabel.setWordWrap(True) + self.selectedTree = pg.DataTreeWidget() + #self.selectedTree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + #self.selInfoLayout.addWidget(self.selNameLabel) + self.selInfoLayout.addWidget(self.selDescLabel) + self.selInfoLayout.addWidget(self.selectedTree) + self.selDock = dockarea.Dock('Selected Node', size=(1000,200)) + self.selDock.addWidget(self.selInfo) + self.addDock(self.selDock, 'bottom') + + self._scene = self.view.scene() + self._viewBox = self.view.viewBox() + #self._scene = QtGui.QGraphicsScene() + #self._scene = FlowchartGraphicsView.FlowchartGraphicsScene() + #self.view.setScene(self._scene) + + self.buildMenu() + #self.ui.addNodeBtn.mouseReleaseEvent = self.addNodeBtnReleased + + self._scene.selectionChanged.connect(self.selectionChanged) + self._scene.sigMouseHover.connect(self.hoverOver) + #self.view.sigClicked.connect(self.showViewMenu) + #self._scene.sigSceneContextMenu.connect(self.showViewMenu) + #self._viewBox.sigActionPositionChanged.connect(self.menuPosChanged) + + + def reloadLibrary(self): + #QtCore.QObject.disconnect(self.nodeMenu, QtCore.SIGNAL('triggered(QAction*)'), self.nodeMenuTriggered) + self.nodeMenu.triggered.disconnect(self.nodeMenuTriggered) + self.nodeMenu = None + self.subMenus = [] + library.loadLibrary(reloadLibs=True) + self.buildMenu() + + def buildMenu(self, pos=None): + self.nodeMenu = QtGui.QMenu() + self.subMenus = [] + for section, nodes in library.getNodeTree().iteritems(): + menu = QtGui.QMenu(section) + self.nodeMenu.addMenu(menu) + for name in nodes: + act = menu.addAction(name) + act.nodeType = name + act.pos = pos + self.subMenus.append(menu) + self.nodeMenu.triggered.connect(self.nodeMenuTriggered) + return self.nodeMenu + + def menuPosChanged(self, pos): + self.menuPos = pos + + def showViewMenu(self, ev): + #QtGui.QPushButton.mouseReleaseEvent(self.ui.addNodeBtn, ev) + #if ev.button() == QtCore.Qt.RightButton: + #self.menuPos = self.view.mapToScene(ev.pos()) + #self.nodeMenu.popup(ev.globalPos()) + #print "Flowchart.showViewMenu called" + + #self.menuPos = ev.scenePos() + self.buildMenu(ev.scenePos()) + self.nodeMenu.popup(ev.screenPos()) + + def scene(self): + return self._scene ## the GraphicsScene item + + def viewBox(self): + return self._viewBox ## the viewBox that items should be added to + + def nodeMenuTriggered(self, action): + nodeType = action.nodeType + if action.pos is not None: + pos = action.pos + else: + pos = self.menuPos + pos = self.viewBox().mapSceneToView(pos) + + self.chart.createNode(nodeType, pos=pos) + + + def selectionChanged(self): + #print "FlowchartWidget.selectionChanged called." + items = self._scene.selectedItems() + #print " scene.selectedItems: ", items + if len(items) == 0: + data = None + else: + item = items[0] + if hasattr(item, 'node') and isinstance(item.node, Node): + n = item.node + self.ctrl.select(n) + data = {'outputs': n.outputValues(), 'inputs': n.inputValues()} + self.selNameLabel.setText(n.name()) + if hasattr(n, 'nodeName'): + self.selDescLabel.setText("%s: %s" % (n.nodeName, n.__class__.__doc__)) + else: + self.selDescLabel.setText("") + if n.exception is not None: + data['exception'] = n.exception + else: + data = None + self.selectedTree.setData(data, hideRoot=True) + + def hoverOver(self, items): + #print "FlowchartWidget.hoverOver called." + term = None + for item in items: + if item is self.hoverItem: + return + self.hoverItem = item + if hasattr(item, 'term') and isinstance(item.term, Terminal): + term = item.term + break + if term is None: + self.hoverText.setPlainText("") + else: + val = term.value() + if isinstance(val, ndarray): + val = "%s %s %s" % (type(val).__name__, str(val.shape), str(val.dtype)) + else: + val = str(val) + if len(val) > 400: + val = val[:400] + "..." + self.hoverText.setPlainText("%s.%s = %s" % (term.node().name(), term.name(), val)) + #self.hoverLabel.setCursorPosition(0) + + + + def clear(self): + #self.outputTree.setData(None) + self.selectedTree.setData(None) + self.hoverText.setPlainText('') + self.selNameLabel.setText('') + self.selDescLabel.setText('') + + +class FlowchartNode(Node): + pass + diff --git a/flowchart/FlowchartCtrlTemplate.py b/flowchart/FlowchartCtrlTemplate.py new file mode 100644 index 00000000..0f2ec162 --- /dev/null +++ b/flowchart/FlowchartCtrlTemplate.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'FlowchartCtrlTemplate.ui' +# +# Created: Sun Dec 18 20:55:57 2011 +# by: PyQt4 UI code generator 4.8.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(217, 499) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setMargin(0) + self.gridLayout.setVerticalSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.loadBtn = QtGui.QPushButton(Form) + self.loadBtn.setObjectName(_fromUtf8("loadBtn")) + self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) + self.saveBtn = FeedbackButton(Form) + self.saveBtn.setObjectName(_fromUtf8("saveBtn")) + self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) + self.saveAsBtn = FeedbackButton(Form) + self.saveAsBtn.setObjectName(_fromUtf8("saveAsBtn")) + self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) + self.reloadBtn = FeedbackButton(Form) + self.reloadBtn.setCheckable(False) + self.reloadBtn.setFlat(False) + self.reloadBtn.setObjectName(_fromUtf8("reloadBtn")) + self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) + self.showChartBtn = QtGui.QPushButton(Form) + self.showChartBtn.setCheckable(True) + self.showChartBtn.setObjectName(_fromUtf8("showChartBtn")) + self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) + self.ctrlList = TreeWidget(Form) + self.ctrlList.setObjectName(_fromUtf8("ctrlList")) + self.ctrlList.headerItem().setText(0, _fromUtf8("1")) + self.ctrlList.header().setVisible(False) + self.ctrlList.header().setStretchLastSection(False) + self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) + self.fileNameLabel = QtGui.QLabel(Form) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.fileNameLabel.setFont(font) + self.fileNameLabel.setText(_fromUtf8("")) + self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fileNameLabel.setObjectName(_fromUtf8("fileNameLabel")) + self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load..", None, QtGui.QApplication.UnicodeUTF8)) + self.saveBtn.setText(QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8)) + self.saveAsBtn.setText(QtGui.QApplication.translate("Form", "As..", None, QtGui.QApplication.UnicodeUTF8)) + self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8)) + self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8)) + +from FeedbackButton import FeedbackButton +from pyqtgraph.widgets.TreeWidget import TreeWidget diff --git a/flowchart/FlowchartCtrlTemplate.ui b/flowchart/FlowchartCtrlTemplate.ui new file mode 100644 index 00000000..a931fb3a --- /dev/null +++ b/flowchart/FlowchartCtrlTemplate.ui @@ -0,0 +1,120 @@ + + + Form + + + + 0 + 0 + 217 + 499 + + + + Form + + + + 0 + + + 0 + + + + + Load.. + + + + + + + + + + + + + + + + Flowchart + + + true + + + + + + + false + + + false + + + false + + + false + + + + 1 + + + + + + + + + 75 + true + + + + + + + Qt::AlignCenter + + + + + + + + TreeWidget + QTreeWidget +
pyqtgraph.widgets.TreeWidget
+
+ + FeedbackButton + QPushButton +
FeedbackButton
+
+
+ + +
diff --git a/flowchart/FlowchartGraphicsView.py b/flowchart/FlowchartGraphicsView.py new file mode 100644 index 00000000..0ec4d5c8 --- /dev/null +++ b/flowchart/FlowchartGraphicsView.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.widgets.GraphicsView import GraphicsView +from pyqtgraph.GraphicsScene import GraphicsScene +from pyqtgraph.graphicsItems.ViewBox import ViewBox + +#class FlowchartGraphicsView(QtGui.QGraphicsView): +class FlowchartGraphicsView(GraphicsView): + + sigHoverOver = QtCore.Signal(object) + sigClicked = QtCore.Signal(object) + + def __init__(self, widget, *args): + #QtGui.QGraphicsView.__init__(self, *args) + GraphicsView.__init__(self, *args, useOpenGL=False) + #self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(255,255,255))) + self._vb = FlowchartViewBox(widget, lockAspect=True, invertY=True) + self.setCentralItem(self._vb) + #self.scene().addItem(self.vb) + #self.setMouseTracking(True) + #self.lastPos = None + #self.setTransformationAnchor(self.AnchorViewCenter) + #self.setRenderHints(QtGui.QPainter.Antialiasing) + self.setRenderHint(QtGui.QPainter.Antialiasing, True) + #self.setDragMode(QtGui.QGraphicsView.RubberBandDrag) + #self.setRubberBandSelectionMode(QtCore.Qt.ContainsItemBoundingRect) + + def viewBox(self): + return self._vb + + + #def mousePressEvent(self, ev): + #self.moved = False + #self.lastPos = ev.pos() + #return QtGui.QGraphicsView.mousePressEvent(self, ev) + + #def mouseMoveEvent(self, ev): + #self.moved = True + #callSuper = False + #if ev.buttons() & QtCore.Qt.RightButton: + #if self.lastPos is not None: + #dif = ev.pos() - self.lastPos + #self.scale(1.01**-dif.y(), 1.01**-dif.y()) + #elif ev.buttons() & QtCore.Qt.MidButton: + #if self.lastPos is not None: + #dif = ev.pos() - self.lastPos + #self.translate(dif.x(), -dif.y()) + #else: + ##self.emit(QtCore.SIGNAL('hoverOver'), self.items(ev.pos())) + #self.sigHoverOver.emit(self.items(ev.pos())) + #callSuper = True + #self.lastPos = ev.pos() + + #if callSuper: + #QtGui.QGraphicsView.mouseMoveEvent(self, ev) + + #def mouseReleaseEvent(self, ev): + #if not self.moved: + ##self.emit(QtCore.SIGNAL('clicked'), ev) + #self.sigClicked.emit(ev) + #return QtGui.QGraphicsView.mouseReleaseEvent(self, ev) + +class FlowchartViewBox(ViewBox): + + def __init__(self, widget, *args, **kwargs): + ViewBox.__init__(self, *args, **kwargs) + self.widget = widget + #self.menu = None + #self._subMenus = None ## need a place to store the menus otherwise they dissappear (even though they've been added to other menus) ((yes, it doesn't make sense)) + + + + + def getMenu(self, ev): + ## called by ViewBox to create a new context menu + self._fc_menu = QtGui.QMenu() + self._subMenus = self.getContextMenus(ev) + for menu in self._subMenus: + self._fc_menu.addMenu(menu) + return self._fc_menu + + def getContextMenus(self, ev): + ## called by scene to add menus on to someone else's context menu + menu = self.widget.buildMenu(ev.scenePos()) + menu.setTitle("Add node") + return [menu, ViewBox.getMenu(self, ev)] + + + + + + + + + + +##class FlowchartGraphicsScene(QtGui.QGraphicsScene): +#class FlowchartGraphicsScene(GraphicsScene): + + #sigContextMenuEvent = QtCore.Signal(object) + + #def __init__(self, *args): + ##QtGui.QGraphicsScene.__init__(self, *args) + #GraphicsScene.__init__(self, *args) + + #def mouseClickEvent(self, ev): + ##QtGui.QGraphicsScene.contextMenuEvent(self, ev) + #if not ev.button() in [QtCore.Qt.RightButton]: + #self.sigContextMenuEvent.emit(ev) \ No newline at end of file diff --git a/flowchart/FlowchartTemplate.py b/flowchart/FlowchartTemplate.py new file mode 100644 index 00000000..ec8823f1 --- /dev/null +++ b/flowchart/FlowchartTemplate.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'FlowchartTemplate.ui' +# +# Created: Sun Dec 18 20:55:57 2011 +# by: PyQt4 UI code generator 4.8.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(529, 329) + self.selInfoWidget = QtGui.QWidget(Form) + self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) + self.selInfoWidget.setObjectName(_fromUtf8("selInfoWidget")) + self.gridLayout = QtGui.QGridLayout(self.selInfoWidget) + self.gridLayout.setMargin(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.selDescLabel = QtGui.QLabel(self.selInfoWidget) + self.selDescLabel.setText(_fromUtf8("")) + self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.selDescLabel.setWordWrap(True) + self.selDescLabel.setObjectName(_fromUtf8("selDescLabel")) + self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) + self.selNameLabel = QtGui.QLabel(self.selInfoWidget) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.selNameLabel.setFont(font) + self.selNameLabel.setText(_fromUtf8("")) + self.selNameLabel.setObjectName(_fromUtf8("selNameLabel")) + self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) + self.selectedTree = DataTreeWidget(self.selInfoWidget) + self.selectedTree.setObjectName(_fromUtf8("selectedTree")) + self.selectedTree.headerItem().setText(0, _fromUtf8("1")) + self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) + self.hoverText = QtGui.QTextEdit(Form) + self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) + self.hoverText.setObjectName(_fromUtf8("hoverText")) + self.view = FlowchartGraphicsView(Form) + self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) + self.view.setObjectName(_fromUtf8("view")) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + +from pyqtgraph.widgets.DataTreeWidget import DataTreeWidget +from FlowchartGraphicsView import FlowchartGraphicsView diff --git a/flowchart/FlowchartTemplate.ui b/flowchart/FlowchartTemplate.ui new file mode 100644 index 00000000..e4530800 --- /dev/null +++ b/flowchart/FlowchartTemplate.ui @@ -0,0 +1,98 @@ + + + Form + + + + 0 + 0 + 529 + 329 + + + + Form + + + + + 260 + 10 + 264 + 222 + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + 75 + true + + + + + + + + + + + + 1 + + + + + + + + + + 0 + 240 + 521 + 81 + + + + + + + 0 + 0 + 256 + 192 + + + + + + + DataTreeWidget + QTreeWidget +
pyqtgraph.widgets.DataTreeWidget
+
+ + FlowchartGraphicsView + QGraphicsView +
FlowchartGraphicsView
+
+
+ + +
diff --git a/flowchart/Node.py b/flowchart/Node.py new file mode 100644 index 00000000..88a6d3b2 --- /dev/null +++ b/flowchart/Node.py @@ -0,0 +1,561 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +#from PySide import QtCore, QtGui +from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject +import pyqtgraph.functions as fn +from Terminal import * +from collections import OrderedDict +from debug import * +import numpy as np +#from pyqtgraph.ObjectWorkaround import QObjectWorkaround +from eq import * + +#TETRACYCLINE = True + +def strDict(d): + return dict([(str(k), v) for k, v in d.iteritems()]) + +class Node(QtCore.QObject): + + sigOutputChanged = QtCore.Signal(object) # self + sigClosed = QtCore.Signal(object) + sigRenamed = QtCore.Signal(object, object) + sigTerminalRenamed = QtCore.Signal(object, object) + + + def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True): + QtCore.QObject.__init__(self) + self._name = name + self._bypass = False + self.bypassButton = None ## this will be set by the flowchart ctrl widget.. + self._graphicsItem = None + self.terminals = OrderedDict() + self._inputs = {} + self._outputs = {} + self._allowAddInput = allowAddInput ## flags to allow the user to add/remove terminals + self._allowAddOutput = allowAddOutput + self._allowRemove = allowRemove + + self.exception = None + if terminals is None: + return + for name, opts in terminals.iteritems(): + self.addTerminal(name, **opts) + + + def nextTerminalName(self, name): + """Return an unused terminal name""" + name2 = name + i = 1 + while name2 in self.terminals: + name2 = "%s.%d" % (name, i) + i += 1 + return name2 + + def addInput(self, name="Input", **args): + #print "Node.addInput called." + return self.addTerminal(name, io='in', **args) + + def addOutput(self, name="Output", **args): + return self.addTerminal(name, io='out', **args) + + def removeTerminal(self, term): + ## term may be a terminal or its name + + if isinstance(term, Terminal): + name = term.name() + else: + name = term + term = self.terminals[name] + + #print "remove", name + #term.disconnectAll() + term.close() + del self.terminals[name] + if name in self._inputs: + del self._inputs[name] + if name in self._outputs: + del self._outputs[name] + self.graphicsItem().updateTerminals() + + def terminalRenamed(self, term, oldName): + """Called after a terminal has been renamed""" + newName = term.name() + #print "node", self, "handling rename..", newName, oldName + for d in [self.terminals, self._inputs, self._outputs]: + if oldName not in d: + continue + #print " got one" + d[newName] = d[oldName] + del d[oldName] + + self.graphicsItem().updateTerminals() + #self.emit(QtCore.SIGNAL('terminalRenamed'), term, oldName) + self.sigTerminalRenamed.emit(term, oldName) + + def addTerminal(self, name, **opts): + #print "Node.addTerminal called. name:", name, "opts:", opts + #global TETRACYCLINE + #print "TETRACYCLINE: ", TETRACYCLINE + #if TETRACYCLINE: + #print "Creating Terminal..." + name = self.nextTerminalName(name) + term = Terminal(self, name, **opts) + self.terminals[name] = term + if term.isInput(): + self._inputs[name] = term + elif term.isOutput(): + self._outputs[name] = term + self.graphicsItem().updateTerminals() + return term + + + def inputs(self): + return self._inputs + + def outputs(self): + return self._outputs + + def process(self, **kargs): + """Process data through this node. Each named argument supplies data to the corresponding terminal.""" + return {} + + def graphicsItem(self): + """Return a (the?) graphicsitem for this node""" + #print "Node.graphicsItem called." + if self._graphicsItem is None: + #print "Creating NodeGraphicsItem..." + self._graphicsItem = NodeGraphicsItem(self) + #print "Node.graphicsItem is returning ", self._graphicsItem + return self._graphicsItem + + def __getattr__(self, attr): + """Return the terminal with the given name""" + if attr not in self.terminals: + raise AttributeError(attr) + else: + return self.terminals[attr] + + def __getitem__(self, item): + return getattr(self, item) + + def name(self): + return self._name + + def rename(self, name): + oldName = self._name + self._name = name + #self.emit(QtCore.SIGNAL('renamed'), self, oldName) + self.sigRenamed.emit(self, oldName) + + def dependentNodes(self): + """Return the list of nodes which provide direct input to this node""" + nodes = set() + for t in self.inputs().itervalues(): + nodes |= set([i.node() for i in t.inputTerminals()]) + return nodes + #return set([t.inputTerminals().node() for t in self.listInputs().itervalues()]) + + def __repr__(self): + return "" % (self.name(), id(self)) + + def ctrlWidget(self): + return None + + def bypass(self, byp): + self._bypass = byp + if self.bypassButton is not None: + self.bypassButton.setChecked(byp) + self.update() + + def isBypassed(self): + return self._bypass + + def setInput(self, **args): + """Set the values on input terminals. For most nodes, this will happen automatically through Terminal.inputChanged. + This is normally only used for nodes with no connected inputs.""" + changed = False + for k, v in args.iteritems(): + term = self._inputs[k] + oldVal = term.value() + if not eq(oldVal, v): + changed = True + term.setValue(v, process=False) + if changed and '_updatesHandled_' not in args: + self.update() + + def inputValues(self): + vals = {} + for n, t in self.inputs().iteritems(): + vals[n] = t.value() + return vals + + def outputValues(self): + vals = {} + for n, t in self.outputs().iteritems(): + vals[n] = t.value() + return vals + + def connected(self, localTerm, remoteTerm): + """Called whenever one of this node's terminals is connected elsewhere.""" + pass + + def disconnected(self, localTerm, remoteTerm): + """Called whenever one of this node's terminals is connected elsewhere.""" + pass + + def update(self, signal=True): + """Collect all input values, attempt to process new output values, and propagate downstream.""" + vals = self.inputValues() + #print " inputs:", vals + try: + if self.isBypassed(): + out = self.processBypassed(vals) + else: + out = self.process(**strDict(vals)) + #print " output:", out + if out is not None: + if signal: + self.setOutput(**out) + else: + self.setOutputNoSignal(**out) + for n,t in self.inputs().iteritems(): + t.setValueAcceptable(True) + self.clearException() + except: + #printExc( "Exception while processing %s:" % self.name()) + for n,t in self.outputs().iteritems(): + t.setValue(None) + self.setException(sys.exc_info()) + + if signal: + #self.emit(QtCore.SIGNAL('outputChanged'), self) ## triggers flowchart to propagate new data + self.sigOutputChanged.emit(self) ## triggers flowchart to propagate new data + + def processBypassed(self, args): + result = {} + for term in self.outputs().values(): + byp = term.bypassValue() + if byp is None: + result[term.name()] = None + else: + result[term.name()] = args.get(byp, None) + return result + + def setOutput(self, **vals): + self.setOutputNoSignal(**vals) + #self.emit(QtCore.SIGNAL('outputChanged'), self) ## triggers flowchart to propagate new data + self.sigOutputChanged.emit(self) ## triggers flowchart to propagate new data + + def setOutputNoSignal(self, **vals): + for k, v in vals.iteritems(): + term = self.outputs()[k] + term.setValue(v) + #targets = term.connections() + #for t in targets: ## propagate downstream + #if t is term: + #continue + #t.inputChanged(term) + term.setValueAcceptable(True) + + def setException(self, exc): + self.exception = exc + self.recolor() + + def clearException(self): + self.setException(None) + + def recolor(self): + if self.exception is None: + self.graphicsItem().setPen(QtGui.QPen(QtGui.QColor(0, 0, 0))) + else: + self.graphicsItem().setPen(QtGui.QPen(QtGui.QColor(150, 0, 0), 3)) + + def saveState(self): + pos = self.graphicsItem().pos() + return {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()} + + def restoreState(self, state): + pos = state.get('pos', (0,0)) + self.graphicsItem().setPos(*pos) + self.bypass(state.get('bypass', False)) + + def saveTerminals(self): + terms = OrderedDict() + for n, t in self.terminals.iteritems(): + terms[n] = (t.saveState()) + return terms + + def restoreTerminals(self, state): + for name in self.terminals.keys(): + if name not in state: + self.removeTerminal(name) + for name, opts in state.iteritems(): + if name in self.terminals: + continue + try: + opts = strDict(opts) + self.addTerminal(name, **opts) + except: + printExc("Error restoring terminal %s (%s):" % (str(name), str(opts))) + + + def clearTerminals(self): + for t in self.terminals.itervalues(): + t.close() + self.terminals = OrderedDict() + self._inputs = {} + self._outputs = {} + + def close(self): + """Cleans up after the node--removes terminals, graphicsItem, widget""" + self.disconnectAll() + self.clearTerminals() + item = self.graphicsItem() + if item.scene() is not None: + item.scene().removeItem(item) + self._graphicsItem = None + w = self.ctrlWidget() + if w is not None: + w.setParent(None) + #self.emit(QtCore.SIGNAL('closed'), self) + self.sigClosed.emit(self) + + def disconnectAll(self): + for t in self.terminals.values(): + t.disconnectAll() + + +#class NodeGraphicsItem(QtGui.QGraphicsItem): +class NodeGraphicsItem(GraphicsObject): + def __init__(self, node): + #QtGui.QGraphicsItem.__init__(self) + GraphicsObject.__init__(self) + #QObjectWorkaround.__init__(self) + + #self.shadow = QtGui.QGraphicsDropShadowEffect() + #self.shadow.setOffset(5,5) + #self.shadow.setBlurRadius(10) + #self.setGraphicsEffect(self.shadow) + + self.pen = fn.mkPen(0,0,0) + self.selectPen = fn.mkPen(200,200,200,width=2) + self.brush = fn.mkBrush(200, 200, 200, 150) + self.hoverBrush = fn.mkBrush(200, 200, 200, 200) + self.selectBrush = fn.mkBrush(200, 200, 255, 200) + self.hovered = False + + self.node = node + flags = self.ItemIsMovable | self.ItemIsSelectable | self.ItemIsFocusable |self.ItemSendsGeometryChanges + #flags = self.ItemIsFocusable |self.ItemSendsGeometryChanges + + self.setFlags(flags) + self.bounds = QtCore.QRectF(0, 0, 100, 100) + self.nameItem = QtGui.QGraphicsTextItem(self.node.name(), self) + self.nameItem.setDefaultTextColor(QtGui.QColor(50, 50, 50)) + self.nameItem.moveBy(self.bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0) + self.nameItem.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction) + self.updateTerminals() + #self.setZValue(10) + + self.nameItem.focusOutEvent = self.labelFocusOut + self.nameItem.keyPressEvent = self.labelKeyPress + + self.menu = None + self.buildMenu() + + #self.node.sigTerminalRenamed.connect(self.updateActionMenu) + + #def setZValue(self, z): + #for t, item in self.terminals.itervalues(): + #item.setZValue(z+1) + #GraphicsObject.setZValue(self, z) + + def labelFocusOut(self, ev): + QtGui.QGraphicsTextItem.focusOutEvent(self.nameItem, ev) + self.labelChanged() + + def labelKeyPress(self, ev): + if ev.key() == QtCore.Qt.Key_Enter or ev.key() == QtCore.Qt.Key_Return: + self.labelChanged() + else: + QtGui.QGraphicsTextItem.keyPressEvent(self.nameItem, ev) + + def labelChanged(self): + newName = str(self.nameItem.toPlainText()) + if newName != self.node.name(): + self.node.rename(newName) + + ### re-center the label + bounds = self.boundingRect() + self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0) + + def setPen(self, pen): + self.pen = pen + self.update() + + def setBrush(self, brush): + self.brush = brush + self.update() + + + def updateTerminals(self): + bounds = self.bounds + self.terminals = {} + inp = self.node.inputs() + dy = bounds.height() / (len(inp)+1) + y = dy + for i, t in inp.iteritems(): + item = t.graphicsItem() + item.setParentItem(self) + #item.setZValue(self.zValue()+1) + br = self.bounds + item.setAnchor(0, y) + self.terminals[i] = (t, item) + y += dy + + out = self.node.outputs() + dy = bounds.height() / (len(out)+1) + y = dy + for i, t in out.iteritems(): + item = t.graphicsItem() + item.setParentItem(self) + item.setZValue(self.zValue()) + br = self.bounds + item.setAnchor(bounds.width(), y) + self.terminals[i] = (t, item) + y += dy + + #self.buildMenu() + + + def boundingRect(self): + return self.bounds.adjusted(-5, -5, 5, 5) + + def paint(self, p, *args): + + p.setPen(self.pen) + if self.isSelected(): + p.setPen(self.selectPen) + p.setBrush(self.selectBrush) + else: + p.setPen(self.pen) + if self.hovered: + p.setBrush(self.hoverBrush) + else: + p.setBrush(self.brush) + + p.drawRect(self.bounds) + + + def mousePressEvent(self, ev): + ev.ignore() + + + def mouseClickEvent(self, ev): + #print "Node.mouseClickEvent called." + if int(ev.button()) == int(QtCore.Qt.LeftButton): + ev.accept() + #print " ev.button: left" + sel = self.isSelected() + #ret = QtGui.QGraphicsItem.mousePressEvent(self, ev) + self.setSelected(True) + if not sel and self.isSelected(): + #self.setBrush(QtGui.QBrush(QtGui.QColor(200, 200, 255))) + #self.emit(QtCore.SIGNAL('selected')) + #self.scene().selectionChanged.emit() ## for some reason this doesn't seem to be happening automatically + self.update() + #return ret + + elif int(ev.button()) == int(QtCore.Qt.RightButton): + #print " ev.button: right" + ev.accept() + #pos = ev.screenPos() + self.raiseContextMenu(ev) + #self.menu.popup(QtCore.QPoint(pos.x(), pos.y())) + + def mouseDragEvent(self, ev): + #print "Node.mouseDrag" + if ev.button() == QtCore.Qt.LeftButton: + ev.accept() + self.setPos(self.pos()+self.mapToParent(ev.pos())-self.mapToParent(ev.lastPos())) + + def hoverEvent(self, ev): + if not ev.isExit() and ev.acceptClicks(QtCore.Qt.LeftButton): + ev.acceptDrags(QtCore.Qt.LeftButton) + self.hovered = True + else: + self.hovered = False + self.update() + + #def mouseReleaseEvent(self, ev): + #ret = QtGui.QGraphicsItem.mouseReleaseEvent(self, ev) + #return ret + + def keyPressEvent(self, ev): + if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: + ev.accept() + if not self.node._allowRemove: + return + self.node.close() + else: + ev.ignore() + + def itemChange(self, change, val): + if change == self.ItemPositionHasChanged: + for k, t in self.terminals.iteritems(): + t[1].nodeMoved() + return QtGui.QGraphicsItem.itemChange(self, change, val) + + + #def contextMenuEvent(self, ev): + #ev.accept() + #self.menu.popup(ev.screenPos()) + + def getMenu(self): + return self.menu + + + def getContextMenus(self, event): + return [self.menu] + + def raiseContextMenu(self, ev): + menu = self.scene().addParentContextMenus(self, self.getMenu(), ev) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + + def buildMenu(self): + self.menu = QtGui.QMenu() + self.menu.setTitle("Node") + a = self.menu.addAction("Add input", self.node.addInput) + if not self.node._allowAddInput: + a.setEnabled(False) + a = self.menu.addAction("Add output", self.node.addOutput) + if not self.node._allowAddOutput: + a.setEnabled(False) + a = self.menu.addAction("Remove node", self.node.close) + if not self.node._allowRemove: + a.setEnabled(False) + + #def menuTriggered(self, action): + ##print "node.menuTriggered called. action:", action + #act = str(action.text()) + #if act == "Add input": + #self.node.addInput() + #self.updateActionMenu() + #elif act == "Add output": + #self.node.addOutput() + #self.updateActionMenu() + #elif act == "Remove node": + #self.node.close() + #else: ## only other option is to remove a terminal + #self.node.removeTerminal(act) + #self.terminalMenu.removeAction(action) + + #def updateActionMenu(self): + #for t in self.node.terminals: + #if t not in [str(a.text()) for a in self.terminalMenu.actions()]: + #self.terminalMenu.addAction(t) + #for a in self.terminalMenu.actions(): + #if str(a.text()) not in self.node.terminals: + #self.terminalMenu.removeAction(a) diff --git a/flowchart/Terminal.py b/flowchart/Terminal.py new file mode 100644 index 00000000..d41f702e --- /dev/null +++ b/flowchart/Terminal.py @@ -0,0 +1,555 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +import weakref +from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject +import pyqtgraph.functions as fn +from pyqtgraph.Point import Point +#from PySide import QtCore, QtGui +from eq import * + +class Terminal: + def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, bypass=None): + """Construct a new terminal. Optiona are: + node - the node to which this terminal belongs + name - string, the name of the terminal + io - 'in' or 'out' + optional - bool, whether the node may process without connection to this terminal + multi - bool, for inputs: whether this terminal may make multiple connections + for outputs: whether this terminal creates a different value for each connection + pos - [x, y], the position of the terminal within its node's boundaries + """ + self._io = io + #self._isOutput = opts[0] in ['out', 'io'] + #self._isInput = opts[0]] in ['in', 'io'] + #self._isIO = opts[0]=='io' + self._optional = optional + self._multi = multi + self._node = weakref.ref(node) + self._name = name + self._renamable = renamable + self._connections = {} + self._graphicsItem = TerminalGraphicsItem(self, parent=self._node().graphicsItem()) + self._bypass = bypass + + if multi: + self._value = {} ## dictionary of terminal:value pairs. + else: + self._value = None + + self.valueOk = None + self.recolor() + + def value(self, term=None): + """Return the value this terminal provides for the connected terminal""" + if term is None: + return self._value + + if self.isMultiValue(): + return self._value.get(term, None) + else: + return self._value + + def bypassValue(self): + return self._bypass + + def setValue(self, val, process=True): + """If this is a single-value terminal, val should be a single value. + If this is a multi-value terminal, val should be a dict of terminal:value pairs""" + if not self.isMultiValue(): + if eq(val, self._value): + return + self._value = val + else: + if val is not None: + self._value.update(val) + + self.setValueAcceptable(None) ## by default, input values are 'unchecked' until Node.update(). + if self.isInput() and process: + self.node().update() + + ## Let the flowchart handle this. + #if self.isOutput(): + #for c in self.connections(): + #if c.isInput(): + #c.inputChanged(self) + self.recolor() + + def connected(self, term): + """Called whenever this terminal has been connected to another. (note--this function is called on both terminals)""" + if self.isInput() and term.isOutput(): + self.inputChanged(term) + if self.isOutput() and self.isMultiValue(): + self.node().update() + self.node().connected(self, term) + + def disconnected(self, term): + """Called whenever this terminal has been disconnected from another. (note--this function is called on both terminals)""" + if self.isMultiValue() and term in self._value: + del self._value[term] + self.node().update() + #self.recolor() + else: + if self.isInput(): + self.setValue(None) + self.node().disconnected(self, term) + #self.node().update() + + def inputChanged(self, term, process=True): + """Called whenever there is a change to the input value to this terminal. + It may often be useful to override this function.""" + if self.isMultiValue(): + self.setValue({term: term.value(self)}, process=process) + else: + self.setValue(term.value(self), process=process) + + def valueIsAcceptable(self): + """Returns True->acceptable None->unknown False->Unacceptable""" + return self.valueOk + + def setValueAcceptable(self, v=True): + self.valueOk = v + self.recolor() + + def connections(self): + return self._connections + + def node(self): + return self._node() + + def isInput(self): + return self._io == 'in' + + def isMultiValue(self): + return self._multi + + def isOutput(self): + return self._io == 'out' + + def isRenamable(self): + return self._renamable + + def name(self): + return self._name + + def graphicsItem(self): + return self._graphicsItem + + def isConnected(self): + return len(self.connections()) > 0 + + def connectedTo(self, term): + return term in self.connections() + + def hasInput(self): + #conn = self.extendedConnections() + for t in self.connections(): + if t.isOutput(): + return True + return False + + def inputTerminals(self): + """Return the terminal(s) that give input to this one.""" + #terms = self.extendedConnections() + #for t in terms: + #if t.isOutput(): + #return t + return [t for t in self.connections() if t.isOutput()] + + + def dependentNodes(self): + """Return the list of nodes which receive input from this terminal.""" + #conn = self.extendedConnections() + #del conn[self] + return set([t.node() for t in self.connections() if t.isInput()]) + + def connectTo(self, term, connectionItem=None): + try: + if self.connectedTo(term): + raise Exception('Already connected') + if term is self: + raise Exception('Not connecting terminal to self') + if term.node() is self.node(): + raise Exception("Can't connect to terminal on same node.") + for t in [self, term]: + if t.isInput() and not t._multi and len(t.connections()) > 0: + raise Exception("Cannot connect %s <-> %s: Terminal %s is already connected to %s (and does not allow multiple connections)" % (self, term, t, t.connections().keys())) + #if self.hasInput() and term.hasInput(): + #raise Exception('Target terminal already has input') + + #if term in self.node().terminals.values(): + #if self.isOutput() or term.isOutput(): + #raise Exception('Can not connect an output back to the same node.') + except: + if connectionItem is not None: + connectionItem.close() + raise + + if connectionItem is None: + connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem()) + #self.graphicsItem().scene().addItem(connectionItem) + self.graphicsItem().getViewBox().addItem(connectionItem) + #connectionItem.setParentItem(self.graphicsItem().parent().parent()) + self._connections[term] = connectionItem + term._connections[self] = connectionItem + + self.recolor() + + #if self.isOutput() and term.isInput(): + #term.inputChanged(self) + #if term.isInput() and term.isOutput(): + #self.inputChanged(term) + self.connected(term) + term.connected(self) + + return connectionItem + + def disconnectFrom(self, term): + if not self.connectedTo(term): + return + item = self._connections[term] + #print "removing connection", item + #item.scene().removeItem(item) + item.close() + del self._connections[term] + del term._connections[self] + self.recolor() + term.recolor() + + self.disconnected(term) + term.disconnected(self) + #if self.isOutput() and term.isInput(): + #term.inputChanged(self) + #if term.isInput() and term.isOutput(): + #self.inputChanged(term) + + + def disconnectAll(self): + for t in self._connections.keys(): + self.disconnectFrom(t) + + def recolor(self, color=None, recurse=True): + if color is None: + if not self.isConnected(): ## disconnected terminals are black + color = QtGui.QColor(0,0,0) + elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals + color = QtGui.QColor(200,200,0) + elif self._value is None or eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error) + color = QtGui.QColor(255,255,255) + elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok + color = QtGui.QColor(200, 200, 0) + elif self.valueIsAcceptable() is True: ## terminal has good input, all ok + color = QtGui.QColor(0, 200, 0) + else: ## terminal has bad input + color = QtGui.QColor(200, 0, 0) + self.graphicsItem().setBrush(QtGui.QBrush(color)) + + if recurse: + for t in self.connections(): + t.recolor(color, recurse=False) + + + def rename(self, name): + oldName = self._name + self._name = name + self.node().terminalRenamed(self, oldName) + self.graphicsItem().termRenamed(name) + + def __repr__(self): + return "" % (str(self.node().name()), str(self.name())) + + #def extendedConnections(self, terms=None): + #"""Return list of terminals (including this one) that are directly or indirectly wired to this.""" + #if terms is None: + #terms = {} + #terms[self] = None + #for t in self._connections: + #if t in terms: + #continue + #terms.update(t.extendedConnections(terms)) + #return terms + + def __hash__(self): + return id(self) + + def close(self): + self.disconnectAll() + item = self.graphicsItem() + if item.scene() is not None: + item.scene().removeItem(item) + + def saveState(self): + return {'io': self._io, 'multi': self._multi, 'optional': self._optional} + + +#class TerminalGraphicsItem(QtGui.QGraphicsItem): +class TerminalGraphicsItem(GraphicsObject): + + def __init__(self, term, parent=None): + self.term = term + #QtGui.QGraphicsItem.__init__(self, parent) + GraphicsObject.__init__(self, parent) + self.brush = fn.mkBrush(0,0,0) + self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self) + self.label = QtGui.QGraphicsTextItem(self.term.name(), self) + self.label.scale(0.7, 0.7) + #self.setAcceptHoverEvents(True) + self.newConnection = None + self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem + if self.term.isRenamable(): + self.label.setTextInteractionFlags(QtCore.Qt.TextEditorInteraction) + self.label.focusOutEvent = self.labelFocusOut + self.label.keyPressEvent = self.labelKeyPress + self.setZValue(1) + self.menu = None + + + def labelFocusOut(self, ev): + QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev) + self.labelChanged() + + def labelKeyPress(self, ev): + if ev.key() == QtCore.Qt.Key_Enter or ev.key() == QtCore.Qt.Key_Return: + self.labelChanged() + else: + QtGui.QGraphicsTextItem.keyPressEvent(self.label, ev) + + def labelChanged(self): + newName = str(self.label.toPlainText()) + if newName != self.term.name(): + self.term.rename(newName) + + def termRenamed(self, name): + self.label.setPlainText(name) + + def setBrush(self, brush): + self.brush = brush + self.box.setBrush(brush) + + def disconnect(self, target): + self.term.disconnectFrom(target.term) + + def boundingRect(self): + br = self.box.mapRectToParent(self.box.boundingRect()) + lr = self.label.mapRectToParent(self.label.boundingRect()) + return br | lr + + def paint(self, p, *args): + pass + + def setAnchor(self, x, y): + pos = QtCore.QPointF(x, y) + self.anchorPos = pos + br = self.box.mapRectToParent(self.box.boundingRect()) + lr = self.label.mapRectToParent(self.label.boundingRect()) + + + if self.term.isInput(): + self.box.setPos(pos.x(), pos.y()-br.height()/2.) + self.label.setPos(pos.x() + br.width(), pos.y() - lr.height()/2.) + else: + self.box.setPos(pos.x()-br.width(), pos.y()-br.height()/2.) + self.label.setPos(pos.x()-br.width()-lr.width(), pos.y()-lr.height()/2.) + self.updateConnections() + + def updateConnections(self): + for t, c in self.term.connections().iteritems(): + c.updateLine() + + def mousePressEvent(self, ev): + #ev.accept() + ev.ignore() + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + ev.accept() + self.label.setFocus(QtCore.Qt.MouseFocusReason) + if ev.button() == QtCore.Qt.RightButton: + if self.raiseContextMenu(ev): + ev.accept() + + def raiseContextMenu(self, ev): + ## only raise menu if this terminal is removable + menu = self.getMenu() + if menu is None: + return False + menu = self.scene().addParentContextMenus(self, menu, ev) + pos = ev.screenPos() + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + return True + + def getMenu(self): + if self.menu is None: + if self.removable(): + self.menu = QtGui.QMenu() + self.menu.setTitle("Terminal") + self.menu.addAction("Remove terminal", self.removeSelf) + else: + return None + return self.menu + + def removable(self): + return ( + (self.term.isOutput() and self.term.node()._allowAddOutput) or + (self.term.isInput() and self.term.node()._allowAddInput)) + + ## probably never need this + #def getContextMenus(self, ev): + #return [self.getMenu()] + + def removeSelf(self): + self.term.node().removeTerminal(self.term) + + def mouseDragEvent(self, ev): + if ev.button() != QtCore.Qt.LeftButton: + ev.ignore() + return + + ev.accept() + if ev.isStart(): + if self.newConnection is None: + self.newConnection = ConnectionItem(self) + #self.scene().addItem(self.newConnection) + self.getViewBox().addItem(self.newConnection) + #self.newConnection.setParentItem(self.parent().parent()) + + self.newConnection.setTarget(self.mapToView(ev.pos())) + elif ev.isFinish(): + if self.newConnection is not None: + items = self.scene().items(ev.scenePos()) + gotTarget = False + for i in items: + if isinstance(i, TerminalGraphicsItem): + self.newConnection.setTarget(i) + try: + self.term.connectTo(i.term, self.newConnection) + gotTarget = True + except: + self.scene().removeItem(self.newConnection) + self.newConnection = None + raise + break + + if not gotTarget: + #print "remove unused connection" + #self.scene().removeItem(self.newConnection) + self.newConnection.close() + self.newConnection = None + else: + if self.newConnection is not None: + self.newConnection.setTarget(self.mapToView(ev.pos())) + + def hoverEvent(self, ev): + if not ev.isExit() and ev.acceptDrags(QtCore.Qt.LeftButton): + ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. + self.box.setBrush(fn.mkBrush('w')) + else: + self.box.setBrush(self.brush) + self.update() + + #def hoverEnterEvent(self, ev): + #self.hover = True + + #def hoverLeaveEvent(self, ev): + #self.hover = False + + def connectPoint(self): + ## return the connect position of this terminal in view coords + return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center())) + + def nodeMoved(self): + for t, item in self.term.connections().iteritems(): + item.updateLine() + + +#class ConnectionItem(QtGui.QGraphicsItem): +class ConnectionItem(GraphicsObject): + + def __init__(self, source, target=None): + #QtGui.QGraphicsItem.__init__(self) + GraphicsObject.__init__(self) + self.setFlags( + self.ItemIsSelectable | + self.ItemIsFocusable + ) + self.source = source + self.target = target + self.length = 0 + self.hovered = False + #self.line = QtGui.QGraphicsLineItem(self) + self.source.getViewBox().addItem(self) + self.updateLine() + self.setZValue(0) + + def close(self): + if self.scene() is not None: + #self.scene().removeItem(self.line) + self.scene().removeItem(self) + + def setTarget(self, target): + self.target = target + self.updateLine() + + def updateLine(self): + start = Point(self.source.connectPoint()) + if isinstance(self.target, TerminalGraphicsItem): + stop = Point(self.target.connectPoint()) + elif isinstance(self.target, QtCore.QPointF): + stop = Point(self.target) + else: + return + self.prepareGeometryChange() + self.resetTransform() + ang = (stop-start).angle(Point(0, 1)) + if ang is None: + ang = 0 + self.rotate(ang) + self.setPos(start) + self.length = (start-stop).length() + self.update() + #self.line.setLine(start.x(), start.y(), stop.x(), stop.y()) + + def keyPressEvent(self, ev): + if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: + #if isinstance(self.target, TerminalGraphicsItem): + self.source.disconnect(self.target) + ev.accept() + else: + ev.ignore() + + def mousePressEvent(self, ev): + ev.ignore() + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton: + ev.accept() + sel = self.isSelected() + self.setSelected(True) + if not sel and self.isSelected(): + self.update() + + def hoverEvent(self, ev): + if (not ev.isExit()) and ev.acceptClicks(QtCore.Qt.LeftButton): + self.hovered = True + else: + self.hovered = False + self.update() + + + def boundingRect(self): + #return self.line.boundingRect() + px = self.pixelWidth() + return QtCore.QRectF(-5*px, 0, 10*px, self.length) + + #def shape(self): + #return self.line.shape() + + def paint(self, p, *args): + if self.isSelected(): + p.setPen(fn.mkPen(200, 200, 0, width=3)) + else: + if self.hovered: + p.setPen(fn.mkPen(150, 150, 250, width=1)) + else: + p.setPen(fn.mkPen(100, 100, 250, width=1)) + + p.drawLine(0, 0, 0, self.length) diff --git a/flowchart/__init__.py b/flowchart/__init__.py new file mode 100644 index 00000000..24f562f4 --- /dev/null +++ b/flowchart/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from Flowchart import * + +from library import getNodeType, registerNodeType, getNodeTree \ No newline at end of file diff --git a/flowchart/eq.py b/flowchart/eq.py new file mode 100644 index 00000000..f2f744e4 --- /dev/null +++ b/flowchart/eq.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from numpy import ndarray, bool_ + +def eq(a, b): + """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" + try: + e = a==b + except ValueError: + return False + except AttributeError: + return False + except: + print "a:", str(type(a)), str(a) + print "b:", str(type(b)), str(b) + raise + t = type(e) + if t is bool: + return e + elif t is bool_: + return bool(e) + elif isinstance(e, ndarray): + try: ## disaster: if a is an empty array and b is not, then e.all() is True + if a.shape != b.shape: + return False + except: + return False + return e.all() + else: + raise Exception("== operator returned type %s" % str(type(e))) diff --git a/flowchart/library/Data.py b/flowchart/library/Data.py new file mode 100644 index 00000000..e8999a3d --- /dev/null +++ b/flowchart/library/Data.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- +from ..Node import Node +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +from common import * +from pyqtgraph.Transform import Transform +from pyqtgraph.Point import Point +from pyqtgraph.widgets.TreeWidget import TreeWidget +from pyqtgraph.graphicsItems.LinearRegionItem import LinearRegionItem + +import functions + +try: + import metaarray + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + +class ColumnSelectNode(Node): + """Select named columns from a record array or MetaArray.""" + nodeName = "ColumnSelect" + def __init__(self, name): + Node.__init__(self, name, terminals={'In': {'io': 'in'}}) + self.columns = set() + self.columnList = QtGui.QListWidget() + self.axis = 0 + self.columnList.itemChanged.connect(self.itemChanged) + + def process(self, In, display=True): + if display: + self.updateList(In) + + out = {} + if HAVE_METAARRAY and isinstance(In, metaarray.MetaArray): + for c in self.columns: + out[c] = In[self.axis:c] + elif isinstance(In, np.ndarray) and In.dtype.fields is not None: + for c in self.columns: + out[c] = In[c] + else: + self.In.setValueAcceptable(False) + raise Exception("Input must be MetaArray or ndarray with named fields") + + return out + + def ctrlWidget(self): + return self.columnList + + def updateList(self, data): + if HAVE_METAARRAY and isinstance(data, metaarray.MetaArray): + cols = data.listColumns() + for ax in cols: ## find first axis with columns + if len(cols[ax]) > 0: + self.axis = ax + cols = set(cols[ax]) + break + else: + cols = data.dtype.fields.keys() + + rem = set() + for c in self.columns: + if c not in cols: + self.removeTerminal(c) + rem.add(c) + self.columns -= rem + + self.columnList.blockSignals(True) + self.columnList.clear() + for c in cols: + item = QtGui.QListWidgetItem(c) + item.setFlags(QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsUserCheckable) + if c in self.columns: + item.setCheckState(QtCore.Qt.Checked) + else: + item.setCheckState(QtCore.Qt.Unchecked) + self.columnList.addItem(item) + self.columnList.blockSignals(False) + + + def itemChanged(self, item): + col = str(item.text()) + if item.checkState() == QtCore.Qt.Checked: + if col not in self.columns: + self.columns.add(col) + self.addOutput(col) + else: + if col in self.columns: + self.columns.remove(col) + self.removeTerminal(col) + self.update() + + def saveState(self): + state = Node.saveState(self) + state['columns'] = list(self.columns) + return state + + def restoreState(self, state): + Node.restoreState(self, state) + self.columns = set(state.get('columns', [])) + for c in self.columns: + self.addOutput(c) + + + +class RegionSelectNode(CtrlNode): + """Returns a slice from a 1-D array. Connect the 'widget' output to a plot to display a region-selection widget.""" + nodeName = "RegionSelect" + uiTemplate = [ + ('start', 'spin', {'value': 0, 'step': 0.1}), + ('stop', 'spin', {'value': 0.1, 'step': 0.1}), + ('display', 'check', {'value': True}), + ('movable', 'check', {'value': True}), + ] + + def __init__(self, name): + self.items = {} + CtrlNode.__init__(self, name, terminals={ + 'data': {'io': 'in'}, + 'selected': {'io': 'out'}, + 'region': {'io': 'out'}, + 'widget': {'io': 'out', 'multi': True} + }) + self.ctrls['display'].toggled.connect(self.displayToggled) + self.ctrls['movable'].toggled.connect(self.movableToggled) + + def displayToggled(self, b): + for item in self.items.itervalues(): + item.setVisible(b) + + def movableToggled(self, b): + for item in self.items.itervalues(): + item.setMovable(b) + + + def process(self, data=None, display=True): + #print "process.." + s = self.stateGroup.state() + region = [s['start'], s['stop']] + + if display: + conn = self['widget'].connections() + for c in conn: + plot = c.node().getPlot() + if plot is None: + continue + if c in self.items: + item = self.items[c] + item.setRegion(region) + #print " set rgn:", c, region + #item.setXVals(events) + else: + item = LinearRegionItem(values=region) + self.items[c] = item + #item.connect(item, QtCore.SIGNAL('regionChanged'), self.rgnChanged) + item.sigRegionChanged.connect(self.rgnChanged) + item.setVisible(s['display']) + item.setMovable(s['movable']) + #print " new rgn:", c, region + #self.items[c].setYRange([0., 0.2], relative=True) + + if self.selected.isConnected(): + if data is None: + sliced = None + elif isinstance(data, MetaArray): + sliced = data[0:s['start']:s['stop']] + else: + mask = (data['time'] >= s['start']) * (data['time'] < s['stop']) + sliced = data[mask] + else: + sliced = None + + return {'selected': sliced, 'widget': self.items, 'region': region} + + + def rgnChanged(self, item): + region = item.getRegion() + self.stateGroup.setState({'start': region[0], 'stop': region[1]}) + self.update() + + +class EvalNode(Node): + """Return the output of a string evaluated/executed by the python interpreter. + The string may be either an expression or a python script, and inputs are accessed as the name of the terminal. + For expressions, a single value may be evaluated for a single output, or a dict for multiple outputs. + For a script, the text will be executed as the body of a function.""" + nodeName = 'PythonEval' + + def __init__(self, name): + Node.__init__(self, name, + terminals = { + 'input': {'io': 'in', 'renamable': True}, + 'output': {'io': 'out', 'renamable': True}, + }, + allowAddInput=True, allowAddOutput=True) + + self.ui = QtGui.QWidget() + self.layout = QtGui.QGridLayout() + self.addInBtn = QtGui.QPushButton('+Input') + self.addOutBtn = QtGui.QPushButton('+Output') + self.text = QtGui.QTextEdit() + self.text.setTabStopWidth(30) + self.layout.addWidget(self.addInBtn, 0, 0) + self.layout.addWidget(self.addOutBtn, 0, 1) + self.layout.addWidget(self.text, 1, 0, 1, 2) + self.ui.setLayout(self.layout) + + #QtCore.QObject.connect(self.addInBtn, QtCore.SIGNAL('clicked()'), self.addInput) + self.addInBtn.clicked.connect(self.addInput) + #QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput) + self.addOutBtn.clicked.connect(self.addOutput) + self.ui.focusOutEvent = lambda ev: self.focusOutEvent(ev) + self.lastText = None + + def ctrlWidget(self): + return self.ui + + def addInput(self): + Node.addInput(self, 'input', renamable=True) + + def addOutput(self): + Node.addOutput(self, 'output', renamable=True) + + def focusOutEvent(self, ev): + text = str(self.text.toPlainText()) + if text != self.lastText: + self.lastText = text + print "eval node update" + self.update() + + def process(self, display=True, **args): + l = locals() + l.update(args) + ## try eval first, then exec + try: + text = str(self.text.toPlainText()).replace('\n', ' ') + output = eval(text, globals(), l) + except SyntaxError: + fn = "def fn(**args):\n" + run = "\noutput=fn(**args)\n" + text = fn + "\n".join([" "+l for l in str(self.text.toPlainText()).split('\n')]) + run + exec(text) + return output + + def saveState(self): + state = Node.saveState(self) + state['text'] = str(self.text.toPlainText()) + state['terminals'] = self.saveTerminals() + return state + + def restoreState(self, state): + Node.restoreState(self, state) + self.text.clear() + self.text.insertPlainText(state['text']) + self.restoreTerminals(state['terminals']) + self.update() + +class ColumnJoinNode(Node): + """Concatenates record arrays and/or adds new columns""" + nodeName = 'ColumnJoin' + + def __init__(self, name): + Node.__init__(self, name, terminals = { + 'output': {'io': 'out'}, + }) + + #self.items = [] + + self.ui = QtGui.QWidget() + self.layout = QtGui.QGridLayout() + self.ui.setLayout(self.layout) + + self.tree = TreeWidget() + self.addInBtn = QtGui.QPushButton('+ Input') + self.remInBtn = QtGui.QPushButton('- Input') + + self.layout.addWidget(self.tree, 0, 0, 1, 2) + self.layout.addWidget(self.addInBtn, 1, 0) + self.layout.addWidget(self.remInBtn, 1, 1) + + self.addInBtn.clicked.connect(self.addInput) + self.remInBtn.clicked.connect(self.remInput) + self.tree.sigItemMoved.connect(self.update) + + def ctrlWidget(self): + return self.ui + + def addInput(self): + #print "ColumnJoinNode.addInput called." + term = Node.addInput(self, 'input', renamable=True) + #print "Node.addInput returned. term:", term + item = QtGui.QTreeWidgetItem([term.name()]) + item.term = term + term.joinItem = item + #self.items.append((term, item)) + self.tree.addTopLevelItem(item) + + def remInput(self): + sel = self.tree.currentItem() + term = sel.term + term.joinItem = None + sel.term = None + self.tree.removeTopLevelItem(sel) + self.removeTerminal(term) + self.update() + + def process(self, display=True, **args): + order = self.order() + vals = [] + for name in order: + if name not in args: + continue + val = args[name] + if isinstance(val, np.ndarray) and len(val.dtype) > 0: + vals.append(val) + else: + vals.append((name, None, val)) + return {'output': functions.concatenateColumns(vals)} + + def order(self): + return [str(self.tree.topLevelItem(i).text(0)) for i in range(self.tree.topLevelItemCount())] + + def saveState(self): + state = Node.saveState(self) + state['order'] = self.order() + return state + + def restoreState(self, state): + Node.restoreState(self, state) + inputs = [inp.name() for inp in self.inputs()] + for name in inputs: + if name not in state['order']: + self.removeTerminal(name) + for name in state['order']: + if name not in inputs: + Node.addInput(self, name, renamable=True) + + self.tree.clear() + for name in state['order']: + term = self[name] + item = QtGui.QTreeWidgetItem([name]) + item.term = term + term.joinItem = item + #self.items.append((term, item)) + self.tree.addTopLevelItem(item) + + def terminalRenamed(self, term, oldName): + Node.terminalRenamed(self, term, oldName) + item = term.joinItem + item.setText(0, term.name()) + self.update() + + \ No newline at end of file diff --git a/flowchart/library/Display.py b/flowchart/library/Display.py new file mode 100644 index 00000000..4379858f --- /dev/null +++ b/flowchart/library/Display.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +from ..Node import Node +import weakref +#from pyqtgraph import graphicsItems +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.graphicsItems.ScatterPlotItem import ScatterPlotItem +from pyqtgraph.graphicsItems.PlotCurveItem import PlotCurveItem + +from common import * +import numpy as np + +class PlotWidgetNode(Node): + """Connection to PlotWidget. Will plot arrays, metaarrays, and display event lists.""" + nodeName = 'PlotWidget' + sigPlotChanged = QtCore.Signal(object) + + def __init__(self, name): + Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}}) + self.plot = None + self.items = {} + + def disconnected(self, localTerm, remoteTerm): + if localTerm is self.In and remoteTerm in self.items: + self.plot.removeItem(self.items[remoteTerm]) + del self.items[remoteTerm] + + def setPlot(self, plot): + #print "======set plot" + self.plot = plot + self.sigPlotChanged.emit(self) + + def getPlot(self): + return self.plot + + def process(self, In, display=True): + if display: + #self.plot.clearPlots() + items = set() + for name, vals in In.iteritems(): + if vals is None: + continue + if type(vals) is not list: + vals = [vals] + + for val in vals: + vid = id(val) + if vid in self.items: + items.add(vid) + else: + #if isinstance(val, PlotCurveItem): + #self.plot.addItem(val) + #item = val + #if isinstance(val, ScatterPlotItem): + #self.plot.addItem(val) + #item = val + if isinstance(val, QtGui.QGraphicsItem): + self.plot.addItem(val) + item = val + else: + item = self.plot.plot(val) + self.items[vid] = item + items.add(vid) + for vid in self.items.keys(): + if vid not in items: + #print "remove", self.items[vid] + self.plot.removeItem(self.items[vid]) + del self.items[vid] + + #def setInput(self, **args): + #for k in args: + #self.plot.plot(args[k]) + + + +class CanvasNode(Node): + """Connection to a Canvas widget.""" + nodeName = 'CanvasWidget' + + def __init__(self, name): + Node.__init__(self, name, terminals={'In': {'io': 'in', 'multi': True}}) + self.canvas = None + self.items = {} + + def disconnected(self, localTerm, remoteTerm): + if localTerm is self.In and remoteTerm in self.items: + self.canvas.removeItem(self.items[remoteTerm]) + del self.items[remoteTerm] + + def setCanvas(self, canvas): + self.canvas = canvas + + def getCanvas(self): + return self.canvas + + def process(self, In, display=True): + if display: + items = set() + for name, vals in In.iteritems(): + if vals is None: + continue + if type(vals) is not list: + vals = [vals] + + for val in vals: + vid = id(val) + if vid in self.items: + items.add(vid) + else: + self.canvas.addItem(val) + item = val + self.items[vid] = item + items.add(vid) + for vid in self.items.keys(): + if vid not in items: + #print "remove", self.items[vid] + self.canvas.removeItem(self.items[vid]) + del self.items[vid] + + + + +class ScatterPlot(CtrlNode): + """Generates a scatter plot from a record array or nested dicts""" + nodeName = 'ScatterPlot' + uiTemplate = [ + ('x', 'combo', {'values': [], 'index': 0}), + ('y', 'combo', {'values': [], 'index': 0}), + ('sizeEnabled', 'check', {'value': False}), + ('size', 'combo', {'values': [], 'index': 0}), + ('absoluteSize', 'check', {'value': False}), + ('colorEnabled', 'check', {'value': False}), + ('color', 'colormap', {}), + ('borderEnabled', 'check', {'value': False}), + ('border', 'colormap', {}), + ] + + def __init__(self, name): + CtrlNode.__init__(self, name, terminals={ + 'input': {'io': 'in'}, + 'plot': {'io': 'out'} + }) + self.item = ScatterPlotItem() + self.keys = [] + + #self.ui = QtGui.QWidget() + #self.layout = QtGui.QGridLayout() + #self.ui.setLayout(self.layout) + + #self.xCombo = QtGui.QComboBox() + #self.yCombo = QtGui.QComboBox() + + + + def process(self, input, display=True): + #print "scatterplot process" + if not display: + return {'plot': None} + + self.updateKeys(input[0]) + + x = str(self.ctrls['x'].currentText()) + y = str(self.ctrls['y'].currentText()) + size = str(self.ctrls['size'].currentText()) + pen = QtGui.QPen(QtGui.QColor(0,0,0,0)) + points = [] + for i in input: + pt = {'pos': (i[x], i[y])} + if self.ctrls['sizeEnabled'].isChecked(): + pt['size'] = i[size] + if self.ctrls['borderEnabled'].isChecked(): + pt['pen'] = QtGui.QPen(self.ctrls['border'].getColor(i)) + else: + pt['pen'] = pen + if self.ctrls['colorEnabled'].isChecked(): + pt['brush'] = QtGui.QBrush(self.ctrls['color'].getColor(i)) + points.append(pt) + self.item.setPxMode(not self.ctrls['absoluteSize'].isChecked()) + + self.item.setPoints(points) + + return {'plot': self.item} + + + + def updateKeys(self, data): + if isinstance(data, dict): + keys = data.keys() + elif isinstance(data, list) or isinstance(data, tuple): + keys = data + elif isinstance(data, np.ndarray) or isinstance(data, np.void): + keys = data.dtype.names + else: + print "Unknown data type:", type(data), data + return + + for c in self.ctrls.itervalues(): + c.blockSignals(True) + for c in [self.ctrls['x'], self.ctrls['y'], self.ctrls['size']]: + cur = str(c.currentText()) + c.clear() + for k in keys: + c.addItem(k) + if k == cur: + c.setCurrentIndex(c.count()-1) + for c in [self.ctrls['color'], self.ctrls['border']]: + c.setArgList(keys) + for c in self.ctrls.itervalues(): + c.blockSignals(False) + + self.keys = keys + + + def saveState(self): + state = CtrlNode.saveState(self) + return {'keys': self.keys, 'ctrls': state} + + def restoreState(self, state): + self.updateKeys(state['keys']) + CtrlNode.restoreState(self, state['ctrls']) + +#class ImageItem(Node): + #"""Creates an ImageItem for display in a canvas from a file handle.""" + #nodeName = 'Image' + + #def __init__(self, name): + #Node.__init__(self, name, terminals={ + #'file': {'io': 'in'}, + #'image': {'io': 'out'} + #}) + #self.imageItem = graphicsItems.ImageItem() + #self.handle = None + + #def process(self, file, display=True): + #if not display: + #return {'image': None} + + #if file != self.handle: + #self.handle = file + #data = file.read() + #self.imageItem.updateImage(data) + + #pos = file. + + + \ No newline at end of file diff --git a/flowchart/library/EventDetection.py b/flowchart/library/EventDetection.py new file mode 100644 index 00000000..eb23b90a --- /dev/null +++ b/flowchart/library/EventDetection.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +from ..Node import Node +import functions +from common import * + +class Threshold(CtrlNode): + """Absolute threshold detection filter. Returns indexes where data crosses threshold.""" + nodeName = 'ThresholdDetect' + uiTemplate = [ + ('direction', 'combo', {'values': ['rising', 'falling'], 'index': 0}), + ('threshold', 'spin', {'value': 0, 'step': 1, 'minStep': 1e-12, 'dec': True, 'range': [None, None], 'siPrefix': True}), + ] + + def __init__(self, name, **opts): + CtrlNode.__init__(self, name, self.uiTemplate) + + def processData(self, data): + s = self.stateGroup.state() + if s['direction'] == 'rising': + d = 1 + else: + d = -1 + return functions.threshold(data, s['threshold'], d) + +class StdevThreshold(CtrlNode): + """Relative threshold event detection. Finds regions in data greater than threshold*stdev. + Returns a record array with columns: index, length, sum, peak. + This function is only useful for data with its baseline removed.""" + + nodeName = 'StdevThreshold' + uiTemplate = [ + ('threshold', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True}), + ] + + def __init__(self, name, **opts): + CtrlNode.__init__(self, name, self.uiTemplate) + + def processData(self, data): + s = self.stateGroup.state() + return functions.stdevThresholdEvents(data, s['threshold']) + + +class ZeroCrossingEvents(CtrlNode): + """Detects events in a waveform by splitting the data up into chunks separated by zero-crossings, + then keeping only the ones that meet certain criteria.""" + nodeName = 'ZeroCrossing' + uiTemplate = [ + ('minLength', 'intSpin', {'value': 0, 'min': 0, 'max': 100000}), + ('minSum', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True}), + ('minPeak', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True}), + ('eventLimit', 'intSpin', {'value': 400, 'min': 1, 'max': 1e9}), + ] + + def __init__(self, name, **opts): + CtrlNode.__init__(self, name, self.uiTemplate) + + def processData(self, data): + s = self.stateGroup.state() + events = functions.zeroCrossingEvents(data, minLength=s['minLength'], minPeak=s['minPeak'], minSum=s['minSum']) + events = events[:s['eventLimit']] + return events + +class ThresholdEvents(CtrlNode): + """Detects regions of a waveform that cross a threshold (positive or negative) and returns the time, length, sum, and peak of each event.""" + nodeName = 'ThresholdEvents' + uiTemplate = [ + ('threshold', 'spin', {'value': 1e-12, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True, 'tip': 'Events are detected only if they cross this threshold.'}), + ('adjustTimes', 'check', {'value': True, 'tip': 'If False, then event times are reported where the trace crosses threshold. If True, the event time is adjusted to estimate when the trace would have crossed 0.'}), + #('index', 'combo', {'values':['start','peak'], 'index':0}), + ('minLength', 'intSpin', {'value': 0, 'min': 0, 'max': 1e9, 'tip': 'Events must contain this many samples to be detected.'}), + ('minSum', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True}), + ('minPeak', 'spin', {'value': 0, 'step': 1, 'minStep': 0.1, 'dec': True, 'range': [None, None], 'siPrefix': True, 'tip': 'Events must reach this threshold to be detected.'}), + ('eventLimit', 'intSpin', {'value': 100, 'min': 1, 'max': 1e9, 'tip': 'Limits the number of events that may be detected in a single trace. This prevents runaway processes due to over-sensitive detection criteria.'}), + ('deadTime', 'spin', {'value': 0, 'step': 1, 'minStep': 1e-4, 'range': [0,None], 'siPrefix': True, 'suffix': 's', 'tip': 'Ignore events that occur too quickly following another event.'}), + ('reverseTime', 'spin', {'value': 0, 'step': 1, 'minStep': 1e-4, 'range': [0,None], 'siPrefix': True, 'suffix': 's', 'tip': 'Ignore events that 1) have the opposite sign of the event immediately prior and 2) occur within the given time window after the prior event. This is useful for ignoring rebound signals.'}), + ] + + def __init__(self, name, **opts): + CtrlNode.__init__(self, name, self.uiTemplate) + #self.addOutput('plot') + #self.remotePlot = None + + #def connected(self, term, remote): + #CtrlNode.connected(self, term, remote) + #if term is not self.plot: + #return + #node = remote.node() + #node.sigPlotChanged.connect(self.connectToPlot) + #self.connectToPlot(node) + + #def connectToPlot(self, node): + #if self.remotePlot is not None: + #self.remotePlot = None + + #if node.plot is None: + #return + #plot = self.plot. + + #def disconnected(self, term, remote): + #CtrlNode.disconnected(self, term, remote) + #if term is not self.plot: + #return + #remote.node().sigPlotChanged.disconnect(self.connectToPlot) + #self.disconnectFromPlot() + + #def disconnectFromPlot(self): + #if self.remotePlot is None: + #return + #for l in self.lines: + #l.scene().removeItem(l) + #self.lines = [] + + def processData(self, data): + s = self.stateGroup.state() + events = functions.thresholdEvents(data, s['threshold'], s['adjustTimes']) + + ## apply first round of filters + mask = events['len'] >= s['minLength'] + mask *= abs(events['sum']) >= s['minSum'] + mask *= abs(events['peak']) >= s['minPeak'] + events = events[mask] + + ## apply deadtime filter + mask = np.ones(len(events), dtype=bool) + last = 0 + dt = s['deadTime'] + rt = s['reverseTime'] + for i in xrange(1, len(events)): + tdiff = events[i]['time'] - events[last]['time'] + if tdiff < dt: ## check dead time + mask[i] = False + elif tdiff < rt and (events[i]['peak'] * events[last]['peak'] < 0): ## check reverse time + mask[i] = False + else: + last = i + #mask[1:] *= (events[1:]['time']-events[:-1]['time']) >= s['deadTime'] + events = events[mask] + + ## limit number of events + events = events[:s['eventLimit']] + return events + + + + + +class SpikeDetector(CtrlNode): + """Very simple spike detector. Returns the indexes of sharp spikes by comparing each sample to its neighbors.""" + nodeName = "SpikeDetect" + uiTemplate = [ + ('radius', 'intSpin', {'value': 1, 'min': 1, 'max': 100000}), + ('minDiff', 'spin', {'value': 0, 'step': 1, 'minStep': 1e-12, 'dec': True, 'siPrefix': True}), + ] + + def __init__(self, name, **opts): + CtrlNode.__init__(self, name, self.uiTemplate) + + def processData(self, data): + s = self.stateGroup.state() + radius = s['radius'] + d1 = data.view(np.ndarray) + d2 = data[radius:] - data[:-radius] #a derivative + mask1 = d2 > s['minDiff'] #where derivative is large and positive + mask2 = d2 < -s['minDiff'] #where derivative is large and negative + maskpos = mask1[:-radius] * mask2[radius:] #both need to be true + maskneg = mask1[radius:] * mask2[:-radius] + mask = maskpos + maskneg ## All regions that match criteria + + ## now reduce consecutive hits to a single hit. + hits = (mask[1:] - mask[:-1]) > 0 + sHits = np.argwhere(hits)[:,0]+(radius+2) + + ## convert to record array with 'index' column + ret = np.empty(len(sHits), dtype=[('index', int), ('time', float)]) + ret['index'] = sHits + ret['time'] = data.xvals('Time')[sHits] + return ret + + def processBypassed(self, args): + return {'Out': np.empty(0, dtype=[('index', int), ('time', float)])} + + + + + + \ No newline at end of file diff --git a/flowchart/library/Filters.py b/flowchart/library/Filters.py new file mode 100644 index 00000000..1819b01e --- /dev/null +++ b/flowchart/library/Filters.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +from ..Node import Node +from scipy.signal import detrend +from scipy.ndimage import median_filter, gaussian_filter +#from pyqtgraph.SignalProxy import SignalProxy +import functions +from common import * +import numpy as np + +try: + import metaarray + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + + +class Downsample(CtrlNode): + """Downsample by averaging samples together.""" + nodeName = 'Downsample' + uiTemplate = [ + ('n', 'intSpin', {'min': 1, 'max': 1000000}) + ] + + def processData(self, data): + return functions.downsample(data, self.ctrls['n'].value(), axis=0) + + +class Subsample(CtrlNode): + """Downsample by selecting every Nth sample.""" + nodeName = 'Subsample' + uiTemplate = [ + ('n', 'intSpin', {'min': 1, 'max': 1000000}) + ] + + def processData(self, data): + return data[::self.ctrls['n'].value()] + + +class Bessel(CtrlNode): + """Bessel filter. Input data must have time values.""" + nodeName = 'BesselFilter' + uiTemplate = [ + ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), + ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('order', 'intSpin', {'value': 4, 'min': 1, 'max': 16}), + ('bidir', 'check', {'checked': True}) + ] + + def processData(self, data): + s = self.stateGroup.state() + if s['band'] == 'lowpass': + mode = 'low' + else: + mode = 'high' + return functions.besselFilter(data, bidir=s['bidir'], btype=mode, cutoff=s['cutoff'], order=s['order']) + + +class Butterworth(CtrlNode): + """Butterworth filter""" + nodeName = 'ButterworthFilter' + uiTemplate = [ + ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), + ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('bidir', 'check', {'checked': True}) + ] + + def processData(self, data): + s = self.stateGroup.state() + if s['band'] == 'lowpass': + mode = 'low' + else: + mode = 'high' + ret = functions.butterworthFilter(data, bidir=s['bidir'], btype=mode, wPass=s['wPass'], wStop=s['wStop'], gPass=s['gPass'], gStop=s['gStop']) + return ret + + +class Mean(CtrlNode): + """Filters data by taking the mean of a sliding window""" + nodeName = 'MeanFilter' + uiTemplate = [ + ('n', 'intSpin', {'min': 1, 'max': 1000000}) + ] + + @metaArrayWrapper + def processData(self, data): + n = self.ctrls['n'].value() + return functions.rollingSum(data, n) / n + + +class Median(CtrlNode): + """Filters data by taking the median of a sliding window""" + nodeName = 'MedianFilter' + uiTemplate = [ + ('n', 'intSpin', {'min': 1, 'max': 1000000}) + ] + + @metaArrayWrapper + def processData(self, data): + return median_filter(data, self.ctrls['n'].value()) + +class Mode(CtrlNode): + """Filters data by taking the mode (histogram-based) of a sliding window""" + nodeName = 'ModeFilter' + uiTemplate = [ + ('window', 'intSpin', {'value': 500, 'min': 1, 'max': 1000000}), + ] + + @metaArrayWrapper + def processData(self, data): + return functions.modeFilter(data, self.ctrls['window'].value()) + + +class Denoise(CtrlNode): + """Removes anomalous spikes from data, replacing with nearby values""" + nodeName = 'DenoiseFilter' + uiTemplate = [ + ('radius', 'intSpin', {'value': 2, 'min': 0, 'max': 1000000}), + ('threshold', 'doubleSpin', {'value': 4.0, 'min': 0, 'max': 1000}) + ] + + def processData(self, data): + #print "DENOISE" + s = self.stateGroup.state() + return functions.denoise(data, **s) + + +class Gaussian(CtrlNode): + """Gaussian smoothing filter.""" + nodeName = 'GaussianFilter' + uiTemplate = [ + ('sigma', 'doubleSpin', {'min': 0, 'max': 1000000}) + ] + + @metaArrayWrapper + def processData(self, data): + return gaussian_filter(data, self.ctrls['sigma'].value()) + + +class Derivative(CtrlNode): + """Returns the pointwise derivative of the input""" + nodeName = 'DerivativeFilter' + + def processData(self, data): + if HAVE_METAARRAY and isinstance(data, metaarray.MetaArray): + info = data.infoCopy() + if 'values' in info[0]: + info[0]['values'] = info[0]['values'][:-1] + return MetaArray(data[1:] - data[:-1], info=info) + else: + return data[1:] - data[:-1] + + +class Integral(CtrlNode): + """Returns the pointwise integral of the input""" + nodeName = 'IntegralFilter' + + @metaArrayWrapper + def processData(self, data): + data[1:] += data[:-1] + return data + + +class Detrend(CtrlNode): + """Removes linear trend from the data""" + nodeName = 'DetrendFilter' + + @metaArrayWrapper + def processData(self, data): + return detrend(data) + + +class AdaptiveDetrend(CtrlNode): + """Removes baseline from data, ignoring anomalous events""" + nodeName = 'AdaptiveDetrend' + uiTemplate = [ + ('threshold', 'doubleSpin', {'value': 3.0, 'min': 0, 'max': 1000000}) + ] + + def processData(self, data): + return functions.adaptiveDetrend(data, threshold=self.ctrls['threshold'].value()) + +class HistogramDetrend(CtrlNode): + """Removes baseline from data by computing mode (from histogram) of beginning and end of data.""" + nodeName = 'HistogramDetrend' + uiTemplate = [ + ('windowSize', 'intSpin', {'value': 500, 'min': 10, 'max': 1000000}), + ('numBins', 'intSpin', {'value': 50, 'min': 3, 'max': 1000000}) + ] + + def processData(self, data): + ws = self.ctrls['windowSize'].value() + bn = self.ctrls['numBins'].value() + return functions.histogramDetrend(data, window=ws, bins=bn) + + +class ExpDeconvolve(CtrlNode): + """Exponential deconvolution filter.""" + nodeName = 'ExpDeconvolve' + uiTemplate = [ + ('tau', 'spin', {'value': 10e-3, 'step': 1, 'minStep': 100e-6, 'dec': True, 'range': [0.0, None], 'suffix': 's', 'siPrefix': True}) + ] + + def processData(self, data): + tau = self.ctrls['tau'].value() + return functions.expDeconvolve(data, tau) + #dt = 1 + #if isinstance(data, MetaArray): + #dt = data.xvals(0)[1] - data.xvals(0)[0] + #d = data[:-1] + (self.ctrls['tau'].value() / dt) * (data[1:] - data[:-1]) + #if isinstance(data, MetaArray): + #info = data.infoCopy() + #if 'values' in info[0]: + #info[0]['values'] = info[0]['values'][:-1] + #return MetaArray(d, info=info) + #else: + #return d + +class ExpReconvolve(CtrlNode): + """Exponential reconvolution filter. Only works with MetaArrays that were previously deconvolved.""" + nodeName = 'ExpReconvolve' + #uiTemplate = [ + #('tau', 'spin', {'value': 10e-3, 'step': 1, 'minStep': 100e-6, 'dec': True, 'range': [0.0, None], 'suffix': 's', 'siPrefix': True}) + #] + + def processData(self, data): + return functions.expReconvolve(data) + +class Tauiness(CtrlNode): + """Sliding-window exponential fit""" + nodeName = 'Tauiness' + uiTemplate = [ + ('window', 'intSpin', {'value': 100, 'min': 3, 'max': 1000000}), + ('skip', 'intSpin', {'value': 10, 'min': 0, 'max': 10000000}) + ] + + def processData(self, data): + return functions.tauiness(data, self.ctrls['window'].value(), self.ctrls['skip'].value()) + + + + \ No newline at end of file diff --git a/flowchart/library/Operators.py b/flowchart/library/Operators.py new file mode 100644 index 00000000..412af573 --- /dev/null +++ b/flowchart/library/Operators.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +from ..Node import Node + +class UniOpNode(Node): + """Generic node for performing any operation like Out = In.fn()""" + def __init__(self, name, fn): + self.fn = fn + Node.__init__(self, name, terminals={ + 'In': {'io': 'in'}, + 'Out': {'io': 'out', 'bypass': 'In'} + }) + + def process(self, **args): + return {'Out': getattr(args['In'], self.fn)()} + +class BinOpNode(Node): + """Generic node for performing any operation like A.fn(B)""" + def __init__(self, name, fn): + self.fn = fn + Node.__init__(self, name, terminals={ + 'A': {'io': 'in'}, + 'B': {'io': 'in'}, + 'Out': {'io': 'out', 'bypass': 'A'} + }) + + def process(self, **args): + fn = getattr(args['A'], self.fn) + out = fn(args['B']) + if out is NotImplemented: + raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B'])))) + #print " ", fn, out + return {'Out': out} + + +class AbsNode(UniOpNode): + """Returns abs(Inp). Does not check input types.""" + nodeName = 'Abs' + def __init__(self, name): + UniOpNode.__init__(self, name, '__abs__') + +class AddNode(BinOpNode): + """Returns A + B. Does not check input types.""" + nodeName = 'Add' + def __init__(self, name): + BinOpNode.__init__(self, name, '__add__') + +class SubtractNode(BinOpNode): + """Returns A - B. Does not check input types.""" + nodeName = 'Subtract' + def __init__(self, name): + BinOpNode.__init__(self, name, '__sub__') + +class MultiplyNode(BinOpNode): + """Returns A * B. Does not check input types.""" + nodeName = 'Multiply' + def __init__(self, name): + BinOpNode.__init__(self, name, '__mul__') + +class DivideNode(BinOpNode): + """Returns A / B. Does not check input types.""" + nodeName = 'Divide' + def __init__(self, name): + BinOpNode.__init__(self, name, '__div__') + diff --git a/flowchart/library/__init__.py b/flowchart/library/__init__.py new file mode 100644 index 00000000..58b5b810 --- /dev/null +++ b/flowchart/library/__init__.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from collections import OrderedDict +import os, types +from pyqtgraph.debug import printExc +from ..Node import Node +import reload + + +NODE_LIST = OrderedDict() ## maps name:class for all registered Node subclasses +NODE_TREE = OrderedDict() ## categorized tree of Node subclasses + +def getNodeType(name): + try: + return NODE_LIST[name] + except KeyError: + raise Exception("No node type called '%s'" % name) + +def getNodeTree(): + return NODE_TREE + +def registerNodeType(cls, paths, override=False): + """ + Register a new node type. If the type's name is already in use, + an exception will be raised (unless override=True). + + Arguments: + cls - a subclass of Node (must have typ.nodeName) + paths - list of tuples specifying the location(s) this + type will appear in the library tree. + override - if True, overwrite any class having the same name + """ + if not isNodeClass(cls): + raise Exception("Object %s is not a Node subclass" % str(cls)) + + name = cls.nodeName + if not override and name in NODE_LIST: + raise Exception("Node type name '%s' is already registered." % name) + + NODE_LIST[name] = cls + for path in paths: + root = NODE_TREE + for n in path: + if n not in root: + root[n] = OrderedDict() + root = root[n] + root[name] = cls + + + +def isNodeClass(cls): + try: + if not issubclass(cls, Node): + return False + except: + return False + return hasattr(cls, 'nodeName') + +def loadLibrary(reloadLibs=False, libPath=None): + """Import all Node subclasses found within files in the library module.""" + + global NODE_LIST, NODE_TREE + if libPath is None: + libPath = os.path.dirname(os.path.abspath(__file__)) + + if reloadLibs: + reload.reloadAll(libPath) + + for f in os.listdir(libPath): + pathName, ext = os.path.splitext(f) + if ext != '.py' or '__init__' in pathName: + continue + try: + #print "importing from", f + mod = __import__(pathName, globals(), locals()) + except: + printExc("Error loading flowchart library %s:" % pathName) + continue + + nodes = [] + for n in dir(mod): + o = getattr(mod, n) + if isNodeClass(o): + #print " ", str(o) + registerNodeType(o, [(pathName,)], override=reloadLibs) + #nodes.append((o.nodeName, o)) + #if len(nodes) > 0: + #NODE_TREE[name] = OrderedDict(nodes) + #NODE_LIST.extend(nodes) + #NODE_LIST = OrderedDict(NODE_LIST) + +def reloadLibrary(): + loadLibrary(reloadLibs=True) + +loadLibrary() +#NODE_LIST = [] +#for o in locals().values(): + #if type(o) is type(AddNode) and issubclass(o, Node) and o is not Node and hasattr(o, 'nodeName'): + #NODE_LIST.append((o.nodeName, o)) +#NODE_LIST.sort(lambda a,b: cmp(a[0], b[0])) +#NODE_LIST = OrderedDict(NODE_LIST) \ No newline at end of file diff --git a/flowchart/library/common.py b/flowchart/library/common.py new file mode 100644 index 00000000..ce7ff68f --- /dev/null +++ b/flowchart/library/common.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.widgets.SpinBox import SpinBox +#from pyqtgraph.SignalProxy import SignalProxy +from pyqtgraph.WidgetGroup import WidgetGroup +#from ColorMapper import ColorMapper +from ..Node import Node +import numpy as np +from pyqtgraph.widgets.ColorButton import ColorButton +try: + import metaarray + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + + +def generateUi(opts): + """Convenience function for generating common UI types""" + widget = QtGui.QWidget() + l = QtGui.QFormLayout() + l.setSpacing(0) + widget.setLayout(l) + ctrls = {} + row = 0 + for opt in opts: + if len(opt) == 2: + k, t = opt + o = {} + elif len(opt) == 3: + k, t, o = opt + else: + raise Exception("Widget specification must be (name, type) or (name, type, {opts})") + if t == 'intSpin': + w = QtGui.QSpinBox() + if 'max' in o: + w.setMaximum(o['max']) + if 'min' in o: + w.setMinimum(o['min']) + if 'value' in o: + w.setValue(o['value']) + elif t == 'doubleSpin': + w = QtGui.QDoubleSpinBox() + if 'max' in o: + w.setMaximum(o['max']) + if 'min' in o: + w.setMinimum(o['min']) + if 'value' in o: + w.setValue(o['value']) + elif t == 'spin': + w = SpinBox() + w.setOpts(**o) + elif t == 'check': + w = QtGui.QCheckBox() + if 'checked' in o: + w.setChecked(o['checked']) + elif t == 'combo': + w = QtGui.QComboBox() + for i in o['values']: + w.addItem(i) + #elif t == 'colormap': + #w = ColorMapper() + elif t == 'color': + w = ColorButton() + else: + raise Exception("Unknown widget type '%s'" % str(t)) + if 'tip' in o: + w.setToolTip(o['tip']) + w.setObjectName(k) + l.addRow(k, w) + if o.get('hidden', False): + w.hide() + label = l.labelForField(w) + label.hide() + + ctrls[k] = w + w.rowNum = row + row += 1 + group = WidgetGroup(widget) + return widget, group, ctrls + + +class CtrlNode(Node): + """Abstract class for nodes with auto-generated control UI""" + + sigStateChanged = QtCore.Signal(object) + + def __init__(self, name, ui=None, terminals=None): + if ui is None: + if hasattr(self, 'uiTemplate'): + ui = self.uiTemplate + else: + ui = [] + if terminals is None: + terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}} + Node.__init__(self, name=name, terminals=terminals) + + self.ui, self.stateGroup, self.ctrls = generateUi(ui) + self.stateGroup.sigChanged.connect(self.changed) + + def ctrlWidget(self): + return self.ui + + def changed(self): + self.update() + self.sigStateChanged.emit(self) + + def process(self, In, display=True): + out = self.processData(In) + return {'Out': out} + + def saveState(self): + state = Node.saveState(self) + state['ctrl'] = self.stateGroup.state() + return state + + def restoreState(self, state): + Node.restoreState(self, state) + if self.stateGroup is not None: + self.stateGroup.setState(state.get('ctrl', {})) + + def hideRow(self, name): + w = self.ctrls[name] + l = self.ui.layout().labelForField(w) + w.hide() + l.hide() + + def showRow(self, name): + w = self.ctrls[name] + l = self.ui.layout().labelForField(w) + w.show() + l.show() + + + +def metaArrayWrapper(fn): + def newFn(self, data, *args, **kargs): + if HAVE_METAARRAY and isinstance(data, metaarray.MetaArray): + d1 = fn(self, data.view(np.ndarray), *args, **kargs) + info = data.infoCopy() + if d1.shape != data.shape: + for i in range(data.ndim): + if 'values' in info[i]: + info[i]['values'] = info[i]['values'][:d1.shape[i]] + return metaarray.MetaArray(d1, info=info) + else: + return fn(self, data, *args, **kargs) + return newFn + diff --git a/functions.py b/functions.py index e5b6a41b..3a249d9c 100644 --- a/functions.py +++ b/functions.py @@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -colorAbbrev = { +Colors = { 'b': (0,0,255,255), 'g': (0,255,0,255), 'r': (255,0,0,255), @@ -14,86 +14,129 @@ colorAbbrev = { 'y': (255,255,0,255), 'k': (0,0,0,255), 'w': (255,255,255,255), -} +} + +SI_PREFIXES = u'yzafpnµm kMGTPEZY' +SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' + +USE_WEAVE = True -from PyQt4 import QtGui, QtCore +from Qt import QtGui, QtCore import numpy as np import scipy.ndimage +import decimal, re +import scipy.weave +import debug -## Copied from acq4/lib/util/functions -SI_PREFIXES = u'yzafpnµm kMGTPEZY' -def siScale(x, minVal=1e-25): - """Return the recommended scale factor and SI prefix string for x.""" +def siScale(x, minVal=1e-25, allowUnicode=True): + """ + Return the recommended scale factor and SI prefix string for x. + + Example:: + + siScale(0.0001) # returns (1e6, 'μ') + # This indicates that the number 0.0001 is best represented as 0.0001 * 1e6 = 100 μUnits + """ + + if isinstance(x, decimal.Decimal): + x = float(x) + + try: + if np.isnan(x) or np.isinf(x): + return(1, '') + except: + print x, type(x) + raise if abs(x) < minVal: m = 0 x = 0 else: m = int(np.clip(np.floor(np.log(abs(x))/np.log(1000)), -9.0, 9.0)) + if m == 0: pref = '' elif m < -8 or m > 8: pref = 'e%d' % (m*3) else: - pref = SI_PREFIXES[m+8] - p = .001**m - return (p, pref) - -def mkBrush(color): - if isinstance(color, QtGui.QBrush): - return color - return QtGui.QBrush(mkColor(color)) - -def mkPen(arg='default', color=None, width=1, style=None, cosmetic=True, hsv=None, ): - """Convenience function for making pens. Examples: - mkPen(color) - mkPen(color, width=2) - mkPen(cosmetic=False, width=4.5, color='r') - mkPen({'color': "FF0", width: 2}) - mkPen(None) (no pen) - """ - if isinstance(arg, dict): - return mkPen(**arg) - elif arg != 'default': - if isinstance(arg, QtGui.QPen): - return arg - elif arg is None: - style = QtCore.Qt.NoPen + if allowUnicode: + pref = SI_PREFIXES[m+8] else: - color = arg - - if color is None: - color = mkColor(200, 200, 200) - if hsv is not None: - color = hsvColor(*hsv) - else: - color = mkColor(color) - - pen = QtGui.QPen(QtGui.QBrush(color), width) - pen.setCosmetic(cosmetic) - if style is not None: - pen.setStyle(style) - return pen + pref = SI_PREFIXES_ASCII[m+8] + p = .001**m + + return (p, pref) -def hsvColor(h, s=1.0, v=1.0, a=1.0): - c = QtGui.QColor() - c.setHsvF(h, s, v, a) - return c +def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): + """ + Return the number x formatted in engineering notation with SI prefix. + + Example:: + + siFormat(0.0001, suffix='V') # returns "100 μV" + """ + + if space is True: + space = ' ' + if space is False: + space = '' + + + (p, pref) = siScale(x, minVal, allowUnicode) + if not (len(pref) > 0 and pref[0] == 'e'): + pref = space + pref + + if error is None: + fmt = "%." + str(precision) + "g%s%s" + return fmt % (x*p, pref, suffix) + else: + plusminus = space + u"±" + space + fmt = "%." + str(precision) + u"g%s%s%s%s" + return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) + +def siEval(s): + """ + Convert a value written in SI notation to its equivalent prefixless value + + Example:: + + siEval("100 μV") # returns 0.0001 + """ + + s = unicode(s) + m = re.match(r'(-?((\d+(\.\d*)?)|(\.\d+))([eE]-?\d+)?)\s*([u' + SI_PREFIXES + r']?)$', s) + if m is None: + raise Exception("Can't convert string '%s' to number." % s) + v = float(m.groups()[0]) + p = m.groups()[6] + #if p not in SI_PREFIXES: + #raise Exception("Can't convert string '%s' to number--unknown prefix." % s) + if p == '': + n = 0 + elif p == 'u': + n = -2 + else: + n = SI_PREFIXES.index(p) - 8 + return v * 1000**n + def mkColor(*args): - """make a QColor from a variety of argument types - accepted types are: - r, g, b, [a] - (r, g, b, [a]) - float (greyscale, 0.0-1.0) - int (uses intColor) - (int, hues) (uses intColor) - QColor - "c" (see colorAbbrev dictionary) - "RGB" (strings may optionally begin with "#") - "RGBA" - "RRGGBB" - "RRGGBBAA" + """ + Convenience function for constructing QColor from a variety of argument types. Accepted arguments are: + + ================ ================================================ + 'c' one of: r, g, b, c, m, y, k, w + R, G, B, [A] integers 0-255 + (R, G, B, [A]) tuple of integers 0-255 + float greyscale, 0.0-1.0 + int see :func:`intColor() ` + (int, hues) see :func:`intColor() ` + "RGB" hexadecimal strings; may begin with '#' + "RGBA" + "RRGGBB" + "RRGGBBAA" + QColor QColor instance; makes a copy. + ================ ================================================ """ err = 'Not sure how to make a color from "%s"' % str(args) if len(args) == 1: @@ -107,7 +150,7 @@ def mkColor(*args): if c[0] == '#': c = c[1:] if len(c) == 1: - (r, g, b, a) = colorAbbrev[c] + (r, g, b, a) = Colors[c] if len(c) == 3: r = int(c[0]*2, 16) g = int(c[1]*2, 16) @@ -149,9 +192,86 @@ def mkColor(*args): (r, g, b, a) = args else: raise Exception(err) - return QtGui.QColor(r, g, b, a) + + args = [r,g,b,a] + args = map(lambda a: 0 if np.isnan(a) or np.isinf(a) else a, args) + args = map(int, args) + return QtGui.QColor(*args) + + +def mkBrush(*args): + """ + | Convenience function for constructing Brush. + | This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() ` + | Calling mkBrush(None) returns an invisible brush. + """ + if len(args) == 1: + arg = args[0] + if arg is None: + return QtGui.QBrush(QtCore.Qt.NoBrush) + elif isinstance(arg, QtGui.QBrush): + return QtGui.QBrush(arg) + else: + color = arg + if len(args) > 1: + color = args + return QtGui.QBrush(mkColor(color)) + +def mkPen(*args, **kargs): + """ + Convenience function for constructing QPen. + + Examples:: + + mkPen(color) + mkPen(color, width=2) + mkPen(cosmetic=False, width=4.5, color='r') + mkPen({'color': "FF0", width: 2}) + mkPen(None) # (no pen) + + In these examples, *color* may be replaced with any arguments accepted by :func:`mkColor() ` """ + + color = kargs.get('color', None) + width = kargs.get('width', 1) + style = kargs.get('style', None) + cosmetic = kargs.get('cosmetic', True) + hsv = kargs.get('hsv', None) + + if len(args) == 1: + arg = args[0] + if isinstance(arg, dict): + return mkPen(**arg) + if isinstance(arg, QtGui.QPen): + return arg + elif arg is None: + style = QtCore.Qt.NoPen + else: + color = arg + if len(args) > 1: + color = args + + if color is None: + color = mkColor(200, 200, 200) + if hsv is not None: + color = hsvColor(*hsv) + else: + color = mkColor(color) + + pen = QtGui.QPen(QtGui.QBrush(color), width) + pen.setCosmetic(cosmetic) + if style is not None: + pen.setStyle(style) + return pen + +def hsvColor(h, s=1.0, v=1.0, a=1.0): + """Generate a QColor from HSVa values.""" + c = QtGui.QColor() + c.setHsvF(h, s, v, a) + return c + def colorTuple(c): + """Return a tuple (R,G,B,A) from a QColor""" return (c.red(), c.green(), c.blue(), c.alpha()) def colorStr(c): @@ -159,12 +279,13 @@ def colorStr(c): return ('%02x'*4) % colorTuple(c) def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): - """Creates a QColor from a single index. Useful for stepping through a predefined list of colors. - - The argument "index" determines which color from the set will be returned - - All other arguments determine what the set of predefined colors will be + """ + Creates a QColor from a single index. Useful for stepping through a predefined list of colors. + + The argument *index* determines which color from the set will be returned. All other arguments determine what the set of predefined colors will be - Colors are chosen by cycling across hues while varying the value (brightness). By default, there - are 9 hues and 3 values for a total of 27 different colors. """ + Colors are chosen by cycling across hues while varying the value (brightness). + By default, this selects from a list of 9 hues.""" hues = int(hues) values = int(values) ind = int(index) % (hues * values) @@ -183,27 +304,41 @@ def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, mi def affineSlice(data, shape, origin, vectors, axes, **kargs): - """Take an arbitrary slice through an array. - Parameters: - data: the original dataset - shape: the shape of the slice to take (Note the return value may have more dimensions than len(shape)) - origin: the location in the original dataset that will become the origin in the sliced data. - vectors: list of unit vectors which point in the direction of the slice axes - each vector must be the same length as axes - If the vectors are not unit length, the result will be scaled. - If the vectors are not orthogonal, the result will be sheared. - axes: the axes in the original dataset which correspond to the slice vectors + """ + Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. + + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. + + For a graphical interface to this function, see :func:`ROI.getArrayRegion` + + Arguments: + + | *data* (ndarray): the original dataset + | *shape*: the shape of the slice to take (Note the return value may have more dimensions than len(shape)) + | *origin*: the location in the original dataset that will become the origin in the sliced data. + | *vectors*: list of unit vectors which point in the direction of the slice axes - Example: start with a 4D data set, take a diagonal-planar slice out of the last 3 axes - - data = array with dims (time, x, y, z) = (100, 40, 40, 40) - - The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) - - The origin of the slice will be at (x,y,z) = (40, 0, 0) - - The we will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20) - affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) + * each vector must have the same length as *axes* + * If the vectors are not unit length, the result will be scaled. + * If the vectors are not orthogonal, the result will be sheared. - Note the following: - len(shape) == len(vectors) - len(origin) == len(axes) == len(vectors[0]) + *axes*: the axes in the original dataset which correspond to the slice *vectors* + + Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes + + * data = array with dims (time, x, y, z) = (100, 40, 40, 40) + * The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1) + * The origin of the slice will be at (x,y,z) = (40, 0, 0) + * We will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20) + + The call for this example would look like:: + + affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) + + Note the following must be true: + + | len(shape) == len(vectors) + | len(origin) == len(axes) == len(vectors[0]) """ # sanity check @@ -214,7 +349,8 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): for v in vectors: if len(v) != len(axes): raise Exception("each vector must be same length as axes.") - shape = (np.ceil(shape[0]), np.ceil(shape[1])) + + shape = map(np.ceil, shape) ## transpose data so slice axes come first trAx = range(data.ndim) @@ -257,3 +393,243 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): ## Untranspose array before returning return output.transpose(tr2) + + + + +def makeARGB(data, lut=None, levels=None): + """ + Convert a 2D or 3D array into an ARGB array suitable for building QImages + Will optionally do scaling and/or table lookups to determine final colors. + + Returns the ARGB array and a boolean indicating whether there is alpha channel data. + + Arguments: + data - 2D or 3D numpy array of int/float types + + For 2D arrays (x, y): + * The color will be determined using a lookup table (see argument 'lut'). + * If levels are given, the data is rescaled and converted to int + before using the lookup table. + + For 3D arrays (x, y, rgba): + * The third axis must have length 3 or 4 and will be interpreted as RGBA. + * The 'lut' argument is not allowed. + + lut - Lookup table for 2D data. May be 1D or 2D (N,rgba) and must have dtype=ubyte. + Values in data will be converted to color by indexing directly from lut. + Lookup tables can be built using GradientWidget. + levels - List [min, max]; optionally rescale data before converting through the + lookup table. rescaled = (data-min) * len(lut) / (max-min) + + """ + + prof = debug.Profiler('functions.makeARGB', disabled=True) + + ## sanity checks + if data.ndim == 3: + if data.shape[2] not in (3,4): + raise Exception("data.shape[2] must be 3 or 4") + #if lut is not None: + #raise Exception("can not use lookup table with 3D data") + elif data.ndim != 2: + raise Exception("data must be 2D or 3D") + + if lut is not None: + if lut.ndim == 2: + if lut.shape[1] not in (3,4): + raise Exception("lut.shape[1] must be 3 or 4") + elif lut.ndim != 1: + raise Exception("lut must be 1D or 2D") + if lut.dtype != np.ubyte: + raise Exception('lookup table must have dtype=ubyte (got %s instead)' % str(lut.dtype)) + + if levels is not None: + levels = np.array(levels) + if levels.shape == (2,): + pass + elif levels.shape in [(3,2), (4,2)]: + if data.ndim == 3: + raise Exception("Can not use 2D levels with 3D data.") + if lut is not None: + raise Exception('Can not use 2D levels and lookup table together.') + else: + raise Exception("Levels must have shape (2,) or (3,2) or (4,2)") + + prof.mark('1') + + if lut is not None: + lutLength = lut.shape[0] + else: + lutLength = 256 + + ## weave requires contiguous arrays + global USE_WEAVE + if (levels is not None or lut is not None) and USE_WEAVE: + data = np.ascontiguousarray(data) + + ## Apply levels if given + if levels is not None: + + try: ## use weave to speed up scaling + if not USE_WEAVE: + raise Exception('Weave is disabled; falling back to slower version.') + if levels.ndim == 1: + scale = float(lutLength) / (levels[1]-levels[0]) + offset = float(levels[0]) + data = rescaleData(data, scale, offset) + else: + if data.ndim == 2: + newData = np.empty(data.shape+(levels.shape[0],), dtype=np.uint32) + for i in xrange(levels.shape[0]): + scale = float(lutLength / (levels[i,1]-levels[i,0])) + offset = float(levels[i,0]) + newData[...,i] = rescaleData(data, scale, offset) + elif data.ndim == 3: + newData = np.empty(data.shape, dtype=np.uint32) + for i in xrange(data.shape[2]): + scale = float(lutLength / (levels[i,1]-levels[i,0])) + offset = float(levels[i,0]) + #print scale, offset, data.shape, newData.shape, levels.shape + newData[...,i] = rescaleData(data[...,i], scale, offset) + data = newData + except: + if USE_WEAVE: + debug.printExc("Error; disabling weave.") + USE_WEAVE = False + + if levels.ndim == 1: + if data.ndim == 2: + levels = levels[np.newaxis, np.newaxis, :] + else: + levels = levels[np.newaxis, np.newaxis, np.newaxis, :] + else: + levels = levels[np.newaxis, np.newaxis, ...] + if data.ndim == 2: + data = data[..., np.newaxis] + data = ((data-levels[...,0]) * lutLength) / (levels[...,1]-levels[...,0]) + + prof.mark('2') + + + ## apply LUT if given + if lut is not None and data.ndim == 2: + + if data.dtype.kind not in ('i', 'u'): + data = data.astype(int) + + data = np.clip(data, 0, lutLength-1) + try: + if not USE_WEAVE: + raise Exception('Weave is disabled; falling back to slower version.') + + newData = np.empty((data.size,) + lut.shape[1:], dtype=np.uint8) + flat = data.reshape(data.size) + size = data.size + ncol = lut.shape[1] + newStride = newData.strides[0] + newColStride = newData.strides[1] + lutStride = lut.strides[0] + lutColStride = lut.strides[1] + flatStride = flat.strides[0] / flat.dtype.itemsize + + #print "newData:", newData.shape, newData.dtype + #print "flat:", flat.shape, flat.dtype, flat.min(), flat.max() + #print "lut:", lut.shape, lut.dtype + #print "size:", size, "ncols:", ncol + #print "strides:", newStride, newColStride, lutStride, lutColStride, flatStride + + code = """ + + for( int i=0; iWif+Prn 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 b*`arbMu$qHgG2p8{Vz=oeR1U5LsS1B{mDK0 literal 0 HcmV?d00001 diff --git a/graphicsItems.py b/graphicsItems.py deleted file mode 100644 index 63de57e0..00000000 --- a/graphicsItems.py +++ /dev/null @@ -1,2997 +0,0 @@ -# -*- coding: utf-8 -*- -""" -graphicsItems.py - Defines several graphics item classes for use in Qt graphics/view framework -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. - -Provides ImageItem, PlotCurveItem, and ViewBox, amongst others. -""" - - -from PyQt4 import QtGui, QtCore -if not hasattr(QtCore, 'Signal'): - QtCore.Signal = QtCore.pyqtSignal -#from ObjectWorkaround import * -#tryWorkaround(QtCore, QtGui) -#from numpy import * -import numpy as np -try: - import scipy.weave as weave - from scipy.weave import converters -except: - pass -from scipy.fftpack import fft -#from scipy.signal import resample -import scipy.stats -#from metaarray import MetaArray -from Point import * -from functions import * -import types, sys, struct -import weakref -import debug -#from debug import * - -## QGraphicsObject didn't appear until 4.6; this is for compatibility with 4.5 -if not hasattr(QtGui, 'QGraphicsObject'): - class QGraphicsObject(QtGui.QGraphicsWidget): - def shape(self): - return QtGui.QGraphicsItem.shape(self) - QtGui.QGraphicsObject = QGraphicsObject - - -## Should probably just use QGraphicsGroupItem and instruct it to pass events on to children.. -class ItemGroup(QtGui.QGraphicsItem): - def __init__(self, *args): - QtGui.QGraphicsItem.__init__(self, *args) - if hasattr(self, "ItemHasNoContents"): - self.setFlag(self.ItemHasNoContents) - - def boundingRect(self): - return QtCore.QRectF() - - def paint(self, *args): - pass - - def addItem(self, item): - item.setParentItem(self) - - -#if hasattr(QtGui, "QGraphicsObject"): - #QGraphicsObject = QtGui.QGraphicsObject -#else: - #class QObjectWorkaround: - #def __init__(self): - #self._qObj_ = QtCore.QObject() - #def connect(self, *args): - #return QtCore.QObject.connect(self._qObj_, *args) - #def disconnect(self, *args): - #return QtCore.QObject.disconnect(self._qObj_, *args) - #def emit(self, *args): - #return QtCore.QObject.emit(self._qObj_, *args) - - #class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround): - #def __init__(self, *args): - #QtGui.QGraphicsItem.__init__(self, *args) - #QObjectWorkaround.__init__(self) - - - -class GraphicsObject(QtGui.QGraphicsObject): - """Extends QGraphicsObject with a few important functions. - (Most of these assume that the object is in a scene with a single view)""" - - def __init__(self, *args): - QtGui.QGraphicsObject.__init__(self, *args) - self._view = None - - def getViewWidget(self): - """Return the view widget for this item. If the scene has multiple views, only the first view is returned. - the view is remembered for the lifetime of the object, so expect trouble if the object is moved to another view.""" - if self._view is None: - scene = self.scene() - if scene is None: - return None - views = scene.views() - if len(views) < 1: - return None - self._view = weakref.ref(self.scene().views()[0]) - return self._view() - - def getBoundingParents(self): - """Return a list of parents to this item that have child clipping enabled.""" - p = self - parents = [] - while True: - p = p.parentItem() - if p is None: - break - if p.flags() & self.ItemClipsChildrenToShape: - parents.append(p) - return parents - - def viewBounds(self): - """Return the allowed visible boundaries for this item. Takes into account the viewport as well as any parents that clip.""" - bounds = QtCore.QRectF(0, 0, 1, 1) - view = self.getViewWidget() - if view is None: - return None - bounds = self.mapRectFromScene(view.visibleRange()) - - for p in self.getBoundingParents(): - bounds &= self.mapRectFromScene(p.sceneBoundingRect()) - - return bounds - - def viewTransform(self): - """Return the transform that maps from local coordinates to the item's view coordinates""" - view = self.getViewWidget() - if view is None: - return None - return self.deviceTransform(view.viewportTransform()) - - def pixelVectors(self): - """Return vectors in local coordinates representing the width and height of a view pixel.""" - vt = self.viewTransform() - if vt is None: - return None - vt = vt.inverted()[0] - orig = vt.map(QtCore.QPointF(0, 0)) - return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig - - def pixelWidth(self): - vt = self.viewTransform() - if vt is None: - return 0 - vt = vt.inverted()[0] - return abs((vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).x()) - - def pixelHeight(self): - vt = self.viewTransform() - if vt is None: - return 0 - vt = vt.inverted()[0] - return abs((vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).y()) - - def mapToView(self, obj): - vt = self.viewTransform() - if vt is None: - return None - return vt.map(obj) - - def mapRectToView(self, obj): - vt = self.viewTransform() - if vt is None: - return None - return vt.mapRect(obj) - - def mapFromView(self, obj): - vt = self.viewTransform() - if vt is None: - return None - vt = vt.inverted()[0] - return vt.map(obj) - - def mapRectFromView(self, obj): - vt = self.viewTransform() - if vt is None: - return None - vt = vt.inverted()[0] - return vt.mapRect(obj) - - - - - -class ImageItem(QtGui.QGraphicsObject): - - sigImageChanged = QtCore.Signal() - - if 'linux' not in sys.platform: ## disable weave optimization on linux--broken there. - useWeave = True - else: - useWeave = False - - def __init__(self, image=None, copy=True, parent=None, border=None, mode=None, *args): - #QObjectWorkaround.__init__(self) - QtGui.QGraphicsObject.__init__(self) - #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) - self.qimage = QtGui.QImage() - self.pixmap = None - self.paintMode = mode - #self.useWeave = True - self.blackLevel = None - self.whiteLevel = None - self.alpha = 1.0 - self.image = None - self.clipLevel = None - self.drawKernel = None - if border is not None: - border = mkPen(border) - self.border = border - - #QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) - #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) - if image is not None: - self.updateImage(image, copy, autoRange=True) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - def setCompositionMode(self, mode): - self.paintMode = mode - self.update() - - def setAlpha(self, alpha): - self.alpha = alpha - self.updateImage() - - #def boundingRect(self): - #return self.pixmapItem.boundingRect() - #return QtCore.QRectF(0, 0, self.qimage.width(), self.qimage.height()) - - def width(self): - if self.pixmap is None: - return None - return self.pixmap.width() - - def height(self): - if self.pixmap is None: - return None - return self.pixmap.height() - - def boundingRect(self): - if self.pixmap is None: - return QtCore.QRectF(0., 0., 0., 0.) - return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) - - def setClipLevel(self, level=None): - self.clipLevel = level - - #def paint(self, p, opt, widget): - #pass - #if self.pixmap is not None: - #p.drawPixmap(0, 0, self.pixmap) - #print "paint" - - def setLevels(self, white=None, black=None): - if white is not None: - self.whiteLevel = white - if black is not None: - self.blackLevel = black - self.updateImage() - - def getLevels(self): - return self.whiteLevel, self.blackLevel - - def updateImage(self, image=None, copy=True, autoRange=False, clipMask=None, white=None, black=None, axes=None): - prof = debug.Profiler('ImageItem.updateImage 0x%x' %id(self), disabled=True) - #debug.printTrace() - if axes is None: - axh = {'x': 0, 'y': 1, 'c': 2} - else: - axh = axes - #print "Update image", black, white - if white is not None: - self.whiteLevel = white - if black is not None: - self.blackLevel = black - - gotNewData = False - if image is None: - if self.image is None: - return - else: - gotNewData = True - if self.image is None or image.shape != self.image.shape: - self.prepareGeometryChange() - if copy: - self.image = image.view(np.ndarray).copy() - else: - self.image = image.view(np.ndarray) - #print " image max:", self.image.max(), "min:", self.image.min() - prof.mark('1') - - # Determine scale factors - if autoRange or self.blackLevel is None: - if self.image.dtype is np.ubyte: - self.blackLevel = 0 - self.whiteLevel = 255 - else: - self.blackLevel = self.image.min() - self.whiteLevel = self.image.max() - #print "Image item using", self.blackLevel, self.whiteLevel - - if self.blackLevel != self.whiteLevel: - scale = 255. / (self.whiteLevel - self.blackLevel) - else: - scale = 0. - - prof.mark('2') - - ## Recolor and convert to 8 bit per channel - # Try using weave, then fall back to python - shape = self.image.shape - black = float(self.blackLevel) - white = float(self.whiteLevel) - - if black == 0 and white == 255 and self.image.dtype == np.ubyte: - im = self.image - - else: - try: - if not ImageItem.useWeave: - raise Exception('Skipping weave compile') - sim = np.ascontiguousarray(self.image) - sim.shape = sim.size - im = np.empty(sim.shape, dtype=np.ubyte) - n = im.size - - code = """ - for( int i=0; i 255.0 ) - a = 255.0; - else if( a < 0.0 ) - a = 0.0; - im(i) = a; - } - """ - - weave.inline(code, ['sim', 'im', 'n', 'black', 'scale'], type_converters=converters.blitz, compiler = 'gcc') - sim.shape = shape - im.shape = shape - except: - if ImageItem.useWeave: - ImageItem.useWeave = False - #sys.excepthook(*sys.exc_info()) - #print "==============================================================================" - print "Weave compile failed, falling back to slower version." - self.image.shape = shape - im = ((self.image - black) * scale).clip(0.,255.).astype(np.ubyte) - prof.mark('3') - - try: - im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte) - except: - print im.shape, axh - raise - alpha = np.clip(int(255 * self.alpha), 0, 255) - prof.mark('4') - # Fill image - if im.ndim == 2: - im2 = im.transpose(axh['y'], axh['x']) - im1[..., 0] = im2 - im1[..., 1] = im2 - im1[..., 2] = im2 - im1[..., 3] = alpha - elif im.ndim == 3: #color image - im2 = im.transpose(axh['y'], axh['x'], axh['c']) - ## [B G R A] Reorder colors - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - - for i in range(0, im.shape[axh['c']]): - im1[..., order[i]] = im2[..., i] - - ## fill in unused channels with 0 or alpha - for i in range(im.shape[axh['c']], 3): - im1[..., i] = 0 - if im.shape[axh['c']] < 4: - im1[..., 3] = alpha - - else: - raise Exception("Image must be 2 or 3 dimensions") - #self.im1 = im1 - # Display image - prof.mark('5') - if self.clipLevel is not None or clipMask is not None: - if clipMask is not None: - mask = clipMask.transpose() - else: - mask = (self.image < self.clipLevel).transpose() - im1[..., 0][mask] *= 0.5 - im1[..., 1][mask] *= 0.5 - im1[..., 2][mask] = 255 - prof.mark('6') - #print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape - self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :( - prof.mark('7') - qimage = QtGui.QImage(buffer(self.ims), im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) - prof.mark('8') - self.pixmap = QtGui.QPixmap.fromImage(qimage) - prof.mark('9') - ##del self.ims - #self.pixmapItem.setPixmap(self.pixmap) - - self.update() - prof.mark('10') - - if gotNewData: - #self.emit(QtCore.SIGNAL('imageChanged')) - self.sigImageChanged.emit() - - prof.finish() - - def getPixmap(self): - return self.pixmap.copy() - - def getHistogram(self, bins=500, step=3): - """returns x and y arrays containing the histogram values for the current image. - The step argument causes pixels to be skipped when computing the histogram to save time.""" - stepData = self.image[::step, ::step] - hist = np.histogram(stepData, bins=bins) - return hist[1][:-1], hist[0] - - def mousePressEvent(self, ev): - if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: - self.drawAt(ev.pos(), ev) - ev.accept() - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - #print "mouse move", ev.pos() - if self.drawKernel is not None: - self.drawAt(ev.pos(), ev) - - def mouseReleaseEvent(self, ev): - pass - - def tabletEvent(self, ev): - print ev.device() - print ev.pointerType() - print ev.pressure() - - def drawAt(self, pos, ev=None): - pos = [int(pos.x()), int(pos.y())] - dk = self.drawKernel - kc = self.drawKernelCenter - sx = [0,dk.shape[0]] - sy = [0,dk.shape[1]] - tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] - ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] - - for i in [0,1]: - dx1 = -min(0, tx[i]) - dx2 = min(0, self.image.shape[0]-tx[i]) - tx[i] += dx1+dx2 - sx[i] += dx1+dx2 - - dy1 = -min(0, ty[i]) - dy2 = min(0, self.image.shape[1]-ty[i]) - ty[i] += dy1+dy2 - sy[i] += dy1+dy2 - - #print sx - #print sy - #print tx - #print ty - #print self.image.shape - #print self.image[tx[0]:tx[1], ty[0]:ty[1]].shape - #print dk[sx[0]:sx[1], sy[0]:sy[1]].shape - ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1])) - ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) - #src = dk[sx[0]:sx[1], sy[0]:sy[1]] - #mask = self.drawMask[sx[0]:sx[1], sy[0]:sy[1]] - mask = self.drawMask - src = dk - #print self.image[ts].shape, src.shape - - if callable(self.drawMode): - self.drawMode(dk, self.image, mask, ss, ts, ev) - else: - mask = mask[ss] - src = src[ss] - if self.drawMode == 'set': - if mask is not None: - self.image[ts] = self.image[ts] * (1-mask) + src * mask - else: - self.image[ts] = src - elif self.drawMode == 'add': - self.image[ts] += src - else: - raise Exception("Unknown draw mode '%s'" % self.drawMode) - self.updateImage() - - def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): - self.drawKernel = kernel - self.drawKernelCenter = center - self.drawMode = mode - self.drawMask = mask - - def paint(self, p, *args): - - #QtGui.QGraphicsPixmapItem.paint(self, p, *args) - if self.pixmap is None: - return - if self.paintMode is not None: - p.setCompositionMode(self.paintMode) - p.drawPixmap(self.boundingRect(), self.pixmap, QtCore.QRectF(0, 0, self.pixmap.width(), self.pixmap.height())) - if self.border is not None: - p.setPen(self.border) - p.drawRect(self.boundingRect()) - - def pixelSize(self): - """return size of a single pixel in the image""" - br = self.sceneBoundingRect() - return br.width()/self.pixmap.width(), br.height()/self.pixmap.height() - -class PlotCurveItem(GraphicsObject): - - sigPlotChanged = QtCore.Signal(object) - - """Class representing a single plot curve.""" - - sigClicked = QtCore.Signal(object) - - def __init__(self, y=None, x=None, copy=False, pen=None, shadow=None, parent=None, color=None, clickable=False): - GraphicsObject.__init__(self, parent) - #GraphicsWidget.__init__(self, parent) - self.free() - #self.dispPath = None - - if pen is None: - if color is None: - self.setPen((200,200,200)) - else: - self.setPen(color) - else: - self.setPen(pen) - - self.shadow = shadow - if y is not None: - self.updateData(y, x, copy) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - self.metaData = {} - self.opts = { - 'spectrumMode': False, - 'logMode': [False, False], - 'pointMode': False, - 'pointStyle': None, - 'downsample': False, - 'alphaHint': 1.0, - 'alphaMode': False - } - - self.setClickable(clickable) - #self.fps = None - - def setClickable(self, s): - self.clickable = s - - - def getData(self): - if self.xData is None: - return (None, None) - if self.xDisp is None: - nanMask = np.isnan(self.xData) | np.isnan(self.yData) - if any(nanMask): - x = self.xData[~nanMask] - y = self.yData[~nanMask] - else: - x = self.xData - y = self.yData - ds = self.opts['downsample'] - if ds > 1: - x = x[::ds] - #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing - y = y[::ds] - if self.opts['spectrumMode']: - f = fft(y) / len(y) - y = abs(f[1:len(f)/2]) - dt = x[-1] - x[0] - x = np.linspace(0, 0.5*len(x)/dt, len(y)) - if self.opts['logMode'][0]: - x = np.log10(x) - if self.opts['logMode'][1]: - y = np.log10(y) - self.xDisp = x - self.yDisp = y - #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() - #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() - return self.xDisp, self.yDisp - - #def generateSpecData(self): - #f = fft(self.yData) / len(self.yData) - #self.ySpec = abs(f[1:len(f)/2]) - #dt = self.xData[-1] - self.xData[0] - #self.xSpec = linspace(0, 0.5*len(self.xData)/dt, len(self.ySpec)) - - def getRange(self, ax, frac=1.0): - #print "getRange", ax, frac - (x, y) = self.getData() - if x is None or len(x) == 0: - return (0, 1) - - if ax == 0: - d = x - elif ax == 1: - d = y - - if frac >= 1.0: - return (d.min(), d.max()) - elif frac <= 0.0: - raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) - else: - return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - #bins = 1000 - #h = histogram(d, bins) - #s = len(d) * (1.0-frac) - #mnTot = mxTot = 0 - #mnInd = mxInd = 0 - #for i in range(bins): - #mnTot += h[0][i] - #if mnTot > s: - #mnInd = i - #break - #for i in range(bins): - #mxTot += h[0][-i-1] - #if mxTot > s: - #mxInd = -i-1 - #break - ##print mnInd, mxInd, h[1][mnInd], h[1][mxInd] - #return(h[1][mnInd], h[1][mxInd]) - - - - - def setMeta(self, data): - self.metaData = data - - def meta(self): - return self.metaData - - def setPen(self, pen): - self.pen = mkPen(pen) - self.update() - - def setColor(self, color): - self.pen.setColor(color) - self.update() - - def setAlpha(self, alpha, auto): - self.opts['alphaHint'] = alpha - self.opts['alphaMode'] = auto - self.update() - - def setSpectrumMode(self, mode): - self.opts['spectrumMode'] = mode - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setLogMode(self, mode): - self.opts['logMode'] = mode - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setPointMode(self, mode): - self.opts['pointMode'] = mode - self.update() - - def setShadowPen(self, pen): - self.shadow = pen - self.update() - - def setDownsampling(self, ds): - if self.opts['downsample'] != ds: - self.opts['downsample'] = ds - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setData(self, x, y, copy=False): - """For Qwt compatibility""" - self.updateData(y, x, copy) - - def updateData(self, data, x=None, copy=False): - prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) - if isinstance(data, list): - data = np.array(data) - if isinstance(x, list): - x = np.array(x) - if not isinstance(data, np.ndarray) or data.ndim > 2: - raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape)) - if x == None: - if 'complex' in str(data.dtype): - raise Exception("Can not plot complex data types.") - else: - if 'complex' in str(data.dtype)+str(x.dtype): - raise Exception("Can not plot complex data types.") - - if data.ndim == 2: ### If data is 2D array, then assume x and y values are in first two columns or rows. - if x is not None: - raise Exception("Plot data may be 2D only if no x argument is supplied.") - ax = 0 - if data.shape[0] > 2 and data.shape[1] == 2: - ax = 1 - ind = [slice(None), slice(None)] - ind[ax] = 0 - y = data[tuple(ind)] - ind[ax] = 1 - x = data[tuple(ind)] - elif data.ndim == 1: - y = data - prof.mark("data checks") - - self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly - ## Test this bug with test_PlotWidget and zoom in on the animated plot - - self.prepareGeometryChange() - if copy: - self.yData = y.copy() - else: - self.yData = y - - if copy and x is not None: - self.xData = x.copy() - else: - self.xData = x - prof.mark('copy') - - if x is None: - self.xData = np.arange(0, self.yData.shape[0]) - - if self.xData.shape != self.yData.shape: - raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) - - self.path = None - self.xDisp = self.yDisp = None - - prof.mark('set') - self.update() - prof.mark('update') - #self.emit(QtCore.SIGNAL('plotChanged'), self) - self.sigPlotChanged.emit(self) - prof.mark('emit') - #prof.finish() - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - prof.mark('set cache mode') - prof.finish() - - def generatePath(self, x, y): - prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) - path = QtGui.QPainterPath() - - ## Create all vertices in path. The method used below creates a binary format so that all - ## vertices can be read in at once. This binary format may change in future versions of Qt, - ## so the original (slower) method is left here for emergencies: - #path.moveTo(x[0], y[0]) - #for i in range(1, y.shape[0]): - # path.lineTo(x[i], y[i]) - - ## Speed this up using >> operator - ## Format is: - ## numVerts(i4) 0(i4) - ## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect - ## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex - ## ... - ## 0(i4) - ## - ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') - - n = x.shape[0] - # create empty array, pad with extra space on either end - arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')]) - # write first two integers - prof.mark('allocate empty') - arr.data[12:20] = struct.pack('>ii', n, 0) - prof.mark('pack header') - # Fill array with vertex values - arr[1:-1]['x'] = x - arr[1:-1]['y'] = y - arr[1:-1]['c'] = 1 - prof.mark('fill array') - # write last 0 - lastInd = 20*(n+1) - arr.data[lastInd:lastInd+4] = struct.pack('>i', 0) - prof.mark('footer') - # create datastream object and stream into path - buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here - prof.mark('create buffer') - ds = QtCore.QDataStream(buf) - prof.mark('create datastream') - ds >> path - prof.mark('load') - - prof.finish() - return path - - def boundingRect(self): - (x, y) = self.getData() - if x is None or y is None or len(x) == 0 or len(y) == 0: - return QtCore.QRectF() - - - if self.shadow is not None: - lineWidth = (max(self.pen.width(), self.shadow.width()) + 1) - else: - lineWidth = (self.pen.width()+1) - - - pixels = self.pixelVectors() - xmin = x.min() - pixels[0].x() * lineWidth - xmax = x.max() + pixels[0].x() * lineWidth - ymin = y.min() - abs(pixels[1].y()) * lineWidth - ymax = y.max() + abs(pixels[1].y()) * lineWidth - - - return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) - - def paint(self, p, opt, widget): - prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) - if self.xData is None: - return - #if self.opts['spectrumMode']: - #if self.specPath is None: - - #self.specPath = self.generatePath(*self.getData()) - #path = self.specPath - #else: - if self.path is None: - self.path = self.generatePath(*self.getData()) - path = self.path - prof.mark('generate path') - - if self.shadow is not None: - sp = QtGui.QPen(self.shadow) - else: - sp = None - - ## Copy pens and apply alpha adjustment - cp = QtGui.QPen(self.pen) - for pen in [sp, cp]: - if pen is None: - continue - c = pen.color() - c.setAlpha(c.alpha() * self.opts['alphaHint']) - pen.setColor(c) - #pen.setCosmetic(True) - - if self.shadow is not None: - p.setPen(sp) - p.drawPath(path) - p.setPen(cp) - p.drawPath(path) - prof.mark('drawPath') - - prof.finish() - #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) - #p.drawRect(self.boundingRect()) - - - def free(self): - self.xData = None ## raw values - self.yData = None - self.xDisp = None ## display values (after log / fft) - self.yDisp = None - self.path = None - #del self.xData, self.yData, self.xDisp, self.yDisp, self.path - - def mousePressEvent(self, ev): - #GraphicsObject.mousePressEvent(self, ev) - if not self.clickable: - ev.ignore() - if ev.button() != QtCore.Qt.LeftButton: - ev.ignore() - self.mousePressPos = ev.pos() - self.mouseMoved = False - - def mouseMoveEvent(self, ev): - #GraphicsObject.mouseMoveEvent(self, ev) - self.mouseMoved = True - #print "move" - - def mouseReleaseEvent(self, ev): - #GraphicsObject.mouseReleaseEvent(self, ev) - if not self.mouseMoved: - self.sigClicked.emit(self) - - -class CurvePoint(QtGui.QGraphicsObject): - """A GraphicsItem that sets its location to a point on a PlotCurveItem. - The position along the curve is a property, and thus can be easily animated.""" - - def __init__(self, curve, index=0, pos=None): - """Position can be set either as an index referring to the sample number or - the position 0.0 - 1.0""" - - QtGui.QGraphicsObject.__init__(self) - #QObjectWorkaround.__init__(self) - self.curve = weakref.ref(curve) - self.setParentItem(curve) - self.setProperty('position', 0.0) - self.setProperty('index', 0) - - if hasattr(self, 'ItemHasNoContents'): - self.setFlags(self.flags() | self.ItemHasNoContents) - - if pos is not None: - self.setPos(pos) - else: - self.setIndex(index) - - def setPos(self, pos): - self.setProperty('position', float(pos))## cannot use numpy types here, MUST be python float. - - def setIndex(self, index): - self.setProperty('index', int(index)) ## cannot use numpy types here, MUST be python int. - - def event(self, ev): - if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve() is None: - return False - - if ev.propertyName() == 'index': - index = self.property('index').toInt()[0] - elif ev.propertyName() == 'position': - index = None - else: - return False - - (x, y) = self.curve().getData() - if index is None: - #print ev.propertyName(), self.property('position').toDouble()[0], self.property('position').typeName() - index = (len(x)-1) * clip(self.property('position').toDouble()[0], 0.0, 1.0) - - if index != int(index): ## interpolate floating-point values - i1 = int(index) - i2 = clip(i1+1, 0, len(x)-1) - s2 = index-i1 - s1 = 1.0-s2 - newPos = (x[i1]*s1+x[i2]*s2, y[i1]*s1+y[i2]*s2) - else: - index = int(index) - i1 = clip(index-1, 0, len(x)-1) - i2 = clip(index+1, 0, len(x)-1) - newPos = (x[index], y[index]) - - p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1])) - p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2])) - ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) ## returns radians - self.resetTransform() - self.rotate(180+ ang * 180 / np.pi) ## takes degrees - QtGui.QGraphicsItem.setPos(self, *newPos) - return True - - def boundingRect(self): - return QtCore.QRectF() - - def paint(self, *args): - pass - - def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1): - anim = QtCore.QPropertyAnimation(self, prop) - anim.setDuration(duration) - anim.setStartValue(start) - anim.setEndValue(end) - anim.setLoopCount(loop) - return anim - - - -class ArrowItem(QtGui.QGraphicsPolygonItem): - def __init__(self, **opts): - QtGui.QGraphicsPolygonItem.__init__(self) - defOpts = { - 'style': 'tri', - 'pxMode': True, - 'size': 20, - 'angle': -150, - 'pos': (0,0), - 'width': 8, - 'tipAngle': 25, - 'baseAngle': 90, - 'pen': (200,200,200), - 'brush': (50,50,200), - } - defOpts.update(opts) - - self.setStyle(**defOpts) - - self.setPen(mkPen(defOpts['pen'])) - self.setBrush(mkBrush(defOpts['brush'])) - - self.rotate(self.opts['angle']) - self.moveBy(*self.opts['pos']) - - def setStyle(self, **opts): - self.opts = opts - - if opts['style'] == 'tri': - points = [ - QtCore.QPointF(0,0), - QtCore.QPointF(opts['size'],-opts['width']/2.), - QtCore.QPointF(opts['size'],opts['width']/2.), - ] - poly = QtGui.QPolygonF(points) - - else: - raise Exception("Unrecognized arrow style '%s'" % opts['style']) - - self.setPolygon(poly) - - if opts['pxMode']: - self.setFlags(self.flags() | self.ItemIgnoresTransformations) - else: - self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) - - def paint(self, p, *args): - p.setRenderHint(QtGui.QPainter.Antialiasing) - QtGui.QGraphicsPolygonItem.paint(self, p, *args) - -class CurveArrow(CurvePoint): - """Provides an arrow that points to any specific sample on a PlotCurveItem. - Provides properties that can be animated.""" - - def __init__(self, curve, index=0, pos=None, **opts): - CurvePoint.__init__(self, curve, index=index, pos=pos) - if opts.get('pxMode', True): - opts['pxMode'] = False - self.setFlags(self.flags() | self.ItemIgnoresTransformations) - opts['angle'] = 0 - self.arrow = ArrowItem(**opts) - self.arrow.setParentItem(self) - - def setStyle(**opts): - return self.arrow.setStyle(**opts) - - - -class ScatterPlotItem(GraphicsObject): - - #sigPointClicked = QtCore.Signal(object, object) - sigClicked = QtCore.Signal(object, object) ## self, points - - def __init__(self, spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=5, identical=False, data=None): - """ - Arguments: - spots: list of dicts. Each dict specifies parameters for a single spot. - x,y: array of x,y values. Alternatively, specify spots['pos'] = (x,y) - pxMode: If True, spots are always the same size regardless of scaling - identical: If True, all spots are forced to look identical. - This can result in performance enhancement.""" - GraphicsObject.__init__(self) - self.spots = [] - self.range = [[0,0], [0,0]] - self.identical = identical - self._spotPixmap = None - - if brush == 'default': - self.brush = QtGui.QBrush(QtGui.QColor(100, 100, 150)) - else: - self.brush = mkBrush(brush) - - if pen == 'default': - self.pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) - else: - self.pen = mkPen(pen) - - self.size = size - - self.pxMode = pxMode - if spots is not None or x is not None: - self.setPoints(spots, x, y, data) - - #self.optimize = optimize - #if optimize: - #self.spotImage = QtGui.QImage(size, size, QtGui.QImage.Format_ARGB32_Premultiplied) - #self.spotImage.fill(0) - #p = QtGui.QPainter(self.spotImage) - #p.setRenderHint(p.Antialiasing) - #p.setBrush(brush) - #p.setPen(pen) - #p.drawEllipse(0, 0, size, size) - #p.end() - #self.optimizePixmap = QtGui.QPixmap(self.spotImage) - #self.optimizeFragments = [] - #self.setFlags(self.flags() | self.ItemIgnoresTransformations) - - def setPxMode(self, mode): - self.pxMode = mode - - def clear(self): - for i in self.spots: - i.setParentItem(None) - s = i.scene() - if s is not None: - s.removeItem(i) - self.spots = [] - - - def getRange(self, ax, percent): - return self.range[ax] - - def setPoints(self, spots=None, x=None, y=None, data=None): - self.clear() - self.range = [[0,0],[0,0]] - self.addPoints(spots, x, y, data) - - def addPoints(self, spots=None, x=None, y=None, data=None): - xmn = ymn = xmx = ymx = None - if spots is not None: - n = len(spots) - else: - n = len(x) - - for i in range(n): - if spots is not None: - s = spots[i] - pos = Point(s['pos']) - else: - s = {} - pos = Point(x[i], y[i]) - if data is not None: - s['data'] = data[i] - - size = s.get('size', self.size) - if self.pxMode: - psize = 0 - else: - psize = size - if xmn is None: - xmn = pos[0]-psize - xmx = pos[0]+psize - ymn = pos[1]-psize - ymx = pos[1]+psize - else: - xmn = min(xmn, pos[0]-psize) - xmx = max(xmx, pos[0]+psize) - ymn = min(ymn, pos[1]-psize) - ymx = max(ymx, pos[1]+psize) - #print pos, xmn, xmx, ymn, ymx - brush = s.get('brush', self.brush) - pen = s.get('pen', self.pen) - pen.setCosmetic(True) - data2 = s.get('data', None) - item = self.mkSpot(pos, size, self.pxMode, brush, pen, data2, index=len(self.spots)) - self.spots.append(item) - #if self.optimize: - #item.hide() - #frag = QtGui.QPainter.PixmapFragment.create(pos, QtCore.QRectF(0, 0, size, size)) - #self.optimizeFragments.append(frag) - self.range = [[xmn, xmx], [ymn, ymx]] - - #def paint(self, p, *args): - #if not self.optimize: - #return - ##p.setClipRegion(self.boundingRect()) - #p.drawPixmapFragments(self.optimizeFragments, self.optimizePixmap) - - def paint(self, *args): - pass - - def spotPixmap(self): - if not self.identical: - return None - if self._spotPixmap is None: - self._spotPixmap = PixmapSpotItem.makeSpotImage(self.size, self.pen, self.brush) - return self._spotPixmap - - def mkSpot(self, pos, size, pxMode, brush, pen, data, index=None): - if pxMode: - img = self.spotPixmap() - item = PixmapSpotItem(size, brush, pen, data, image=img, index=index) - else: - item = SpotItem(size, pxMode, brush, pen, data, index=index) - item.setParentItem(self) - item.setPos(pos) - #item.sigClicked.connect(self.pointClicked) - return item - - def boundingRect(self): - ((xmn, xmx), (ymn, ymx)) = self.range - if xmn is None or xmx is None or ymn is None or ymx is None: - return QtCore.QRectF() - return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) - return QtCore.QRectF(xmn-1, ymn-1, xmx-xmn+2, ymx-ymn+2) - - #def pointClicked(self, point): - #self.sigPointClicked.emit(self, point) - - def points(self): - return self.spots[:] - - def pointsAt(self, pos): - x = pos.x() - y = pos.y() - pw = self.pixelWidth() - ph = self.pixelHeight() - pts = [] - for s in self.spots: - sp = s.pos() - ss = s.size - sx = sp.x() - sy = sp.y() - s2x = s2y = ss * 0.5 - if self.pxMode: - s2x *= pw - s2y *= ph - if x > sx-s2x and x < sx+s2x and y > sy-s2y and y < sy+s2y: - pts.append(s) - #print "HIT:", x, y, sx, sy, s2x, s2y - #else: - #print "No hit:", (x, y), (sx, sy) - #print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y) - pts.sort(lambda a,b: cmp(b.zValue(), a.zValue())) - return pts - - - def mousePressEvent(self, ev): - QtGui.QGraphicsItem.mousePressEvent(self, ev) - if ev.button() == QtCore.Qt.LeftButton: - pts = self.pointsAt(ev.pos()) - if len(pts) > 0: - self.mouseMoved = False - self.ptsClicked = pts - ev.accept() - else: - #print "no spots" - ev.ignore() - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - QtGui.QGraphicsItem.mouseMoveEvent(self, ev) - self.mouseMoved = True - pass - - def mouseReleaseEvent(self, ev): - QtGui.QGraphicsItem.mouseReleaseEvent(self, ev) - if not self.mouseMoved: - self.sigClicked.emit(self, self.ptsClicked) - - -class SpotItem(QtGui.QGraphicsWidget): - #sigClicked = QtCore.Signal(object) - - def __init__(self, size, pxMode, brush, pen, data, index=None): - QtGui.QGraphicsWidget.__init__(self) - self.pxMode = pxMode - - self.pen = pen - self.brush = brush - self.size = size - self.index = index - #s2 = size/2. - self.path = QtGui.QPainterPath() - self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) - if pxMode: - #self.setCacheMode(self.DeviceCoordinateCache) ## broken. - self.setFlags(self.flags() | self.ItemIgnoresTransformations) - self.spotImage = QtGui.QImage(size, size, QtGui.QImage.Format_ARGB32_Premultiplied) - self.spotImage.fill(0) - p = QtGui.QPainter(self.spotImage) - p.setRenderHint(p.Antialiasing) - p.setBrush(brush) - p.setPen(pen) - p.drawEllipse(0, 0, size, size) - p.end() - self.pixmap = QtGui.QPixmap(self.spotImage) - else: - self.scale(size, size) - self.data = data - - def setBrush(self, brush): - self.brush = mkBrush(brush) - self.update() - - def setPen(self, pen): - self.pen = mkPen(pen) - self.update() - - def boundingRect(self): - return self.path.boundingRect() - - def shape(self): - return self.path - - def paint(self, p, *opts): - if self.pxMode: - p.drawPixmap(QtCore.QPoint(int(-0.5*self.size), int(-0.5*self.size)), self.pixmap) - else: - p.setPen(self.pen) - p.setBrush(self.brush) - p.drawPath(self.path) - - #def mousePressEvent(self, ev): - #QtGui.QGraphicsItem.mousePressEvent(self, ev) - #if ev.button() == QtCore.Qt.LeftButton: - #self.mouseMoved = False - #ev.accept() - #else: - #ev.ignore() - - - - #def mouseMoveEvent(self, ev): - #QtGui.QGraphicsItem.mouseMoveEvent(self, ev) - #self.mouseMoved = True - #pass - - #def mouseReleaseEvent(self, ev): - #QtGui.QGraphicsItem.mouseReleaseEvent(self, ev) - #if not self.mouseMoved: - #self.sigClicked.emit(self) - -class PixmapSpotItem(QtGui.QGraphicsItem): - #sigClicked = QtCore.Signal(object) - - def __init__(self, size, brush, pen, data, image=None, index=None): - """This class draws a scale-invariant image centered at 0,0. - If no image is specified, then an antialiased circle is constructed instead. - It should be quite fast, but large spots will use a lot of memory.""" - - QtGui.QGraphicsItem.__init__(self) - self.pen = pen - self.brush = brush - self.size = size - self.index = index - self.setFlags(self.flags() | self.ItemIgnoresTransformations | self.ItemHasNoContents) - if image is None: - self.image = self.makeSpotImage(self.size, self.pen, self.brush) - else: - self.image = image - self.pixmap = QtGui.QPixmap(self.image) - #self.setPixmap(self.pixmap) - self.data = data - self.pi = QtGui.QGraphicsPixmapItem(self.pixmap, self) - self.pi.setPos(-0.5*size, -0.5*size) - - #self.translate(-0.5, -0.5) - def boundingRect(self): - return self.pi.boundingRect() - - @staticmethod - def makeSpotImage(size, pen, brush): - img = QtGui.QImage(size+2, size+2, QtGui.QImage.Format_ARGB32_Premultiplied) - img.fill(0) - p = QtGui.QPainter(img) - try: - p.setRenderHint(p.Antialiasing) - p.setBrush(brush) - p.setPen(pen) - p.drawEllipse(1, 1, size, size) - finally: - p.end() ## failure to end a painter properly causes crash. - return img - - - - #def paint(self, p, *args): - #p.setCompositionMode(p.CompositionMode_Plus) - #QtGui.QGraphicsPixmapItem.paint(self, p, *args) - - #def setBrush(self, brush): - #self.brush = mkBrush(brush) - #self.update() - - #def setPen(self, pen): - #self.pen = mkPen(pen) - #self.update() - - #def boundingRect(self): - #return self.path.boundingRect() - - #def shape(self): - #return self.path - - #def paint(self, p, *opts): - #if self.pxMode: - #p.drawPixmap(QtCore.QPoint(int(-0.5*self.size), int(-0.5*self.size)), self.pixmap) - #else: - #p.setPen(self.pen) - #p.setBrush(self.brush) - #p.drawPath(self.path) - - - -class ROIPlotItem(PlotCurveItem): - """Plot curve that monitors an ROI and image for changes to automatically replot.""" - def __init__(self, roi, data, img, axes=(0,1), xVals=None, color=None): - self.roi = roi - self.roiData = data - self.roiImg = img - self.axes = axes - self.xVals = xVals - PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color) - #roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) - roi.sigRegionChanged.connect(self.roiChangedEvent) - #self.roiChangedEvent() - - def getRoiData(self): - d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes) - if d is None: - return - while d.ndim > 1: - d = d.mean(axis=1) - return d - - def roiChangedEvent(self): - d = self.getRoiData() - self.updateData(d, self.xVals) - - - - -class UIGraphicsItem(GraphicsObject): - """Base class for graphics items with boundaries relative to a GraphicsView widget""" - def __init__(self, view, bounds=None): - GraphicsObject.__init__(self) - self._view = weakref.ref(view) - if bounds is None: - self._bounds = QtCore.QRectF(0, 0, 1, 1) - else: - self._bounds = bounds - self._viewRect = self._view().rect() - self._viewTransform = self.viewTransform() - self.setNewBounds() - #QtCore.QObject.connect(view, QtCore.SIGNAL('viewChanged'), self.viewChangedEvent) - view.sigRangeChanged.connect(self.viewRangeChanged) - - def viewRect(self): - """Return the viewport widget rect""" - return self._view().rect() - - def viewTransform(self): - """Returns a matrix that maps viewport coordinates onto scene coordinates""" - if self._view() is None: - return QtGui.QTransform() - else: - return self._view().viewportTransform() - - def boundingRect(self): - if self._view() is None: - self.bounds = self._bounds - else: - vr = self._view().rect() - tr = self.viewTransform() - if vr != self._viewRect or tr != self._viewTransform: - #self.viewChangedEvent(vr, self._viewRect) - self._viewRect = vr - self._viewTransform = tr - self.setNewBounds() - #print "viewRect", self._viewRect.x(), self._viewRect.y(), self._viewRect.width(), self._viewRect.height() - #print "bounds", self.bounds.x(), self.bounds.y(), self.bounds.width(), self.bounds.height() - return self.bounds - - def setNewBounds(self): - bounds = QtCore.QRectF( - QtCore.QPointF(self._bounds.left()*self._viewRect.width(), self._bounds.top()*self._viewRect.height()), - QtCore.QPointF(self._bounds.right()*self._viewRect.width(), self._bounds.bottom()*self._viewRect.height()) - ) - bounds.adjust(0.5, 0.5, 0.5, 0.5) - self.bounds = self.viewTransform().inverted()[0].mapRect(bounds) - self.prepareGeometryChange() - - def viewRangeChanged(self): - """Called when the view widget is resized""" - self.boundingRect() - self.update() - - def unitRect(self): - return self.viewTransform().inverted()[0].mapRect(QtCore.QRectF(0, 0, 1, 1)) - - def paint(self, *args): - pass - - -class DebugText(QtGui.QGraphicsTextItem): - def paint(self, *args): - p = debug.Profiler("DebugText.paint", disabled=True) - QtGui.QGraphicsTextItem.paint(self, *args) - p.finish() - -class LabelItem(QtGui.QGraphicsWidget): - def __init__(self, text, parent=None, **args): - QtGui.QGraphicsWidget.__init__(self, parent) - self.item = DebugText(self) - self.opts = args - if 'color' not in args: - self.opts['color'] = 'CCC' - else: - if isinstance(args['color'], QtGui.QColor): - self.opts['color'] = colorStr(args['color'])[:6] - self.sizeHint = {} - self.setText(text) - - - def setAttr(self, attr, value): - """Set default text properties. See setText() for accepted parameters.""" - self.opts[attr] = value - - def setText(self, text, **args): - """Set the text and text properties in the label. Accepts optional arguments for auto-generating - a CSS style string: - color: string (example: 'CCFF00') - size: string (example: '8pt') - bold: boolean - italic: boolean - """ - self.text = text - opts = self.opts.copy() - for k in args: - opts[k] = args[k] - - optlist = [] - if 'color' in opts: - optlist.append('color: #' + opts['color']) - if 'size' in opts: - optlist.append('font-size: ' + opts['size']) - if 'bold' in opts and opts['bold'] in [True, False]: - optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']]) - if 'italic' in opts and opts['italic'] in [True, False]: - optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']]) - full = "%s" % ('; '.join(optlist), text) - #print full - self.item.setHtml(full) - self.updateMin() - - def resizeEvent(self, ev): - c1 = self.boundingRect().center() - c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos() - dif = c1 - c2 - self.item.moveBy(dif.x(), dif.y()) - #print c1, c2, dif, self.item.pos() - - def setAngle(self, angle): - self.angle = angle - self.item.resetTransform() - self.item.rotate(angle) - self.updateMin() - - def updateMin(self): - bounds = self.item.mapRectToParent(self.item.boundingRect()) - self.setMinimumWidth(bounds.width()) - self.setMinimumHeight(bounds.height()) - #print self.text, bounds.width(), bounds.height() - - #self.sizeHint = { - #QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), - #QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), - #QtCore.Qt.MaximumSize: (bounds.width()*2, bounds.height()*2), - #QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? - #} - - - #def sizeHint(self, hint, constraint): - #return self.sizeHint[hint] - - - - - -class ScaleItem(QtGui.QGraphicsWidget): - def __init__(self, orientation, pen=None, linkView=None, parent=None): - """GraphicsItem showing a single plot axis with ticks, values, and label. - Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. - Ticks can be extended to make a grid.""" - QtGui.QGraphicsWidget.__init__(self, parent) - self.label = QtGui.QGraphicsTextItem(self) - self.orientation = orientation - if orientation not in ['left', 'right', 'top', 'bottom']: - raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") - if orientation in ['left', 'right']: - #self.setMinimumWidth(25) - #self.setSizePolicy(QtGui.QSizePolicy( - #QtGui.QSizePolicy.Minimum, - #QtGui.QSizePolicy.Expanding - #)) - self.label.rotate(-90) - #else: - #self.setMinimumHeight(50) - #self.setSizePolicy(QtGui.QSizePolicy( - #QtGui.QSizePolicy.Expanding, - #QtGui.QSizePolicy.Minimum - #)) - #self.drawLabel = False - - self.labelText = '' - self.labelUnits = '' - self.labelUnitPrefix='' - self.labelStyle = {'color': '#CCC'} - - self.textHeight = 18 - self.tickLength = 10 - self.scale = 1.0 - self.autoScale = True - - self.setRange(0, 1) - - if pen is None: - pen = QtGui.QPen(QtGui.QColor(100, 100, 100)) - self.setPen(pen) - - self.linkedView = None - if linkView is not None: - self.linkToView(linkView) - - self.showLabel(False) - - self.grid = False - self.setCacheMode(self.DeviceCoordinateCache) - - def close(self): - self.scene().removeItem(self.label) - self.label = None - self.scene().removeItem(self) - - def setGrid(self, grid): - """Set the alpha value for the grid, or False to disable.""" - self.grid = grid - self.update() - - - def resizeEvent(self, ev=None): - #s = self.size() - - ## Set the position of the label - nudge = 5 - br = self.label.boundingRect() - p = QtCore.QPointF(0, 0) - if self.orientation == 'left': - p.setY(int(self.size().height()/2 + br.width()/2)) - p.setX(-nudge) - #s.setWidth(10) - elif self.orientation == 'right': - #s.setWidth(10) - p.setY(int(self.size().height()/2 + br.width()/2)) - p.setX(int(self.size().width()-br.height()+nudge)) - elif self.orientation == 'top': - #s.setHeight(10) - p.setY(-nudge) - p.setX(int(self.size().width()/2. - br.width()/2.)) - elif self.orientation == 'bottom': - p.setX(int(self.size().width()/2. - br.width()/2.)) - #s.setHeight(10) - p.setY(int(self.size().height()-br.height()+nudge)) - #self.label.resize(s) - self.label.setPos(p) - - def showLabel(self, show=True): - #self.drawLabel = show - self.label.setVisible(show) - if self.orientation in ['left', 'right']: - self.setWidth() - else: - self.setHeight() - if self.autoScale: - self.setScale() - - def setLabel(self, text=None, units=None, unitPrefix=None, **args): - if text is not None: - self.labelText = text - self.showLabel() - if units is not None: - self.labelUnits = units - self.showLabel() - if unitPrefix is not None: - self.labelUnitPrefix = unitPrefix - if len(args) > 0: - self.labelStyle = args - self.label.setHtml(self.labelString()) - self.resizeEvent() - self.update() - - def labelString(self): - if self.labelUnits == '': - if self.scale == 1.0: - units = '' - else: - units = u'(x%g)' % (1.0/self.scale) - else: - #print repr(self.labelUnitPrefix), repr(self.labelUnits) - units = u'(%s%s)' % (self.labelUnitPrefix, self.labelUnits) - - s = u'%s %s' % (self.labelText, units) - - style = ';'.join(['%s: "%s"' % (k, self.labelStyle[k]) for k in self.labelStyle]) - - return u"%s" % (style, s) - - def setHeight(self, h=None): - if h is None: - h = self.textHeight + self.tickLength - if self.label.isVisible(): - h += self.textHeight - self.setMaximumHeight(h) - self.setMinimumHeight(h) - - - def setWidth(self, w=None): - if w is None: - w = self.tickLength + 40 - if self.label.isVisible(): - w += self.textHeight - self.setMaximumWidth(w) - self.setMinimumWidth(w) - - def setPen(self, pen): - self.pen = pen - self.update() - - def setScale(self, scale=None): - if scale is None: - #if self.drawLabel: ## If there is a label, then we are free to rescale the values - if self.label.isVisible(): - d = self.range[1] - self.range[0] - #pl = 1-int(log10(d)) - #scale = 10 ** pl - (scale, prefix) = siScale(d / 2.) - if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. - scale = 1.0 - prefix = '' - self.setLabel(unitPrefix=prefix) - else: - scale = 1.0 - - - if scale != self.scale: - self.scale = scale - self.setLabel() - self.update() - - def setRange(self, mn, mx): - if mn in [np.nan, np.inf, -np.inf] or mx in [np.nan, np.inf, -np.inf]: - raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx))) - self.range = [mn, mx] - if self.autoScale: - self.setScale() - self.update() - - def linkToView(self, view): - if self.orientation in ['right', 'left']: - if self.linkedView is not None and self.linkedView() is not None: - #view.sigYRangeChanged.disconnect(self.linkedViewChanged) - ## should be this instead? - self.linkedView().sigYRangeChanged.disconnect(self.linkedViewChanged) - self.linkedView = weakref.ref(view) - view.sigYRangeChanged.connect(self.linkedViewChanged) - #signal = QtCore.SIGNAL('yRangeChanged') - else: - if self.linkedView is not None and self.linkedView() is not None: - #view.sigYRangeChanged.disconnect(self.linkedViewChanged) - ## should be this instead? - self.linkedView().sigXRangeChanged.disconnect(self.linkedViewChanged) - self.linkedView = weakref.ref(view) - view.sigXRangeChanged.connect(self.linkedViewChanged) - #signal = QtCore.SIGNAL('xRangeChanged') - - - def linkedViewChanged(self, view, newRange): - self.setRange(*newRange) - - def boundingRect(self): - if self.linkedView is None or self.linkedView() is None or self.grid is False: - return self.mapRectFromParent(self.geometry()) - else: - return self.mapRectFromParent(self.geometry()) | self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) - - def paint(self, p, opt, widget): - prof = debug.Profiler("ScaleItem.paint", disabled=True) - p.setPen(self.pen) - - #bounds = self.boundingRect() - bounds = self.mapRectFromParent(self.geometry()) - - if self.linkedView is None or self.linkedView() is None or self.grid is False: - tbounds = bounds - else: - tbounds = self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) - - if self.orientation == 'left': - p.drawLine(bounds.topRight(), bounds.bottomRight()) - tickStart = tbounds.right() - tickStop = bounds.right() - tickDir = -1 - axis = 0 - elif self.orientation == 'right': - p.drawLine(bounds.topLeft(), bounds.bottomLeft()) - tickStart = tbounds.left() - tickStop = bounds.left() - tickDir = 1 - axis = 0 - elif self.orientation == 'top': - p.drawLine(bounds.bottomLeft(), bounds.bottomRight()) - tickStart = tbounds.bottom() - tickStop = bounds.bottom() - tickDir = -1 - axis = 1 - elif self.orientation == 'bottom': - p.drawLine(bounds.topLeft(), bounds.topRight()) - tickStart = tbounds.top() - tickStop = bounds.top() - tickDir = 1 - axis = 1 - - ## Determine optimal tick spacing - #intervals = [1., 2., 5., 10., 20., 50.] - #intervals = [1., 2.5, 5., 10., 25., 50.] - intervals = [1., 2., 10., 20., 100.] - dif = abs(self.range[1] - self.range[0]) - if dif == 0.0: - return - #print "dif:", dif - pw = 10 ** (np.floor(np.log10(dif))-1) - for i in range(len(intervals)): - i1 = i - if dif / (pw*intervals[i]) < 10: - break - - textLevel = i1 ## draw text at this scale level - - #print "range: %s dif: %f power: %f interval: %f spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp) - - #print " start at %f, %d ticks" % (start, num) - - - if axis == 0: - xs = -bounds.height() / dif - else: - xs = bounds.width() / dif - - prof.mark('init') - - tickPositions = set() # remembers positions of previously drawn ticks - ## draw ticks and generate list of texts to draw - ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) - ## draw three different intervals, long ticks first - texts = [] - for i in reversed([i1, i1+1, i1+2]): - if i > len(intervals): - continue - ## spacing for this interval - sp = pw*intervals[i] - - ## determine starting tick - start = np.ceil(self.range[0] / sp) * sp - - ## determine number of ticks - num = int(dif / sp) + 1 - - ## last tick value - last = start + sp * num - - ## Number of decimal places to print - maxVal = max(abs(start), abs(last)) - places = max(0, 1-int(np.log10(sp*self.scale))) - - ## length of tick - h = min(self.tickLength, (self.tickLength*3 / num) - 1.) - - ## alpha - a = min(255, (765. / num) - 1.) - - if axis == 0: - offset = self.range[0] * xs - bounds.height() - else: - offset = self.range[0] * xs - - for j in range(num): - v = start + sp * j - x = (v * xs) - offset - p1 = [0, 0] - p2 = [0, 0] - p1[axis] = tickStart - p2[axis] = tickStop + h*tickDir - p1[1-axis] = p2[1-axis] = x - - if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]: - continue - if p1[1-axis] < 0: - continue - p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100, a))) - # draw tick only if there is none - tickPos = p1[1-axis] - if tickPos not in tickPositions: - p.drawLine(Point(p1), Point(p2)) - tickPositions.add(tickPos) - if i == textLevel: - if abs(v) < .001 or abs(v) >= 10000: - vstr = "%g" % (v * self.scale) - else: - vstr = ("%%0.%df" % places) % (v * self.scale) - - textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) - height = textRect.height() - self.textHeight = height - if self.orientation == 'left': - textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop-100, x-(height/2), 100-self.tickLength, height) - elif self.orientation == 'right': - textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop+self.tickLength, x-(height/2), 100-self.tickLength, height) - elif self.orientation == 'top': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-100, tickStop-self.tickLength-height, 200, height) - elif self.orientation == 'bottom': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-100, tickStop+self.tickLength, 200, height) - - p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) - #p.drawText(rect, textFlags, vstr) - texts.append((rect, textFlags, vstr)) - - prof.mark('draw ticks') - for args in texts: - p.drawText(*args) - prof.mark('draw text') - prof.finish() - - def show(self): - - if self.orientation in ['left', 'right']: - self.setWidth() - else: - self.setHeight() - QtGui.QGraphicsWidget.show(self) - - def hide(self): - if self.orientation in ['left', 'right']: - self.setWidth(0) - else: - self.setHeight(0) - QtGui.QGraphicsWidget.hide(self) - - def wheelEvent(self, ev): - if self.linkedView is None or self.linkedView() is None: return - if self.orientation in ['left', 'right']: - self.linkedView().wheelEvent(ev, axis=1) - else: - self.linkedView().wheelEvent(ev, axis=0) - ev.accept() - - - -class ViewBox(QtGui.QGraphicsWidget): - - sigYRangeChanged = QtCore.Signal(object, object) - sigXRangeChanged = QtCore.Signal(object, object) - sigRangeChangedManually = QtCore.Signal(object) - sigRangeChanged = QtCore.Signal(object, object) - - """Box that allows internal scaling/panning of children by mouse drag. Not compatible with GraphicsView having the same functionality.""" - def __init__(self, parent=None, border=None): - QtGui.QGraphicsWidget.__init__(self, parent) - #self.gView = view - #self.showGrid = showGrid - - ## separating targetRange and viewRange allows the view to be resized - ## while keeping all previously viewed contents visible - self.targetRange = [[0,1], [0,1]] ## child coord. range visible [[xmin, xmax], [ymin, ymax]] - self.viewRange = [[0,1], [0,1]] ## actual range viewed - - self.wheelScaleFactor = -1.0 / 8.0 - self.aspectLocked = False - self.setFlag(QtGui.QGraphicsItem.ItemClipsChildrenToShape) - #self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - #self.childGroup = QtGui.QGraphicsItemGroup(self) - self.childGroup = ItemGroup(self) - self.currentScale = Point(1, 1) - - self.yInverted = False - #self.invertY() - self.setZValue(-100) - #self.picture = None - self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) - - self.border = border - - self.mouseEnabled = [True, True] - - def setMouseEnabled(self, x, y): - self.mouseEnabled = [x, y] - - def addItem(self, item): - if item.zValue() < self.zValue(): - item.setZValue(self.zValue()+1) - item.setParentItem(self.childGroup) - #print "addItem:", item, item.boundingRect() - - def removeItem(self, item): - self.scene().removeItem(item) - - def resizeEvent(self, ev): - #self.setRange(self.range, padding=0) - self.updateMatrix() - - - def viewRect(self): - try: - vr0 = self.viewRange[0] - vr1 = self.viewRange[1] - return QtCore.QRectF(vr0[0], vr1[0], vr0[1]-vr0[0], vr1[1] - vr1[0]) - except: - print "make qrectf failed:", self.viewRange - raise - - def targetRect(self): - """Return the region which has been requested to be visible. - (this is not necessarily the same as the region that is *actually* visible)""" - try: - tr0 = self.targetRange[0] - tr1 = self.targetRange[1] - return QtCore.QRectF(tr0[0], tr1[0], tr0[1]-tr0[0], tr1[1] - tr1[0]) - except: - print "make qrectf failed:", self.targetRange - raise - - def invertY(self, b=True): - self.yInverted = b - self.updateMatrix() - - def setAspectLocked(self, lock=True, ratio=1): - """If the aspect ratio is locked, view scaling is always forced to be isotropic. - By default, the ratio is set to 1; x and y both have the same scaling. - This ratio can be overridden (width/height), or use None to lock in the current ratio. - """ - if not lock: - self.aspectLocked = False - else: - vr = self.viewRect() - currentRatio = vr.width() / vr.height() - if ratio is None: - ratio = currentRatio - self.aspectLocked = ratio - if ratio != currentRatio: ## If this would change the current range, do that now - #self.setRange(0, self.viewRange[0][0], self.viewRange[0][1]) - self.updateMatrix() - - def childTransform(self): - m = self.childGroup.transform() - m1 = QtGui.QTransform() - m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y()) - return m*m1 - - - def viewScale(self): - vr = self.viewRect() - #print "viewScale:", self.range - xd = vr.width() - yd = vr.height() - if xd == 0 or yd == 0: - print "Warning: 0 range in view:", xd, yd - return np.array([1,1]) - - #cs = self.canvas().size() - cs = self.boundingRect() - scale = np.array([cs.width() / xd, cs.height() / yd]) - #print "view scale:", scale - return scale - - def scaleBy(self, s, center=None): - """Scale by s around given center point (or center of view)""" - #print "scaleBy", s, center - #if self.aspectLocked: - #s[0] = s[1] - scale = Point(s) - if self.aspectLocked is not False: - scale[0] = self.aspectLocked * scale[1] - - - #xr, yr = self.range - vr = self.viewRect() - if center is None: - center = Point(vr.center()) - #xc = (xr[1] + xr[0]) * 0.5 - #yc = (yr[1] + yr[0]) * 0.5 - else: - center = Point(center) - #(xc, yc) = center - - #x1 = xc + (xr[0]-xc) * s[0] - #x2 = xc + (xr[1]-xc) * s[0] - #y1 = yc + (yr[0]-yc) * s[1] - #y2 = yc + (yr[1]-yc) * s[1] - tl = center + (vr.topLeft()-center) * scale - br = center + (vr.bottomRight()-center) * scale - - #print xr, xc, s, (xr[0]-xc) * s[0], (xr[1]-xc) * s[0] - #print [[x1, x2], [y1, y2]] - - #if not self.aspectLocked: - #self.setXRange(x1, x2, update=False, padding=0) - #self.setYRange(y1, y2, padding=0) - #print self.range - - self.setRange(QtCore.QRectF(tl, br), padding=0) - - def translateBy(self, t, viewCoords=False): - t = t.astype(np.float) - #print "translate:", t, self.viewScale() - if viewCoords: ## scale from pixels - t /= self.viewScale() - #xr, yr = self.range - - vr = self.viewRect() - #print xr, yr, t - #self.setXRange(xr[0] + t[0], xr[1] + t[0], update=False, padding=0) - #self.setYRange(yr[0] + t[1], yr[1] + t[1], padding=0) - self.setRange(vr.translated(Point(t)), padding=0) - - def wheelEvent(self, ev, axis=None): - mask = np.array(self.mouseEnabled, dtype=np.float) - if axis is not None and axis >= 0 and axis < len(mask): - mv = mask[axis] - mask[:] = 0 - mask[axis] = mv - s = ((mask * 0.02) + 1) ** (ev.delta() * self.wheelScaleFactor) # actual scaling factor - # scale 'around' mouse cursor position - center = Point(self.childGroup.transform().inverted()[0].map(ev.pos())) - self.scaleBy(s, center) - #self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) - self.sigRangeChangedManually.emit(self.mouseEnabled) - ev.accept() - - def mouseMoveEvent(self, ev): - QtGui.QGraphicsWidget.mouseMoveEvent(self, ev) - pos = np.array([ev.pos().x(), ev.pos().y()]) - dif = pos - self.mousePos - dif *= -1 - self.mousePos = pos - - ## Ignore axes if mouse is disabled - mask = np.array(self.mouseEnabled, dtype=np.float) - - ## Scale or translate based on mouse button - if ev.buttons() & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton): - if not self.yInverted: - mask *= np.array([1, -1]) - tr = dif*mask - self.translateBy(tr, viewCoords=True) - #self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) - self.sigRangeChangedManually.emit(self.mouseEnabled) - ev.accept() - elif ev.buttons() & QtCore.Qt.RightButton: - if self.aspectLocked is not False: - mask[0] = 0 - dif = ev.screenPos() - ev.lastScreenPos() - dif = np.array([dif.x(), dif.y()]) - dif[0] *= -1 - s = ((mask * 0.02) + 1) ** dif - #print mask, dif, s - center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton))) - self.scaleBy(s, center) - #self.emit(QtCore.SIGNAL('rangeChangedManually'), self.mouseEnabled) - self.sigRangeChangedManually.emit(self.mouseEnabled) - ev.accept() - else: - ev.ignore() - - def mousePressEvent(self, ev): - QtGui.QGraphicsWidget.mousePressEvent(self, ev) - - self.mousePos = np.array([ev.pos().x(), ev.pos().y()]) - self.pressPos = self.mousePos.copy() - ev.accept() - - def mouseReleaseEvent(self, ev): - QtGui.QGraphicsWidget.mouseReleaseEvent(self, ev) - pos = np.array([ev.pos().x(), ev.pos().y()]) - #if sum(abs(self.pressPos - pos)) < 3: ## Detect click - #if ev.button() == QtCore.Qt.RightButton: - #self.ctrlMenu.popup(self.mapToGlobal(ev.pos())) - self.mousePos = pos - ev.accept() - - def setRange(self, ax, min=None, max=None, padding=0.02, update=True): - if isinstance(ax, QtCore.QRectF): - changes = {0: [ax.left(), ax.right()], 1: [ax.top(), ax.bottom()]} - #if self.aspectLocked is not False: - #sbr = self.boundingRect() - #if sbr.width() == 0 or (ax.height()/ax.width()) > (sbr.height()/sbr.width()): - #chax = 0 - #else: - #chax = 1 - - - - - elif ax in [1,0]: - changes = {ax: [min,max]} - #if self.aspectLocked is not False: - #ax2 = 1 - ax - #ratio = self.aspectLocked - #r2 = self.range[ax2] - #d = ratio * (max-min) * 0.5 - #c = (self.range[ax2][1] + self.range[ax2][0]) * 0.5 - #changes[ax2] = [c-d, c+d] - - else: - print ax - raise Exception("argument 'ax' must be 0, 1, or QRectF.") - - - changed = [False, False] - for ax, range in changes.iteritems(): - min, max = range - if min == max: ## If we requested 0 range, try to preserve previous scale. Otherwise just pick an arbitrary scale. - dy = self.viewRange[ax][1] - self.viewRange[ax][0] - if dy == 0: - dy = 1 - min -= dy*0.5 - max += dy*0.5 - padding = 0.0 - if any(np.isnan([min, max])) or any(np.isinf([min, max])): - raise Exception("Not setting range [%s, %s]" % (str(min), str(max))) - - p = (max-min) * padding - min -= p - max += p - - if self.targetRange[ax] != [min, max]: - self.targetRange[ax] = [min, max] - changed[ax] = True - - if update: - self.updateMatrix(changed) - - - - - def setYRange(self, min, max, update=True, padding=0.02): - self.setRange(1, min, max, update=update, padding=padding) - - def setXRange(self, min, max, update=True, padding=0.02): - self.setRange(0, min, max, update=update, padding=padding) - - def autoRange(self, padding=0.02): - br = self.childGroup.childrenBoundingRect() - self.setRange(br, padding=padding) - - - def updateMatrix(self, changed=None): - if changed is None: - changed = [False, False] - #print "udpateMatrix:" - #print " range:", self.range - tr = self.targetRect() - bounds = self.boundingRect() - - ## set viewRect, given targetRect and possibly aspect ratio constraint - if self.aspectLocked is False or bounds.height() == 0: - self.viewRange = [self.targetRange[0][:], self.targetRange[1][:]] - else: - viewRatio = bounds.width() / bounds.height() - targetRatio = self.aspectLocked * tr.width() / tr.height() - if targetRatio > viewRatio: - ## target is wider than view - dy = 0.5 * (tr.width() / (self.aspectLocked * viewRatio) - tr.height()) - if dy != 0: - changed[1] = True - self.viewRange = [self.targetRange[0][:], [self.targetRange[1][0] - dy, self.targetRange[1][1] + dy]] - else: - dx = 0.5 * (tr.height() * viewRatio * self.aspectLocked - tr.width()) - if dx != 0: - changed[0] = True - self.viewRange = [[self.targetRange[0][0] - dx, self.targetRange[0][1] + dx], self.targetRange[1][:]] - - - vr = self.viewRect() - translate = Point(vr.center()) - #print " bounds:", bounds - if vr.height() == 0 or vr.width() == 0: - return - scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) - #print " scale:", scale - m = QtGui.QTransform() - - ## First center the viewport at 0 - self.childGroup.resetTransform() - center = self.transform().inverted()[0].map(bounds.center()) - #print " transform to center:", center - if self.yInverted: - m.translate(center.x(), -center.y()) - #print " inverted; translate", center.x(), center.y() - else: - m.translate(center.x(), center.y()) - #print " not inverted; translate", center.x(), -center.y() - - ## Now scale and translate properly - if not self.yInverted: - scale = scale * Point(1, -1) - m.scale(scale[0], scale[1]) - st = translate - m.translate(-st[0], -st[1]) - self.childGroup.setTransform(m) - self.currentScale = scale - - - if changed[0]: - self.sigXRangeChanged.emit(self, tuple(self.viewRange[0])) - if changed[1]: - self.sigYRangeChanged.emit(self, tuple(self.viewRange[1])) - if any(changed): - self.sigRangeChanged.emit(self, self.viewRange) - - - - def boundingRect(self): - return QtCore.QRectF(0, 0, self.size().width(), self.size().height()) - - def paint(self, p, opt, widget): - if self.border is not None: - bounds = self.boundingRect() - p.setPen(self.border) - #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) - p.drawRect(bounds) - - -class InfiniteLine(GraphicsObject): - - sigDragged = QtCore.Signal(object) - sigPositionChangeFinished = QtCore.Signal(object) - sigPositionChanged = QtCore.Signal(object) - - def __init__(self, view, pos=0, angle=90, pen=None, movable=False, bounds=None): - GraphicsObject.__init__(self) - self.bounds = QtCore.QRectF() ## graphicsitem boundary - - if bounds is None: ## allowed value boundaries for orthogonal lines - self.maxRange = [None, None] - else: - self.maxRange = bounds - self.setMovable(movable) - self.view = weakref.ref(view) - self.p = [0, 0] - self.setAngle(angle) - self.setPos(pos) - - - self.hasMoved = False - - - if pen is None: - pen = QtGui.QPen(QtGui.QColor(200, 200, 100)) - self.setPen(pen) - self.currentPen = self.pen - #self.setFlag(self.ItemSendsScenePositionChanges) - #for p in self.getBoundingParents(): - #QtCore.QObject.connect(p, QtCore.SIGNAL('viewChanged'), self.updateLine) - #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateLine) - self.view().sigRangeChanged.connect(self.updateLine) - - def setMovable(self, m): - self.movable = m - self.setAcceptHoverEvents(m) - - - def setBounds(self, bounds): - self.maxRange = bounds - self.setValue(self.value()) - - def hoverEnterEvent(self, ev): - self.currentPen = QtGui.QPen(QtGui.QColor(255, 0,0)) - self.update() - ev.ignore() - - def hoverLeaveEvent(self, ev): - self.currentPen = self.pen - self.update() - ev.ignore() - - def setPen(self, pen): - self.pen = pen - self.currentPen = self.pen - - def setAngle(self, angle): - """Takes angle argument in degrees.""" - self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 - self.updateLine() - - def setPos(self, pos): - if type(pos) in [list, tuple]: - newPos = pos - elif isinstance(pos, QtCore.QPointF): - newPos = [pos.x(), pos.y()] - else: - if self.angle == 90: - newPos = [pos, 0] - elif self.angle == 0: - newPos = [0, pos] - else: - raise Exception("Must specify 2D coordinate for non-orthogonal lines.") - - ## check bounds (only works for orthogonal lines) - if self.angle == 90: - if self.maxRange[0] is not None: - newPos[0] = max(newPos[0], self.maxRange[0]) - if self.maxRange[1] is not None: - newPos[0] = min(newPos[0], self.maxRange[1]) - elif self.angle == 0: - if self.maxRange[0] is not None: - newPos[1] = max(newPos[1], self.maxRange[0]) - if self.maxRange[1] is not None: - newPos[1] = min(newPos[1], self.maxRange[1]) - - - if self.p != newPos: - self.p = newPos - self.updateLine() - #self.emit(QtCore.SIGNAL('positionChanged'), self) - self.sigPositionChanged.emit(self) - - def getXPos(self): - return self.p[0] - - def getYPos(self): - return self.p[1] - - def getPos(self): - return self.p - - def value(self): - if self.angle%180 == 0: - return self.getYPos() - elif self.angle%180 == 90: - return self.getXPos() - else: - return self.getPos() - - def setValue(self, v): - self.setPos(v) - - ## broken in 4.7 - #def itemChange(self, change, val): - #if change in [self.ItemScenePositionHasChanged, self.ItemSceneHasChanged]: - #self.updateLine() - #print "update", change - #print self.getBoundingParents() - #else: - #print "ignore", change - #return GraphicsObject.itemChange(self, change, val) - - def updateLine(self): - - #unit = QtCore.QRect(0, 0, 10, 10) - #if self.scene() is not None: - #gv = self.scene().views()[0] - #unit = gv.mapToScene(unit).boundingRect() - ##print unit - #unit = self.mapRectFromScene(unit) - ##print unit - - vr = self.view().viewRect() - #vr = self.viewBounds() - if vr is None: - return - #print 'before', self.bounds - - if self.angle > 45: - m = np.tan((90-self.angle) * np.pi / 180.) - y2 = vr.bottom() - y1 = vr.top() - x1 = self.p[0] + (y1 - self.p[1]) * m - x2 = self.p[0] + (y2 - self.p[1]) * m - else: - m = np.tan(self.angle * np.pi / 180.) - x1 = vr.left() - x2 = vr.right() - y2 = self.p[1] + (x1 - self.p[0]) * m - y1 = self.p[1] + (x2 - self.p[0]) * m - #print vr, x1, y1, x2, y2 - self.prepareGeometryChange() - self.line = (QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)) - self.bounds = QtCore.QRectF(self.line[0], self.line[1]) - ## Stupid bug causes lines to disappear: - if self.angle % 180 == 90: - px = self.pixelWidth() - #self.bounds.setWidth(1e-9) - self.bounds.setX(x1 + px*-5) - self.bounds.setWidth(px*10) - if self.angle % 180 == 0: - px = self.pixelHeight() - #self.bounds.setHeight(1e-9) - self.bounds.setY(y1 + px*-5) - self.bounds.setHeight(px*10) - - #QtGui.QGraphicsLineItem.setLine(self, x1, y1, x2, y2) - #self.update() - - def boundingRect(self): - #self.updateLine() - #return QtGui.QGraphicsLineItem.boundingRect(self) - #print "bounds", self.bounds - return self.bounds - - def paint(self, p, *args): - w,h = self.pixelWidth()*5, self.pixelHeight()*5*1.1547 - #self.updateLine() - l = self.line - - p.setPen(self.currentPen) - #print "paint", self.line - p.drawLine(l[0], l[1]) - - p.setBrush(QtGui.QBrush(self.currentPen.color())) - p.drawConvexPolygon(QtGui.QPolygonF([ - l[0] + QtCore.QPointF(-w, 0), - l[0] + QtCore.QPointF(0, h), - l[0] + QtCore.QPointF(w, 0), - ])) - - #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) - #p.drawRect(self.boundingRect()) - - def mousePressEvent(self, ev): - if self.movable and ev.button() == QtCore.Qt.LeftButton: - ev.accept() - self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - #self.emit(QtCore.SIGNAL('dragged'), self) - self.sigDragged.emit(self) - self.hasMoved = True - - def mouseReleaseEvent(self, ev): - if self.hasMoved and ev.button() == QtCore.Qt.LeftButton: - self.hasMoved = False - #self.emit(QtCore.SIGNAL('positionChangeFinished'), self) - self.sigPositionChangeFinished.emit(self) - - - -class LinearRegionItem(GraphicsObject): - - sigRegionChangeFinished = QtCore.Signal(object) - sigRegionChanged = QtCore.Signal(object) - - """Used for marking a horizontal or vertical region in plots.""" - def __init__(self, view, orientation="vertical", vals=[0,1], brush=None, movable=True, bounds=None): - GraphicsObject.__init__(self) - self.orientation = orientation - if hasattr(self, "ItemHasNoContents"): - self.setFlag(self.ItemHasNoContents) - self.rect = QtGui.QGraphicsRectItem(self) - self.rect.setParentItem(self) - self.bounds = QtCore.QRectF() - self.view = weakref.ref(view) - self.setBrush = self.rect.setBrush - self.brush = self.rect.brush - - if orientation[0] == 'h': - self.lines = [ - InfiniteLine(view, QtCore.QPointF(0, vals[0]), 0, movable=movable, bounds=bounds), - InfiniteLine(view, QtCore.QPointF(0, vals[1]), 0, movable=movable, bounds=bounds)] - else: - self.lines = [ - InfiniteLine(view, QtCore.QPointF(vals[0], 0), 90, movable=movable, bounds=bounds), - InfiniteLine(view, QtCore.QPointF(vals[1], 0), 90, movable=movable, bounds=bounds)] - #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.updateBounds) - self.view().sigRangeChanged.connect(self.updateBounds) - - for l in self.lines: - l.setParentItem(self) - #l.connect(l, QtCore.SIGNAL('positionChangeFinished'), self.lineMoveFinished) - l.sigPositionChangeFinished.connect(self.lineMoveFinished) - #l.connect(l, QtCore.SIGNAL('positionChanged'), self.lineMoved) - l.sigPositionChanged.connect(self.lineMoved) - - if brush is None: - brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) - self.setBrush(brush) - self.setMovable(movable) - - def setBounds(self, bounds): - for l in self.lines: - l.setBounds(bounds) - - def setMovable(self, m): - for l in self.lines: - l.setMovable(m) - self.movable = m - - def boundingRect(self): - return self.rect.boundingRect() - - def lineMoved(self): - self.updateBounds() - #self.emit(QtCore.SIGNAL('regionChanged'), self) - self.sigRegionChanged.emit(self) - - def lineMoveFinished(self): - #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - self.sigRegionChangeFinished.emit(self) - - - def updateBounds(self): - vb = self.view().viewRect() - vals = [self.lines[0].value(), self.lines[1].value()] - if self.orientation[0] == 'h': - vb.setTop(min(vals)) - vb.setBottom(max(vals)) - else: - vb.setLeft(min(vals)) - vb.setRight(max(vals)) - if vb != self.bounds: - self.bounds = vb - self.rect.setRect(vb) - - def mousePressEvent(self, ev): - if not self.movable: - ev.ignore() - return - for l in self.lines: - l.mousePressEvent(ev) ## pass event to both lines so they move together - #if self.movable and ev.button() == QtCore.Qt.LeftButton: - #ev.accept() - #self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) - #else: - #ev.ignore() - - def mouseReleaseEvent(self, ev): - for l in self.lines: - l.mouseReleaseEvent(ev) - - def mouseMoveEvent(self, ev): - #print "move", ev.pos() - if not self.movable: - return - self.lines[0].blockSignals(True) # only want to update once - for l in self.lines: - l.mouseMoveEvent(ev) - self.lines[0].blockSignals(False) - #self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - #self.emit(QtCore.SIGNAL('dragged'), self) - - def getRegion(self): - if self.orientation[0] == 'h': - r = (self.bounds.top(), self.bounds.bottom()) - else: - r = (self.bounds.left(), self.bounds.right()) - return (min(r), max(r)) - - def setRegion(self, rgn): - self.lines[0].setValue(rgn[0]) - self.lines[1].setValue(rgn[1]) - - -class VTickGroup(QtGui.QGraphicsPathItem): - def __init__(self, xvals=None, yrange=None, pen=None, relative=False, view=None): - QtGui.QGraphicsPathItem.__init__(self) - if yrange is None: - yrange = [0, 1] - if xvals is None: - xvals = [] - if pen is None: - pen = (200, 200, 200) - self.ticks = [] - self.xvals = [] - if view is None: - self.view = None - else: - self.view = weakref.ref(view) - self.yrange = [0,1] - self.setPen(pen) - self.setYRange(yrange, relative) - self.setXVals(xvals) - self.valid = False - - def setPen(self, pen): - pen = mkPen(pen) - QtGui.QGraphicsPathItem.setPen(self, pen) - - def setXVals(self, vals): - self.xvals = vals - self.rebuildTicks() - self.valid = False - - def setYRange(self, vals, relative=False): - self.yrange = vals - self.relative = relative - if self.view is not None: - if relative: - #QtCore.QObject.connect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) - #QtCore.QObject.connect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) - self.view().sigRangeChanged.connect(self.rescale) - else: - try: - #QtCore.QObject.disconnect(self.view, QtCore.SIGNAL('viewChanged'), self.rebuildTicks) - #QtCore.QObject.disconnect(self.view(), QtCore.SIGNAL('viewChanged'), self.rescale) - self.view().sigRangeChanged.disconnect(self.rescale) - except: - pass - self.rebuildTicks() - self.valid = False - - def rescale(self): - #print "RESCALE:" - self.resetTransform() - #height = self.view.size().height() - #p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) - #p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) - #yr = [p1.y(), p2.y()] - vb = self.view().viewRect() - p1 = vb.bottom() - vb.height() * self.yrange[0] - p2 = vb.bottom() - vb.height() * self.yrange[1] - yr = [p1, p2] - - #print " ", vb, yr - self.translate(0.0, yr[0]) - self.scale(1.0, (yr[1]-yr[0])) - #print " ", self.mapRectToScene(self.boundingRect()) - self.boundingRect() - self.update() - - def boundingRect(self): - #print "--request bounds:" - b = QtGui.QGraphicsPathItem.boundingRect(self) - #print " ", self.mapRectToScene(b) - return b - - def yRange(self): - #if self.relative: - #height = self.view.size().height() - #p1 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[0])))) - #p2 = self.mapFromScene(self.view.mapToScene(QtCore.QPoint(0, height * (1.0-self.yrange[1])))) - #return [p1.y(), p2.y()] - #else: - #return self.yrange - - return self.yrange - - def rebuildTicks(self): - self.path = QtGui.QPainterPath() - yrange = self.yRange() - #print "rebuild ticks:", yrange - for x in self.xvals: - #path.moveTo(x, yrange[0]) - #path.lineTo(x, yrange[1]) - self.path.moveTo(x, 0.) - self.path.lineTo(x, 1.) - self.setPath(self.path) - self.valid = True - self.rescale() - #print " done..", self.boundingRect() - - def paint(self, *args): - if not self.valid: - self.rebuildTicks() - #print "Paint", self.boundingRect() - QtGui.QGraphicsPathItem.paint(self, *args) - - -class GridItem(UIGraphicsItem): - """Class used to make square grids in plots. NOT the grid used for running scanner sequences.""" - - def __init__(self, view, bounds=None, *args): - UIGraphicsItem.__init__(self, view, bounds) - #QtGui.QGraphicsItem.__init__(self, *args) - self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - self.picture = None - - - def viewRangeChanged(self): - self.picture = None - UIGraphicsItem.viewRangeChanged(self) - #self.update() - - def paint(self, p, opt, widget): - #p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) - #p.drawRect(self.boundingRect()) - - ## draw picture - if self.picture is None: - #print "no pic, draw.." - self.generatePicture() - p.drawPicture(0, 0, self.picture) - #print "drawing Grid." - - - def generatePicture(self): - self.picture = QtGui.QPicture() - p = QtGui.QPainter() - p.begin(self.picture) - - dt = self.viewTransform().inverted()[0] - vr = self.viewRect() - unit = self.unitRect() - dim = [vr.width(), vr.height()] - lvr = self.boundingRect() - ul = np.array([lvr.left(), lvr.top()]) - br = np.array([lvr.right(), lvr.bottom()]) - - texts = [] - - if ul[1] > br[1]: - x = ul[1] - ul[1] = br[1] - br[1] = x - for i in range(2, -1, -1): ## Draw three different scales of grid - - dist = br-ul - nlTarget = 10.**i - d = 10. ** np.floor(np.log10(abs(dist/nlTarget))+0.5) - ul1 = np.floor(ul / d) * d - br1 = np.ceil(br / d) * d - dist = br1-ul1 - nl = (dist / d) + 0.5 - for ax in range(0,2): ## Draw grid for both axes - ppl = dim[ax] / nl[ax] - c = np.clip(3.*(ppl-3), 0., 30.) - linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c)) - textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2)) - #linePen.setCosmetic(True) - #linePen.setWidth(1) - bx = (ax+1) % 2 - for x in range(0, int(nl[ax])): - linePen.setCosmetic(False) - if ax == 0: - linePen.setWidthF(self.pixelHeight()) - else: - linePen.setWidthF(self.pixelWidth()) - p.setPen(linePen) - p1 = np.array([0.,0.]) - p2 = np.array([0.,0.]) - p1[ax] = ul1[ax] + x * d[ax] - p2[ax] = p1[ax] - p1[bx] = ul[bx] - p2[bx] = br[bx] - p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1])) - if i < 2: - p.setPen(textPen) - if ax == 0: - x = p1[0] + unit.width() - y = ul[1] + unit.height() * 8. - else: - x = ul[0] + unit.width()*3 - y = p1[1] + unit.height() - texts.append((QtCore.QPointF(x, y), "%g"%p1[ax])) - tr = self.viewTransform() - tr.scale(1.5, 1.5) - p.setWorldTransform(tr.inverted()[0]) - for t in texts: - x = tr.map(t[0]) - p.drawText(x, t[1]) - p.end() - -class ScaleBar(UIGraphicsItem): - def __init__(self, view, size, width=5, color=(100, 100, 255)): - self.size = size - UIGraphicsItem.__init__(self, view) - self.setAcceptedMouseButtons(QtCore.Qt.NoButton) - #self.pen = QtGui.QPen(QtGui.QColor(*color)) - #self.pen.setWidth(width) - #self.pen.setCosmetic(True) - #self.pen2 = QtGui.QPen(QtGui.QColor(0,0,0)) - #self.pen2.setWidth(width+2) - #self.pen2.setCosmetic(True) - self.brush = QtGui.QBrush(QtGui.QColor(*color)) - self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) - self.width = width - - def paint(self, p, opt, widget): - rect = self.boundingRect() - unit = self.unitRect() - y = rect.bottom() + (rect.top()-rect.bottom()) * 0.02 - y1 = y + unit.height()*self.width - x = rect.right() + (rect.left()-rect.right()) * 0.02 - x1 = x - self.size - - - p.setPen(self.pen) - p.setBrush(self.brush) - rect = QtCore.QRectF( - QtCore.QPointF(x1, y1), - QtCore.QPointF(x, y) - ) - p.translate(x1, y1) - p.scale(rect.width(), rect.height()) - p.drawRect(0, 0, 1, 1) - - alpha = np.clip(((self.size/unit.width()) - 40.) * 255. / 80., 0, 255) - p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha))) - for i in range(1, 10): - #x2 = x + (x1-x) * 0.1 * i - x2 = 0.1 * i - p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1)) - - - def setSize(self, s): - self.size = s - -class ColorScaleBar(UIGraphicsItem): - def __init__(self, view, size, offset): - self.size = size - self.offset = offset - UIGraphicsItem.__init__(self, view) - self.setAcceptedMouseButtons(QtCore.Qt.NoButton) - self.brush = QtGui.QBrush(QtGui.QColor(200,0,0)) - self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) - self.labels = {'max': 1, 'min': 0} - self.gradient = QtGui.QLinearGradient() - self.gradient.setColorAt(0, QtGui.QColor(0,0,0)) - self.gradient.setColorAt(1, QtGui.QColor(255,0,0)) - - def setGradient(self, g): - self.gradient = g - self.update() - - def setIntColorScale(self, minVal, maxVal, *args, **kargs): - colors = [intColor(i, maxVal-minVal, *args, **kargs) for i in range(minVal, maxVal)] - g = QtGui.QLinearGradient() - for i in range(len(colors)): - x = float(i)/len(colors) - g.setColorAt(x, colors[i]) - self.setGradient(g) - if 'labels' not in kargs: - self.setLabels({str(minVal/10.): 0, str(maxVal): 1}) - else: - self.setLabels({kargs['labels'][0]:0, kargs['labels'][1]:1}) - - def setLabels(self, l): - """Defines labels to appear next to the color scale""" - self.labels = l - self.update() - - def paint(self, p, opt, widget): - rect = self.boundingRect() ## Boundaries of visible area in scene coords. - unit = self.unitRect() ## Size of one view pixel in scene coords. - - ## determine max width of all labels - labelWidth = 0 - labelHeight = 0 - for k in self.labels: - b = p.boundingRect(QtCore.QRectF(0, 0, 0, 0), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) - labelWidth = max(labelWidth, b.width()) - labelHeight = max(labelHeight, b.height()) - - labelWidth *= unit.width() - labelHeight *= unit.height() - - textPadding = 2 # in px - - if self.offset[0] < 0: - x3 = rect.right() + unit.width() * self.offset[0] - x2 = x3 - labelWidth - unit.width()*textPadding*2 - x1 = x2 - unit.width() * self.size[0] - else: - x1 = rect.left() + unit.width() * self.offset[0] - x2 = x1 + unit.width() * self.size[0] - x3 = x2 + labelWidth + unit.width()*textPadding*2 - if self.offset[1] < 0: - y2 = rect.top() - unit.height() * self.offset[1] - y1 = y2 + unit.height() * self.size[1] - else: - y1 = rect.bottom() - unit.height() * self.offset[1] - y2 = y1 - unit.height() * self.size[1] - self.b = [x1,x2,x3,y1,y2,labelWidth] - - ## Draw background - p.setPen(self.pen) - p.setBrush(QtGui.QBrush(QtGui.QColor(255,255,255,100))) - rect = QtCore.QRectF( - QtCore.QPointF(x1 - unit.width()*textPadding, y1 + labelHeight/2 + unit.height()*textPadding), - QtCore.QPointF(x3, y2 - labelHeight/2 - unit.height()*textPadding) - ) - p.drawRect(rect) - - - ## Have to scale painter so that text and gradients are correct size. Bleh. - p.scale(unit.width(), unit.height()) - - ## Draw color bar - self.gradient.setStart(0, y1/unit.height()) - self.gradient.setFinalStop(0, y2/unit.height()) - p.setBrush(self.gradient) - rect = QtCore.QRectF( - QtCore.QPointF(x1/unit.width(), y1/unit.height()), - QtCore.QPointF(x2/unit.width(), y2/unit.height()) - ) - p.drawRect(rect) - - - ## draw labels - p.setPen(QtGui.QPen(QtGui.QColor(0,0,0))) - tx = x2 + unit.width()*textPadding - lh = labelHeight/unit.height() - for k in self.labels: - y = y1 + self.labels[k] * (y2-y1) - p.drawText(QtCore.QRectF(tx/unit.width(), y/unit.height() - lh/2.0, 1000, lh), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) - - diff --git a/graphicsItems/ArrowItem.py b/graphicsItems/ArrowItem.py new file mode 100644 index 00000000..d9cf9663 --- /dev/null +++ b/graphicsItems/ArrowItem.py @@ -0,0 +1,60 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn + +__all__ = ['ArrowItem'] +class ArrowItem(QtGui.QGraphicsPolygonItem): + """ + For displaying scale-invariant arrows. + For arrows pointing to a location on a curve, see CurveArrow + + """ + + + def __init__(self, **opts): + QtGui.QGraphicsPolygonItem.__init__(self) + defOpts = { + 'style': 'tri', + 'pxMode': True, + 'size': 20, + 'angle': -150, + 'pos': (0,0), + 'width': 8, + 'tipAngle': 25, + 'baseAngle': 90, + 'pen': (200,200,200), + 'brush': (50,50,200), + } + defOpts.update(opts) + + self.setStyle(**defOpts) + + self.setPen(fn.mkPen(defOpts['pen'])) + self.setBrush(fn.mkBrush(defOpts['brush'])) + + self.rotate(self.opts['angle']) + self.moveBy(*self.opts['pos']) + + def setStyle(self, **opts): + self.opts = opts + + if opts['style'] == 'tri': + points = [ + QtCore.QPointF(0,0), + QtCore.QPointF(opts['size'],-opts['width']/2.), + QtCore.QPointF(opts['size'],opts['width']/2.), + ] + poly = QtGui.QPolygonF(points) + + else: + raise Exception("Unrecognized arrow style '%s'" % opts['style']) + + self.setPolygon(poly) + + if opts['pxMode']: + self.setFlags(self.flags() | self.ItemIgnoresTransformations) + else: + self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) + + def paint(self, p, *args): + p.setRenderHint(QtGui.QPainter.Antialiasing) + QtGui.QGraphicsPolygonItem.paint(self, p, *args) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py new file mode 100644 index 00000000..98faa152 --- /dev/null +++ b/graphicsItems/AxisItem.py @@ -0,0 +1,441 @@ +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +from pyqtgraph.Point import Point +import pyqtgraph.debug as debug +import weakref +import pyqtgraph.functions as fn +from GraphicsWidget import GraphicsWidget + +__all__ = ['AxisItem'] +class AxisItem(GraphicsWidget): + def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True): + """ + GraphicsItem showing a single plot axis with ticks, values, and label. + Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. + Ticks can be extended to make a grid. + """ + + + GraphicsWidget.__init__(self, parent) + self.label = QtGui.QGraphicsTextItem(self) + self.showValues = showValues + self.orientation = orientation + if orientation not in ['left', 'right', 'top', 'bottom']: + raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") + if orientation in ['left', 'right']: + #self.setMinimumWidth(25) + #self.setSizePolicy(QtGui.QSizePolicy( + #QtGui.QSizePolicy.Minimum, + #QtGui.QSizePolicy.Expanding + #)) + self.label.rotate(-90) + #else: + #self.setMinimumHeight(50) + #self.setSizePolicy(QtGui.QSizePolicy( + #QtGui.QSizePolicy.Expanding, + #QtGui.QSizePolicy.Minimum + #)) + #self.drawLabel = False + + self.labelText = '' + self.labelUnits = '' + self.labelUnitPrefix='' + self.labelStyle = {'color': '#CCC'} + + self.textHeight = 18 + self.tickLength = maxTickLength + self.scale = 1.0 + self.autoScale = True + + self.setRange(0, 1) + + if pen is None: + pen = QtGui.QPen(QtGui.QColor(100, 100, 100)) + self.setPen(pen) + + self.linkedView = None + if linkView is not None: + self.linkToView(linkView) + + self.showLabel(False) + + self.grid = False + #self.setCacheMode(self.DeviceCoordinateCache) + + def close(self): + self.scene().removeItem(self.label) + self.label = None + self.scene().removeItem(self) + + def setGrid(self, grid): + """Set the alpha value for the grid, or False to disable.""" + self.grid = grid + self.update() + + + def resizeEvent(self, ev=None): + #s = self.size() + + ## Set the position of the label + nudge = 5 + br = self.label.boundingRect() + p = QtCore.QPointF(0, 0) + if self.orientation == 'left': + p.setY(int(self.size().height()/2 + br.width()/2)) + p.setX(-nudge) + #s.setWidth(10) + elif self.orientation == 'right': + #s.setWidth(10) + p.setY(int(self.size().height()/2 + br.width()/2)) + p.setX(int(self.size().width()-br.height()+nudge)) + elif self.orientation == 'top': + #s.setHeight(10) + p.setY(-nudge) + p.setX(int(self.size().width()/2. - br.width()/2.)) + elif self.orientation == 'bottom': + p.setX(int(self.size().width()/2. - br.width()/2.)) + #s.setHeight(10) + p.setY(int(self.size().height()-br.height()+nudge)) + #self.label.resize(s) + self.label.setPos(p) + + def showLabel(self, show=True): + #self.drawLabel = show + self.label.setVisible(show) + if self.orientation in ['left', 'right']: + self.setWidth() + else: + self.setHeight() + if self.autoScale: + self.setScale() + + def setLabel(self, text=None, units=None, unitPrefix=None, **args): + if text is not None: + self.labelText = text + self.showLabel() + if units is not None: + self.labelUnits = units + self.showLabel() + if unitPrefix is not None: + self.labelUnitPrefix = unitPrefix + if len(args) > 0: + self.labelStyle = args + self.label.setHtml(self.labelString()) + self.resizeEvent() + self.update() + + def labelString(self): + if self.labelUnits == '': + if self.scale == 1.0: + units = '' + else: + units = u'(x%g)' % (1.0/self.scale) + else: + #print repr(self.labelUnitPrefix), repr(self.labelUnits) + units = u'(%s%s)' % (self.labelUnitPrefix, self.labelUnits) + + s = u'%s %s' % (self.labelText, units) + + style = ';'.join(['%s: "%s"' % (k, self.labelStyle[k]) for k in self.labelStyle]) + + return u"%s" % (style, s) + + def setHeight(self, h=None): + if h is None: + h = self.textHeight + max(0, self.tickLength) + if self.label.isVisible(): + h += self.textHeight + self.setMaximumHeight(h) + self.setMinimumHeight(h) + + + def setWidth(self, w=None): + if w is None: + w = max(0, self.tickLength) + 40 + if self.label.isVisible(): + w += self.textHeight + self.setMaximumWidth(w) + self.setMinimumWidth(w) + + def setPen(self, pen): + self.pen = pen + self.update() + + def setScale(self, scale=None): + """ + Set the value scaling for this axis. + The scaling value 1) multiplies the values displayed along the axis + and 2) changes the way units are displayed in the label. + For example: + If the axis spans values from -0.1 to 0.1 and has units set to 'V' + then a scale of 1000 would cause the axis to display values -100 to 100 + and the units would appear as 'mV' + If scale is None, then it will be determined automatically based on the current + range displayed by the axis. + """ + if scale is None: + #if self.drawLabel: ## If there is a label, then we are free to rescale the values + if self.label.isVisible(): + d = self.range[1] - self.range[0] + #pl = 1-int(log10(d)) + #scale = 10 ** pl + (scale, prefix) = fn.siScale(d / 2.) + if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. + scale = 1.0 + prefix = '' + self.setLabel(unitPrefix=prefix) + else: + scale = 1.0 + + + if scale != self.scale: + self.scale = scale + self.setLabel() + self.update() + + def setRange(self, mn, mx): + if mn in [np.nan, np.inf, -np.inf] or mx in [np.nan, np.inf, -np.inf]: + raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx))) + self.range = [mn, mx] + if self.autoScale: + self.setScale() + self.update() + + def linkToView(self, view): + if self.orientation in ['right', 'left']: + if self.linkedView is not None and self.linkedView() is not None: + #view.sigYRangeChanged.disconnect(self.linkedViewChanged) + ## should be this instead? + self.linkedView().sigYRangeChanged.disconnect(self.linkedViewChanged) + self.linkedView = weakref.ref(view) + view.sigYRangeChanged.connect(self.linkedViewChanged) + #signal = QtCore.SIGNAL('yRangeChanged') + else: + if self.linkedView is not None and self.linkedView() is not None: + #view.sigYRangeChanged.disconnect(self.linkedViewChanged) + ## should be this instead? + self.linkedView().sigXRangeChanged.disconnect(self.linkedViewChanged) + self.linkedView = weakref.ref(view) + view.sigXRangeChanged.connect(self.linkedViewChanged) + #signal = QtCore.SIGNAL('xRangeChanged') + + + def linkedViewChanged(self, view, newRange): + self.setRange(*newRange) + + def boundingRect(self): + if self.linkedView is None or self.linkedView() is None or self.grid is False: + rect = self.mapRectFromParent(self.geometry()) + ## extend rect if ticks go in negative direction + if self.orientation == 'left': + rect.setRight(rect.right() - min(0,self.tickLength)) + elif self.orientation == 'right': + rect.setLeft(rect.left() + min(0,self.tickLength)) + elif self.orientation == 'top': + rect.setBottom(rect.bottom() - min(0,self.tickLength)) + elif self.orientation == 'bottom': + rect.setTop(rect.top() + min(0,self.tickLength)) + return rect + else: + return self.mapRectFromParent(self.geometry()) | self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) + + def paint(self, p, opt, widget): + prof = debug.Profiler("AxisItem.paint", disabled=True) + p.setPen(self.pen) + + #bounds = self.boundingRect() + bounds = self.mapRectFromParent(self.geometry()) + + if self.linkedView is None or self.linkedView() is None or self.grid is False: + tbounds = bounds + else: + tbounds = self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) + + if self.orientation == 'left': + span = (bounds.topRight(), bounds.bottomRight()) + tickStart = tbounds.right() + tickStop = bounds.right() + tickDir = -1 + axis = 0 + elif self.orientation == 'right': + span = (bounds.topLeft(), bounds.bottomLeft()) + tickStart = tbounds.left() + tickStop = bounds.left() + tickDir = 1 + axis = 0 + elif self.orientation == 'top': + span = (bounds.bottomLeft(), bounds.bottomRight()) + tickStart = tbounds.bottom() + tickStop = bounds.bottom() + tickDir = -1 + axis = 1 + elif self.orientation == 'bottom': + span = (bounds.topLeft(), bounds.topRight()) + tickStart = tbounds.top() + tickStop = bounds.top() + tickDir = 1 + axis = 1 + + ## draw long line along axis + p.drawLine(*span) + + ## determine size of this item in pixels + points = map(self.mapToDevice, span) + lengthInPixels = Point(points[1] - points[0]).length() + + ## decide optimal tick spacing in pixels + pixelSpacing = np.log(lengthInPixels+10) * 3 + optimalTickCount = lengthInPixels / pixelSpacing + + ## Determine optimal tick spacing + #intervals = [1., 2., 5., 10., 20., 50.] + #intervals = [1., 2.5, 5., 10., 25., 50.] + intervals = np.array([0.1, 0.2, 1., 2., 10., 20., 100., 200.]) + dif = abs(self.range[1] - self.range[0]) + if dif == 0.0: + return + pw = 10 ** (np.floor(np.log10(dif))-1) + scaledIntervals = intervals * pw + scaledTickCounts = dif / scaledIntervals + i1 = np.argwhere(scaledTickCounts < optimalTickCount)[0,0] + + distBetweenIntervals = (optimalTickCount-scaledTickCounts[i1]) / (scaledTickCounts[i1-1]-scaledTickCounts[i1]) + + #print optimalTickCount, i1, scaledIntervals, distBetweenIntervals + + #for i in range(len(intervals)): + #i1 = i + #if dif / (pw*intervals[i]) < 10: + #break + + textLevel = 0 ## draw text at this scale level + + #print "range: %s dif: %f power: %f interval: %f spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp) + + #print " start at %f, %d ticks" % (start, num) + + + if axis == 0: + xs = -bounds.height() / dif + else: + xs = bounds.width() / dif + + prof.mark('init') + + tickPositions = set() # remembers positions of previously drawn ticks + ## draw ticks and generate list of texts to draw + ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) + ## draw three different intervals, long ticks first + texts = [] + for i in [2,1,0]: + if i1+i > len(intervals): + continue + ## spacing for this interval + sp = pw*intervals[i1+i] + + ## determine starting tick + start = np.ceil(self.range[0] / sp) * sp + + ## determine number of ticks + num = int(dif / sp) + 1 + + ## last tick value + last = start + sp * num + + ## Number of decimal places to print + maxVal = max(abs(start), abs(last)) + places = max(0, 1-int(np.log10(sp*self.scale))) + + ## length of tick + #h = np.clip((self.tickLength*3 / num) - 1., min(0, self.tickLength), max(0, self.tickLength)) + if i == 0: + h = self.tickLength * distBetweenIntervals / 2. + else: + h = self.tickLength*i/2. + + ## alpha + if i == 0: + #a = min(255, (765. / num) - 1.) + a = 255 * distBetweenIntervals + else: + a = 255 + + if axis == 0: + offset = self.range[0] * xs - bounds.height() + else: + offset = self.range[0] * xs + + for j in range(num): + v = start + sp * j + x = (v * xs) - offset + p1 = [0, 0] + p2 = [0, 0] + p1[axis] = tickStart + p2[axis] = tickStop + h*tickDir + p1[1-axis] = p2[1-axis] = x + + if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]: + continue + if p1[1-axis] < 0: + continue + p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a))) + # draw tick only if there is none + tickPos = p1[1-axis] + if tickPos not in tickPositions: + p.drawLine(Point(p1), Point(p2)) + tickPositions.add(tickPos) + if i >= textLevel: + if abs(v) < .001 or abs(v) >= 10000: + vstr = "%g" % (v * self.scale) + else: + vstr = ("%%0.%df" % places) % (v * self.scale) + + textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + height = textRect.height() + self.textHeight = height + if self.orientation == 'left': + textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height) + elif self.orientation == 'right': + textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height) + elif self.orientation == 'top': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height) + elif self.orientation == 'bottom': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) + + #p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a))) + #p.drawText(rect, textFlags, vstr) + texts.append((rect, textFlags, vstr, a)) + + prof.mark('draw ticks') + for args in texts: + p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, args[3]))) + p.drawText(*args[:3]) + prof.mark('draw text') + prof.finish() + + def show(self): + + if self.orientation in ['left', 'right']: + self.setWidth() + else: + self.setHeight() + GraphicsWidget.show(self) + + def hide(self): + if self.orientation in ['left', 'right']: + self.setWidth(0) + else: + self.setHeight(0) + GraphicsWidget.hide(self) + + def wheelEvent(self, ev): + if self.linkedView is None or self.linkedView() is None: return + if self.orientation in ['left', 'right']: + self.linkedView().wheelEvent(ev, axis=1) + else: + self.linkedView().wheelEvent(ev, axis=0) + ev.accept() diff --git a/graphicsItems/ButtonItem.py b/graphicsItems/ButtonItem.py new file mode 100644 index 00000000..2de8cfdc --- /dev/null +++ b/graphicsItems/ButtonItem.py @@ -0,0 +1,51 @@ +from pyqtgraph.Qt import QtGui, QtCore +from GraphicsObject import GraphicsObject + +__all__ = ['ButtonItem'] +class ButtonItem(GraphicsObject): + """Button graphicsItem displaying an image.""" + + clicked = QtCore.Signal(object) + + def __init__(self, imageFile, width=None, parentItem=None): + self.enabled = True + GraphicsObject.__init__(self) + self.setImageFile(imageFile) + if width is not None: + s = float(width) / self.pixmap.width() + self.scale(s, s) + if parentItem is not None: + self.setParentItem(parentItem) + self.setOpacity(0.7) + + def setImageFile(self, imageFile): + self.pixmap = QtGui.QPixmap(imageFile) + self.update() + + def mouseClickEvent(self, ev): + if self.enabled: + self.clicked.emit(self) + + def mouseHoverEvent(self, ev): + if not self.enabled: + return + if ev.isEnter(): + self.setOpacity(1.0) + else: + self.setOpacity(0.7) + + def disable(self): + self.enabled = False + self.setOpacity(0.4) + + def enable(self): + self.enabled = True + self.setOpacity(0.7) + + def paint(self, p, *args): + p.setRenderHint(p.Antialiasing) + p.drawPixmap(0, 0, self.pixmap) + + def boundingRect(self): + return QtCore.QRectF(self.pixmap.rect()) + diff --git a/graphicsItems/CurvePoint.py b/graphicsItems/CurvePoint.py new file mode 100644 index 00000000..a1ec5ae4 --- /dev/null +++ b/graphicsItems/CurvePoint.py @@ -0,0 +1,113 @@ +from pyqtgraph.Qt import QtGui, QtCore +import ArrowItem +import numpy as np +from pyqtgraph.Point import Point +import weakref +from GraphicsObject import GraphicsObject + +__all__ = ['CurvePoint', 'CurveArrow'] +class CurvePoint(GraphicsObject): + """A GraphicsItem that sets its location to a point on a PlotCurveItem. + Also rotates to be tangent to the curve. + The position along the curve is a Qt property, and thus can be easily animated. + + Note: This class does not display anything; see CurveArrow for an applied example + """ + + def __init__(self, curve, index=0, pos=None): + """Position can be set either as an index referring to the sample number or + the position 0.0 - 1.0""" + + GraphicsObject.__init__(self) + #QObjectWorkaround.__init__(self) + self.curve = weakref.ref(curve) + self.setParentItem(curve) + self.setProperty('position', 0.0) + self.setProperty('index', 0) + + if hasattr(self, 'ItemHasNoContents'): + self.setFlags(self.flags() | self.ItemHasNoContents) + + if pos is not None: + self.setPos(pos) + else: + self.setIndex(index) + + def setPos(self, pos): + self.setProperty('position', float(pos))## cannot use numpy types here, MUST be python float. + + def setIndex(self, index): + self.setProperty('index', int(index)) ## cannot use numpy types here, MUST be python int. + + def event(self, ev): + if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve() is None: + return False + + if ev.propertyName() == 'index': + index = self.property('index') + if 'QVariant' in repr(index): + index = index.toInt()[0] + elif ev.propertyName() == 'position': + index = None + else: + return False + + (x, y) = self.curve().getData() + if index is None: + #print ev.propertyName(), self.property('position').toDouble()[0], self.property('position').typeName() + pos = self.property('position') + if 'QVariant' in repr(pos): ## need to support 2 APIs :( + pos = pos.toDouble()[0] + index = (len(x)-1) * np.clip(pos, 0.0, 1.0) + + if index != int(index): ## interpolate floating-point values + i1 = int(index) + i2 = np.clip(i1+1, 0, len(x)-1) + s2 = index-i1 + s1 = 1.0-s2 + newPos = (x[i1]*s1+x[i2]*s2, y[i1]*s1+y[i2]*s2) + else: + index = int(index) + i1 = np.clip(index-1, 0, len(x)-1) + i2 = np.clip(index+1, 0, len(x)-1) + newPos = (x[index], y[index]) + + p1 = self.parentItem().mapToScene(QtCore.QPointF(x[i1], y[i1])) + p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2])) + ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) ## returns radians + self.resetTransform() + self.rotate(180+ ang * 180 / np.pi) ## takes degrees + QtGui.QGraphicsItem.setPos(self, *newPos) + return True + + def boundingRect(self): + return QtCore.QRectF() + + def paint(self, *args): + pass + + def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1): + anim = QtCore.QPropertyAnimation(self, prop) + anim.setDuration(duration) + anim.setStartValue(start) + anim.setEndValue(end) + anim.setLoopCount(loop) + return anim + + +class CurveArrow(CurvePoint): + """Provides an arrow that points to any specific sample on a PlotCurveItem. + Provides properties that can be animated.""" + + def __init__(self, curve, index=0, pos=None, **opts): + CurvePoint.__init__(self, curve, index=index, pos=pos) + if opts.get('pxMode', True): + opts['pxMode'] = False + self.setFlags(self.flags() | self.ItemIgnoresTransformations) + opts['angle'] = 0 + self.arrow = ArrowItem.ArrowItem(**opts) + self.arrow.setParentItem(self) + + def setStyle(**opts): + return self.arrow.setStyle(**opts) + diff --git a/graphicsItems/GradientEditorItem.py b/graphicsItems/GradientEditorItem.py new file mode 100644 index 00000000..1ae2e4cc --- /dev/null +++ b/graphicsItems/GradientEditorItem.py @@ -0,0 +1,624 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +from GraphicsObject import GraphicsObject +from GraphicsWidget import GraphicsWidget +import weakref, collections +import numpy as np + +__all__ = ['TickSliderItem', 'GradientEditorItem'] + + +Gradients = collections.OrderedDict([ + ('thermal', {'ticks': [(0.3333, (185, 0, 0, 255)), (0.6666, (255, 220, 0, 255)), (1, (255, 255, 255, 255)), (0, (0, 0, 0, 255))], 'mode': 'rgb'}), + ('flame', {'ticks': [(0.2, (7, 0, 220, 255)), (0.5, (236, 0, 134, 255)), (0.8, (246, 246, 0, 255)), (1.0, (255, 255, 255, 255)), (0.0, (0, 0, 0, 255))], 'mode': 'rgb'}), + ('yellowy', {'ticks': [(0.0, (0, 0, 0, 255)), (0.2328863796753704, (32, 0, 129, 255)), (0.8362738179251941, (255, 255, 0, 255)), (0.5257586450247, (115, 15, 255, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'} ), + ('bipolar', {'ticks': [(0.0, (0, 255, 255, 255)), (1.0, (255, 255, 0, 255)), (0.5, (0, 0, 0, 255)), (0.25, (0, 0, 255, 255)), (0.75, (255, 0, 0, 255))], 'mode': 'rgb'}), + ('spectrum', {'ticks': [(1.0, (255, 0, 255, 255)), (0.0, (255, 0, 0, 255))], 'mode': 'hsv'}), + ('cyclic', {'ticks': [(0.0, (255, 0, 4, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'hsv'}), + ('greyclip', {'ticks': [(0.0, (0, 0, 0, 255)), (0.99, (255, 255, 255, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'rgb'}), + ('grey', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'}), +]) + + +class TickSliderItem(GraphicsWidget): + + def __init__(self, orientation='bottom', allowAdd=True, **kargs): + GraphicsWidget.__init__(self) + self.orientation = orientation + self.length = 100 + self.tickSize = 15 + self.ticks = {} + self.maxDim = 20 + self.allowAdd = allowAdd + if 'tickPen' in kargs: + self.tickPen = fn.mkPen(kargs['tickPen']) + else: + self.tickPen = fn.mkPen('w') + + self.orientations = { + 'left': (90, 1, 1), + 'right': (90, 1, 1), + 'top': (0, 1, -1), + 'bottom': (0, 1, 1) + } + + self.setOrientation(orientation) + #self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) + #self.setBackgroundRole(QtGui.QPalette.NoRole) + #self.setMouseTracking(True) + + #def boundingRect(self): + #return self.mapRectFromParent(self.geometry()).normalized() + + #def shape(self): ## No idea why this is necessary, but rotated items do not receive clicks otherwise. + #p = QtGui.QPainterPath() + #p.addRect(self.boundingRect()) + #return p + + def paint(self, p, opt, widget): + #p.setPen(fn.mkPen('g', width=3)) + #p.drawRect(self.boundingRect()) + return + + def keyPressEvent(self, ev): + ev.ignore() + + def setMaxDim(self, mx=None): + if mx is None: + mx = self.maxDim + else: + self.maxDim = mx + + if self.orientation in ['bottom', 'top']: + self.setFixedHeight(mx) + self.setMaximumWidth(16777215) + else: + self.setFixedWidth(mx) + self.setMaximumHeight(16777215) + + + def setOrientation(self, ort): + self.orientation = ort + self.setMaxDim() + self.resetTransform() + if ort == 'top': + self.scale(1, -1) + self.translate(0, -self.height()) + elif ort == 'left': + self.rotate(270) + self.scale(1, -1) + self.translate(-self.height(), -self.maxDim) + elif ort == 'right': + self.rotate(270) + self.translate(-self.height(), 0) + #self.setPos(0, -self.height()) + + self.translate(self.tickSize/2., 0) + + def addTick(self, x, color=None, movable=True): + if color is None: + color = QtGui.QColor(255,255,255) + tick = Tick(self, [x*self.length, 0], color, movable, self.tickSize, pen=self.tickPen) + self.ticks[tick] = x + tick.setParentItem(self) + return tick + + def removeTick(self, tick): + del self.ticks[tick] + tick.setParentItem(None) + if self.scene() is not None: + self.scene().removeItem(tick) + + def tickMoved(self, tick, pos): + #print "tick changed" + ## Correct position of tick if it has left bounds. + newX = min(max(0, pos.x()), self.length) + pos.setX(newX) + tick.setPos(pos) + self.ticks[tick] = float(newX) / self.length + + def tickClicked(self, tick, ev): + if ev.button() == QtCore.Qt.RightButton: + self.removeTick(tick) + + def widgetLength(self): + if self.orientation in ['bottom', 'top']: + return self.width() + else: + return self.height() + + def resizeEvent(self, ev): + wlen = max(40, self.widgetLength()) + self.setLength(wlen-self.tickSize) + self.setOrientation(self.orientation) + #bounds = self.scene().itemsBoundingRect() + #bounds.setLeft(min(-self.tickSize*0.5, bounds.left())) + #bounds.setRight(max(self.length + self.tickSize, bounds.right())) + #self.setSceneRect(bounds) + #self.fitInView(bounds, QtCore.Qt.KeepAspectRatio) + + def setLength(self, newLen): + for t, x in self.ticks.items(): + t.setPos(x * newLen, t.pos().y()) + self.length = float(newLen) + + #def mousePressEvent(self, ev): + #QtGui.QGraphicsView.mousePressEvent(self, ev) + #self.ignoreRelease = False + #for i in self.items(ev.pos()): + #if isinstance(i, Tick): + #self.ignoreRelease = True + #break + ##if len(self.items(ev.pos())) > 0: ## Let items handle their own clicks + ##self.ignoreRelease = True + + #def mouseReleaseEvent(self, ev): + #QtGui.QGraphicsView.mouseReleaseEvent(self, ev) + #if self.ignoreRelease: + #return + + #pos = self.mapToScene(ev.pos()) + + #if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: + #if pos.x() < 0 or pos.x() > self.length: + #return + #if pos.y() < 0 or pos.y() > self.tickSize: + #return + #pos.setX(min(max(pos.x(), 0), self.length)) + #self.addTick(pos.x()/self.length) + #elif ev.button() == QtCore.Qt.RightButton: + #self.showMenu(ev) + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: + pos = ev.pos() + if pos.x() < 0 or pos.x() > self.length: + return + if pos.y() < 0 or pos.y() > self.tickSize: + return + pos.setX(min(max(pos.x(), 0), self.length)) + self.addTick(pos.x()/self.length) + elif ev.button() == QtCore.Qt.RightButton: + self.showMenu(ev) + + #if ev.button() == QtCore.Qt.RightButton: + #if self.moving: + #ev.accept() + #self.setPos(self.startPosition) + #self.moving = False + #self.sigMoving.emit(self) + #self.sigMoved.emit(self) + #else: + #pass + #self.view().tickClicked(self, ev) + ###remove + + def hoverEvent(self, ev): + if (not ev.isExit()) and ev.acceptClicks(QtCore.Qt.LeftButton): + ev.acceptClicks(QtCore.Qt.RightButton) + ## show ghost tick + #self.currentPen = fn.mkPen(255, 0,0) + #else: + #self.currentPen = self.pen + #self.update() + + def showMenu(self, ev): + pass + + def setTickColor(self, tick, color): + tick = self.getTick(tick) + tick.color = color + tick.update() + #tick.setBrush(QtGui.QBrush(QtGui.QColor(tick.color))) + + def setTickValue(self, tick, val): + tick = self.getTick(tick) + val = min(max(0.0, val), 1.0) + x = val * self.length + pos = tick.pos() + pos.setX(x) + tick.setPos(pos) + self.ticks[tick] = val + + def tickValue(self, tick): + tick = self.getTick(tick) + return self.ticks[tick] + + def getTick(self, tick): + if type(tick) is int: + tick = self.listTicks()[tick][0] + return tick + + #def mouseMoveEvent(self, ev): + #QtGui.QGraphicsView.mouseMoveEvent(self, ev) + + def listTicks(self): + ticks = self.ticks.items() + ticks.sort(lambda a,b: cmp(a[1], b[1])) + return ticks + + +class GradientEditorItem(TickSliderItem): + + sigGradientChanged = QtCore.Signal(object) + + def __init__(self, *args, **kargs): + + self.currentTick = None + self.currentTickColor = None + self.rectSize = 15 + self.gradRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, self.rectSize, 100, self.rectSize)) + self.backgroundRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize)) + self.backgroundRect.setBrush(QtGui.QBrush(QtCore.Qt.DiagCrossPattern)) + self.colorMode = 'rgb' + + TickSliderItem.__init__(self, *args, **kargs) + + self.colorDialog = QtGui.QColorDialog() + self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) + self.colorDialog.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) + + self.colorDialog.currentColorChanged.connect(self.currentColorChanged) + self.colorDialog.rejected.connect(self.currentColorRejected) + + self.backgroundRect.setParentItem(self) + self.gradRect.setParentItem(self) + + self.setMaxDim(self.rectSize + self.tickSize) + + self.rgbAction = QtGui.QAction('RGB', self) + self.rgbAction.setCheckable(True) + self.rgbAction.triggered.connect(lambda: self.setColorMode('rgb')) + self.hsvAction = QtGui.QAction('HSV', self) + self.hsvAction.setCheckable(True) + self.hsvAction.triggered.connect(lambda: self.setColorMode('hsv')) + + self.menu = QtGui.QMenu() + + ## build context menu of gradients + l = self.length + self.length = 100 + global Gradients + for g in Gradients: + px = QtGui.QPixmap(100, 15) + p = QtGui.QPainter(px) + self.restoreState(Gradients[g]) + grad = self.getGradient() + brush = QtGui.QBrush(grad) + p.fillRect(QtCore.QRect(0, 0, 100, 15), brush) + p.end() + label = QtGui.QLabel() + label.setPixmap(px) + label.setContentsMargins(1, 1, 1, 1) + act = QtGui.QWidgetAction(self) + act.setDefaultWidget(label) + act.triggered.connect(self.contextMenuClicked) + act.name = g + self.menu.addAction(act) + self.length = l + self.menu.addSeparator() + self.menu.addAction(self.rgbAction) + self.menu.addAction(self.hsvAction) + + + for t in self.ticks.keys(): + self.removeTick(t) + self.addTick(0, QtGui.QColor(0,0,0), True) + self.addTick(1, QtGui.QColor(255,0,0), True) + self.setColorMode('rgb') + self.updateGradient() + + def setOrientation(self, ort): + TickSliderItem.setOrientation(self, ort) + self.translate(0, self.rectSize) + + def showMenu(self, ev): + self.menu.popup(ev.screenPos().toQPoint()) + + def contextMenuClicked(self, b): + global Gradients + act = self.sender() + self.loadPreset(act.name) + + def loadPreset(self, name): + self.restoreState(Gradients[name]) + + def setColorMode(self, cm): + if cm not in ['rgb', 'hsv']: + raise Exception("Unknown color mode %s. Options are 'rgb' and 'hsv'." % str(cm)) + + try: + self.rgbAction.blockSignals(True) + self.hsvAction.blockSignals(True) + self.rgbAction.setChecked(cm == 'rgb') + self.hsvAction.setChecked(cm == 'hsv') + finally: + self.rgbAction.blockSignals(False) + self.hsvAction.blockSignals(False) + self.colorMode = cm + self.updateGradient() + + def updateGradient(self): + self.gradient = self.getGradient() + self.gradRect.setBrush(QtGui.QBrush(self.gradient)) + self.sigGradientChanged.emit(self) + + def setLength(self, newLen): + TickSliderItem.setLength(self, newLen) + self.backgroundRect.setRect(0, -self.rectSize, newLen, self.rectSize) + self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize) + self.updateGradient() + + def currentColorChanged(self, color): + if color.isValid() and self.currentTick is not None: + self.setTickColor(self.currentTick, color) + self.updateGradient() + + def currentColorRejected(self): + self.setTickColor(self.currentTick, self.currentTickColor) + self.updateGradient() + + def tickClicked(self, tick, ev): + if ev.button() == QtCore.Qt.LeftButton: + if not tick.colorChangeAllowed: + return + self.currentTick = tick + self.currentTickColor = tick.color + self.colorDialog.setCurrentColor(tick.color) + self.colorDialog.open() + #color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) + #if color.isValid(): + #self.setTickColor(tick, color) + #self.updateGradient() + elif ev.button() == QtCore.Qt.RightButton: + if not tick.removeAllowed: + return + if len(self.ticks) > 2: + self.removeTick(tick) + self.updateGradient() + + def tickMoved(self, tick, pos): + TickSliderItem.tickMoved(self, tick, pos) + self.updateGradient() + + + def getGradient(self): + g = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(self.length,0)) + if self.colorMode == 'rgb': + ticks = self.listTicks() + g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks]) + elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop + ticks = self.listTicks() + stops = [] + stops.append((ticks[0][1], ticks[0][0].color)) + for i in range(1,len(ticks)): + x1 = ticks[i-1][1] + x2 = ticks[i][1] + dx = (x2-x1) / 10. + for j in range(1,10): + x = x1 + dx*j + stops.append((x, self.getColor(x))) + stops.append((x2, self.getColor(x2))) + g.setStops(stops) + return g + + def getColor(self, x, toQColor=True): + ticks = self.listTicks() + if x <= ticks[0][1]: + c = ticks[0][0].color + if toQColor: + return QtGui.QColor(c) # always copy colors before handing them out + else: + return (c.red(), c.green(), c.blue(), c.alpha()) + if x >= ticks[-1][1]: + c = ticks[-1][0].color + if toQColor: + return QtGui.QColor(c) # always copy colors before handing them out + else: + return (c.red(), c.green(), c.blue(), c.alpha()) + + x2 = ticks[0][1] + for i in range(1,len(ticks)): + x1 = x2 + x2 = ticks[i][1] + if x1 <= x and x2 >= x: + break + + dx = (x2-x1) + if dx == 0: + f = 0. + else: + f = (x-x1) / dx + c1 = ticks[i-1][0].color + c2 = ticks[i][0].color + if self.colorMode == 'rgb': + r = c1.red() * (1.-f) + c2.red() * f + g = c1.green() * (1.-f) + c2.green() * f + b = c1.blue() * (1.-f) + c2.blue() * f + a = c1.alpha() * (1.-f) + c2.alpha() * f + if toQColor: + return QtGui.QColor(r, g, b,a) + else: + return (r,g,b,a) + elif self.colorMode == 'hsv': + h1,s1,v1,_ = c1.getHsv() + h2,s2,v2,_ = c2.getHsv() + h = h1 * (1.-f) + h2 * f + s = s1 * (1.-f) + s2 * f + v = v1 * (1.-f) + v2 * f + c = QtGui.QColor() + c.setHsv(h,s,v) + if toQColor: + return c + else: + return (c.red(), c.green(), c.blue(), c.alpha()) + + def getLookupTable(self, nPts, alpha=True): + """Return an RGB/A lookup table.""" + if alpha: + table = np.empty((nPts,4), dtype=np.ubyte) + else: + table = np.empty((nPts,3), dtype=np.ubyte) + + for i in range(nPts): + x = float(i)/(nPts-1) + color = self.getColor(x, toQColor=False) + table[i] = color[:table.shape[1]] + + return table + + + + def mouseReleaseEvent(self, ev): + TickSliderItem.mouseReleaseEvent(self, ev) + self.updateGradient() + + def addTick(self, x, color=None, movable=True): + if color is None: + color = self.getColor(x) + t = TickSliderItem.addTick(self, x, color=color, movable=movable) + t.colorChangeAllowed = True + t.removeAllowed = True + return t + + def saveState(self): + ticks = [] + for t in self.ticks: + c = t.color + ticks.append((self.ticks[t], (c.red(), c.green(), c.blue(), c.alpha()))) + state = {'mode': self.colorMode, 'ticks': ticks} + return state + + def restoreState(self, state): + self.setColorMode(state['mode']) + for t in self.ticks.keys(): + self.removeTick(t) + for t in state['ticks']: + c = QtGui.QColor(*t[1]) + self.addTick(t[0], c) + self.updateGradient() + + +class Tick(GraphicsObject): + + sigMoving = QtCore.Signal(object) + sigMoved = QtCore.Signal(object) + + def __init__(self, view, pos, color, movable=True, scale=10, pen='w'): + self.movable = movable + self.moving = False + self.view = weakref.ref(view) + self.scale = scale + self.color = color + self.pen = fn.mkPen(pen) + self.hoverPen = fn.mkPen(255,255,0) + self.currentPen = self.pen + self.pg = QtGui.QPainterPath(QtCore.QPointF(0,0)) + self.pg.lineTo(QtCore.QPointF(-scale/3**0.5, scale)) + self.pg.lineTo(QtCore.QPointF(scale/3**0.5, scale)) + self.pg.closeSubpath() + + GraphicsObject.__init__(self) + self.setPos(pos[0], pos[1]) + if self.movable: + self.setZValue(1) + else: + self.setZValue(0) + + def boundingRect(self): + return self.pg.boundingRect() + + def shape(self): + return self.pg + + def paint(self, p, *args): + p.setRenderHints(QtGui.QPainter.Antialiasing) + p.fillPath(self.pg, fn.mkBrush(self.color)) + + p.setPen(self.currentPen) + p.drawPath(self.pg) + + + def mouseDragEvent(self, ev): + if self.movable and ev.button() == QtCore.Qt.LeftButton: + if ev.isStart(): + self.moving = True + self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) + self.startPosition = self.pos() + ev.accept() + + if not self.moving: + return + + newPos = self.cursorOffset + self.mapToParent(ev.pos()) + newPos.setY(self.pos().y()) + + self.setPos(newPos) + self.view().tickMoved(self, newPos) + self.sigMoving.emit(self) + if ev.isFinish(): + self.moving = False + self.sigMoved.emit(self) + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton and self.moving: + ev.accept() + self.setPos(self.startPosition) + self.view().tickMoved(self, self.startPosition) + self.moving = False + self.sigMoving.emit(self) + self.sigMoved.emit(self) + else: + self.view().tickClicked(self, ev) + ##remove + + def hoverEvent(self, ev): + if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): + ev.acceptClicks(QtCore.Qt.LeftButton) + ev.acceptClicks(QtCore.Qt.RightButton) + self.currentPen = self.hoverPen + else: + self.currentPen = self.pen + self.update() + + #def mouseMoveEvent(self, ev): + ##print self, "move", ev.scenePos() + #if not self.movable: + #return + #if not ev.buttons() & QtCore.Qt.LeftButton: + #return + + + #newPos = ev.scenePos() + self.mouseOffset + #newPos.setY(self.pos().y()) + ##newPos.setX(min(max(newPos.x(), 0), 100)) + #self.setPos(newPos) + #self.view().tickMoved(self, newPos) + #self.movedSincePress = True + ##self.emit(QtCore.SIGNAL('tickChanged'), self) + #ev.accept() + + #def mousePressEvent(self, ev): + #self.movedSincePress = False + #if ev.button() == QtCore.Qt.LeftButton: + #ev.accept() + #self.mouseOffset = self.pos() - ev.scenePos() + #self.pressPos = ev.scenePos() + #elif ev.button() == QtCore.Qt.RightButton: + #ev.accept() + ##if self.endTick: + ##return + ##self.view.tickChanged(self, delete=True) + + #def mouseReleaseEvent(self, ev): + ##print self, "release", ev.scenePos() + #if not self.movedSincePress: + #self.view().tickClicked(self, ev) + + ##if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: + ##color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) + ##if color.isValid(): + ##self.color = color + ##self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) + ###self.emit(QtCore.SIGNAL('tickChanged'), self) + ##self.view.tickChanged(self) diff --git a/graphicsItems/GradientLegend.py b/graphicsItems/GradientLegend.py new file mode 100644 index 00000000..442b8f09 --- /dev/null +++ b/graphicsItems/GradientLegend.py @@ -0,0 +1,112 @@ +from pyqtgraph.Qt import QtGui, QtCore +from UIGraphicsItem import * +import pyqtgraph.functions as fn + +__all__ = ['GradientLegend'] + +class GradientLegend(UIGraphicsItem): + """ + Draws a color gradient rectangle along with text labels denoting the value at specific + points along the gradient. + """ + + def __init__(self, view, size, offset): + self.size = size + self.offset = offset + UIGraphicsItem.__init__(self, view) + self.setAcceptedMouseButtons(QtCore.Qt.NoButton) + self.brush = QtGui.QBrush(QtGui.QColor(200,0,0)) + self.pen = QtGui.QPen(QtGui.QColor(0,0,0)) + self.labels = {'max': 1, 'min': 0} + self.gradient = QtGui.QLinearGradient() + self.gradient.setColorAt(0, QtGui.QColor(0,0,0)) + self.gradient.setColorAt(1, QtGui.QColor(255,0,0)) + + def setGradient(self, g): + self.gradient = g + self.update() + + def setIntColorScale(self, minVal, maxVal, *args, **kargs): + colors = [fn.intColor(i, maxVal-minVal, *args, **kargs) for i in range(minVal, maxVal)] + g = QtGui.QLinearGradient() + for i in range(len(colors)): + x = float(i)/len(colors) + g.setColorAt(x, colors[i]) + self.setGradient(g) + if 'labels' not in kargs: + self.setLabels({str(minVal/10.): 0, str(maxVal): 1}) + else: + self.setLabels({kargs['labels'][0]:0, kargs['labels'][1]:1}) + + def setLabels(self, l): + """Defines labels to appear next to the color scale. Accepts a dict of {text: value} pairs""" + self.labels = l + self.update() + + def paint(self, p, opt, widget): + UIGraphicsItem.paint(self, p, opt, widget) + rect = self.boundingRect() ## Boundaries of visible area in scene coords. + unit = self.pixelSize() ## Size of one view pixel in scene coords. + + ## determine max width of all labels + labelWidth = 0 + labelHeight = 0 + for k in self.labels: + b = p.boundingRect(QtCore.QRectF(0, 0, 0, 0), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) + labelWidth = max(labelWidth, b.width()) + labelHeight = max(labelHeight, b.height()) + + labelWidth *= unit[0] + labelHeight *= unit[1] + + textPadding = 2 # in px + + if self.offset[0] < 0: + x3 = rect.right() + unit[0] * self.offset[0] + x2 = x3 - labelWidth - unit[0]*textPadding*2 + x1 = x2 - unit[0] * self.size[0] + else: + x1 = rect.left() + unit[0] * self.offset[0] + x2 = x1 + unit[0] * self.size[0] + x3 = x2 + labelWidth + unit[0]*textPadding*2 + if self.offset[1] < 0: + y2 = rect.top() - unit[1] * self.offset[1] + y1 = y2 + unit[1] * self.size[1] + else: + y1 = rect.bottom() - unit[1] * self.offset[1] + y2 = y1 - unit[1] * self.size[1] + self.b = [x1,x2,x3,y1,y2,labelWidth] + + ## Draw background + p.setPen(self.pen) + p.setBrush(QtGui.QBrush(QtGui.QColor(255,255,255,100))) + rect = QtCore.QRectF( + QtCore.QPointF(x1 - unit[0]*textPadding, y1 + labelHeight/2 + unit[1]*textPadding), + QtCore.QPointF(x3, y2 - labelHeight/2 - unit[1]*textPadding) + ) + p.drawRect(rect) + + + ## Have to scale painter so that text and gradients are correct size. Bleh. + p.scale(unit[0], unit[1]) + + ## Draw color bar + self.gradient.setStart(0, y1/unit[1]) + self.gradient.setFinalStop(0, y2/unit[1]) + p.setBrush(self.gradient) + rect = QtCore.QRectF( + QtCore.QPointF(x1/unit[0], y1/unit[1]), + QtCore.QPointF(x2/unit[0], y2/unit[1]) + ) + p.drawRect(rect) + + + ## draw labels + p.setPen(QtGui.QPen(QtGui.QColor(0,0,0))) + tx = x2 + unit[0]*textPadding + lh = labelHeight/unit[1] + for k in self.labels: + y = y1 + self.labels[k] * (y2-y1) + p.drawText(QtCore.QRectF(tx/unit[0], y/unit[1] - lh/2.0, 1000, lh), QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, str(k)) + + diff --git a/graphicsItems/GraphicsItemMethods.py b/graphicsItems/GraphicsItemMethods.py new file mode 100644 index 00000000..63e95476 --- /dev/null +++ b/graphicsItems/GraphicsItemMethods.py @@ -0,0 +1,256 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.GraphicsScene import GraphicsScene +from pyqtgraph.Point import Point +import weakref + +class GraphicsItemMethods: + """ + Class providing useful methods to GraphicsObject and GraphicsWidget. + """ + def __init__(self): + self._viewWidget = None + self._viewBox = None + GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() + + def getViewWidget(self): + """ + Return the view widget for this item. If the scene has multiple views, only the first view is returned. + The return value is cached; clear the cached value with forgetViewWidget() + """ + if self._viewWidget is None: + scene = self.scene() + if scene is None: + return None + views = scene.views() + if len(views) < 1: + return None + self._viewWidget = weakref.ref(self.scene().views()[0]) + return self._viewWidget() + + def forgetViewWidget(self): + self._viewWidget = None + + def getViewBox(self): + """ + Return the first ViewBox or GraphicsView which bounds this item's visible space. + If this item is not contained within a ViewBox, then the GraphicsView is returned. + If the item is contained inside nested ViewBoxes, then the inner-most ViewBox is returned. + The result is cached; clear the cache with forgetViewBox() + """ + if self._viewBox is None: + p = self + while True: + p = p.parentItem() + if p is None: + vb = self.getViewWidget() + if vb is None: + return None + else: + self._viewBox = weakref.ref(vb) + break + if hasattr(p, 'implements') and p.implements('ViewBox'): + self._viewBox = weakref.ref(p) + break + + return self._viewBox() ## If we made it this far, _viewBox is definitely not None + + def forgetViewBox(self): + self._viewBox = None + + + def deviceTransform(self, viewportTransform=None): + """ + Return the transform that converts local item coordinates to device coordinates (usually pixels). + Extends deviceTransform to automatically determine the viewportTransform. + """ + if viewportTransform is None: + view = self.getViewWidget() + if view is None: + return None + viewportTransform = view.viewportTransform() + return QtGui.QGraphicsObject.deviceTransform(self, viewportTransform) + + def viewTransform(self): + """Return the transform that maps from local coordinates to the item's ViewBox coordinates + If there is no ViewBox, return the scene transform. + Returns None if the item does not have a view.""" + view = self.getViewBox() + if view is None: + return None + if hasattr(view, 'implements') and view.implements('ViewBox'): + return self.itemTransform(view.innerSceneItem())[0] + else: + return self.sceneTransform() + #return self.deviceTransform(view.viewportTransform()) + + + + def getBoundingParents(self): + """Return a list of parents to this item that have child clipping enabled.""" + p = self + parents = [] + while True: + p = p.parentItem() + if p is None: + break + if p.flags() & self.ItemClipsChildrenToShape: + parents.append(p) + return parents + + def viewRect(self): + """Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget""" + view = self.getViewBox() + if view is None: + return None + bounds = self.mapRectFromView(view.viewRect()).normalized() + + ## nah. + #for p in self.getBoundingParents(): + #bounds &= self.mapRectFromScene(p.sceneBoundingRect()) + + return bounds + + + + def pixelVectors(self): + """Return vectors in local coordinates representing the width and height of a view pixel.""" + vt = self.deviceTransform() + if vt is None: + return None + vt = vt.inverted()[0] + orig = vt.map(QtCore.QPointF(0, 0)) + return vt.map(QtCore.QPointF(1, 0))-orig, vt.map(QtCore.QPointF(0, 1))-orig + + def pixelLength(self, direction): + """Return the length of one pixel in the direction indicated (in local coordinates)""" + dt = self.deviceTransform() + if dt is None: + return None + viewDir = Point(dt.map(direction) - dt.map(Point(0,0))) + norm = viewDir.norm() + dti = dt.inverted()[0] + return Point(dti.map(norm)-dti.map(Point(0,0))).length() + + + def pixelSize(self): + v = self.pixelVectors() + return (v[0].x()**2+v[0].y()**2)**0.5, (v[1].x()**2+v[1].y()**2)**0.5 + + def pixelWidth(self): + vt = self.deviceTransform() + if vt is None: + return 0 + vt = vt.inverted()[0] + return Point(vt.map(QtCore.QPointF(1, 0))-vt.map(QtCore.QPointF(0, 0))).length() + + def pixelHeight(self): + vt = self.deviceTransform() + if vt is None: + return 0 + vt = vt.inverted()[0] + return Point(vt.map(QtCore.QPointF(0, 1))-vt.map(QtCore.QPointF(0, 0))).length() + + + def mapToDevice(self, obj): + """ + Return *obj* mapped from local coordinates to device coordinates (pixels). + """ + vt = self.deviceTransform() + if vt is None: + return None + return vt.map(obj) + + def mapFromDevice(self, obj): + """ + Return *obj* mapped from device coordinates (pixels) to local coordinates. + """ + vt = self.deviceTransform() + if vt is None: + return None + vt = vt.inverted()[0] + return vt.map(obj) + + def mapToView(self, obj): + vt = self.viewTransform() + if vt is None: + return None + return vt.map(obj) + + def mapRectToView(self, obj): + vt = self.viewTransform() + if vt is None: + return None + return vt.mapRect(obj) + + def mapFromView(self, obj): + vt = self.viewTransform() + if vt is None: + return None + vt = vt.inverted()[0] + return vt.map(obj) + + def mapRectFromView(self, obj): + vt = self.viewTransform() + if vt is None: + return None + vt = vt.inverted()[0] + return vt.mapRect(obj) + + def pos(self): + return Point(QtGui.QGraphicsObject.pos(self)) + + def viewPos(self): + return self.mapToView(self.pos()) + + #def itemChange(self, change, value): + #ret = QtGui.QGraphicsObject.itemChange(self, change, value) + #if change == self.ItemParentHasChanged or change == self.ItemSceneHasChanged: + #print "Item scene changed:", self + #self.setChildScene(self) ## This is bizarre. + #return ret + + #def setChildScene(self, ch): + #scene = self.scene() + #for ch2 in ch.childItems(): + #if ch2.scene() is not scene: + #print "item", ch2, "has different scene:", ch2.scene(), scene + #scene.addItem(ch2) + #QtGui.QApplication.processEvents() + #print " --> ", ch2.scene() + #self.setChildScene(ch2) + + def parentItem(self): + ## PyQt bug -- some items are returned incorrectly. + return GraphicsScene.translateGraphicsItem(QtGui.QGraphicsObject.parentItem(self)) + + + def childItems(self): + ## PyQt bug -- some child items are returned incorrectly. + return map(GraphicsScene.translateGraphicsItem, QtGui.QGraphicsObject.childItems(self)) + + + def sceneTransform(self): + ## Qt bug: do no allow access to sceneTransform() until + ## the item has a scene. + + if self.scene() is None: + return self.transform() + else: + return QtGui.QGraphicsObject.sceneTransform(self) + + + def transformAngle(self, relativeItem=None): + """Return the rotation produced by this item's transform (this assumes there is no shear in the transform) + If relativeItem is given, then the angle is determined relative to that item. + """ + if relativeItem is None: + relativeItem = self.parentItem() + + tr = self.itemTransform(relativeItem)[0] + vec = tr.map(Point(1,0)) - tr.map(Point(0,0)) + return Point(vec).angle(Point(1,0)) + + + + + \ No newline at end of file diff --git a/graphicsItems/GraphicsLayout.py b/graphicsItems/GraphicsLayout.py new file mode 100644 index 00000000..c1d28c28 --- /dev/null +++ b/graphicsItems/GraphicsLayout.py @@ -0,0 +1,97 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +from GraphicsWidget import GraphicsWidget + +__all__ = ['GraphicsLayout'] +class GraphicsLayout(GraphicsWidget): + """ + Used for laying out GraphicsWidgets in a grid. + """ + + + def __init__(self, parent=None, border=None): + GraphicsWidget.__init__(self, parent) + if border is True: + border = (100,100,100) + self.border = border + self.layout = QtGui.QGraphicsGridLayout() + self.setLayout(self.layout) + self.items = {} + self.rows = {} + self.currentRow = 0 + self.currentCol = 0 + + def nextRow(self): + """Advance to next row for automatic item placement""" + self.currentRow += 1 + self.currentCol = 0 + + def nextCol(self, colspan=1): + """Advance to next column, while returning the current column number + (generally only for internal use--called by addItem)""" + self.currentCol += colspan + return self.currentCol-colspan + + def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + from PlotItem import PlotItem + plot = PlotItem(**kargs) + self.addItem(plot, row, col, rowspan, colspan) + return plot + + def addViewBox(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + vb = ViewBox(**kargs) + self.addItem(vb, row, col, rowspan, colspan) + return vb + + + def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): + if row is None: + row = self.currentRow + if col is None: + col = self.nextCol(colspan) + + if row not in self.rows: + self.rows[row] = {} + self.rows[row][col] = item + self.items[item] = (row, col) + + self.layout.addItem(item, row, col, rowspan, colspan) + + def getItem(self, row, col): + return self.row[row][col] + + def boundingRect(self): + return self.rect() + + def paint(self, p, *args): + if self.border is None: + return + p.setPen(fn.mkPen(self.border)) + for i in self.items: + r = i.mapRectToParent(i.boundingRect()) + p.drawRect(r) + + def itemIndex(self, item): + for i in range(self.layout.count()): + if self.layout.itemAt(i).graphicsItem() is item: + return i + raise Exception("Could not determine index of item " + str(item)) + + def removeItem(self, item): + ind = self.itemIndex(item) + self.layout.removeAt(ind) + self.scene().removeItem(item) + r,c = self.items[item] + del self.items[item] + del self.rows[r][c] + self.update() + + def clear(self): + items = [] + for i in self.items.keys(): + self.removeItem(i) + + +## Must be imported at the end to avoid cyclic-dependency hell: +from ViewBox import ViewBox +from PlotItem import PlotItem diff --git a/graphicsItems/GraphicsObject.py b/graphicsItems/GraphicsObject.py new file mode 100644 index 00000000..af727315 --- /dev/null +++ b/graphicsItems/GraphicsObject.py @@ -0,0 +1,19 @@ +from pyqtgraph.Qt import QtGui, QtCore +from GraphicsItemMethods import GraphicsItemMethods + +__all__ = ['GraphicsObject'] +class GraphicsObject(GraphicsItemMethods, QtGui.QGraphicsObject): + """Extends QGraphicsObject with a few important functions. + (Most of these assume that the object is in a scene with a single view) + + This class also generates a cache of the Qt-internal addresses of each item + so that GraphicsScene.items() can return the correct objects (this is a PyQt bug) + + Note: most of the extended functionality is inherited from GraphicsItemMethods, + which is shared between GraphicsObject and GraphicsWidget. + """ + def __init__(self, *args): + QtGui.QGraphicsObject.__init__(self, *args) + GraphicsItemMethods.__init__(self) + + diff --git a/graphicsItems/GraphicsWidget.py b/graphicsItems/GraphicsWidget.py new file mode 100644 index 00000000..0181ea17 --- /dev/null +++ b/graphicsItems/GraphicsWidget.py @@ -0,0 +1,44 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.GraphicsScene import GraphicsScene +from GraphicsItemMethods import GraphicsItemMethods + +__all__ = ['GraphicsWidget'] +class GraphicsWidget(GraphicsItemMethods, QtGui.QGraphicsWidget): + def __init__(self, *args, **kargs): + """ + Extends QGraphicsWidget with several helpful methods and workarounds for PyQt bugs. + Most of the extra functionality is inherited from GraphicsObjectSuperclass. + """ + QtGui.QGraphicsWidget.__init__(self, *args, **kargs) + GraphicsItemMethods.__init__(self) + GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() + + #def getMenu(self): + #pass + + def setFixedHeight(self, h): + self.setMaximumHeight(h) + self.setMinimumHeight(h) + + def setFixedWidth(self, h): + self.setMaximumWidth(h) + self.setMinimumWidth(h) + + def height(self): + return self.geometry().height() + + def width(self): + return self.geometry().width() + + def boundingRect(self): + br = self.mapRectFromParent(self.geometry()).normalized() + #print "bounds:", br + return br + + def shape(self): ## No idea why this is necessary, but rotated items do not receive clicks otherwise. + p = QtGui.QPainterPath() + p.addRect(self.boundingRect()) + #print "shape:", p.boundingRect() + return p + + diff --git a/graphicsItems/GridItem.py b/graphicsItems/GridItem.py new file mode 100644 index 00000000..e3a4f1a0 --- /dev/null +++ b/graphicsItems/GridItem.py @@ -0,0 +1,116 @@ +from pyqtgraph.Qt import QtGui, QtCore +from UIGraphicsItem import * +import numpy as np +from pyqtgraph.Point import Point + +__all__ = ['GridItem'] +class GridItem(UIGraphicsItem): + """ + Displays a rectangular grid of lines indicating major divisions within a coordinate system. + Automatically determines what divisions to use. + """ + + def __init__(self): + UIGraphicsItem.__init__(self) + #QtGui.QGraphicsItem.__init__(self, *args) + #self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + + self.picture = None + + + def viewChangedEvent(self): + self.picture = None + #UIGraphicsItem.viewRangeChanged(self) + #self.update() + + def paint(self, p, opt, widget): + #p.setPen(QtGui.QPen(QtGui.QColor(100, 100, 100))) + #p.drawRect(self.boundingRect()) + #UIGraphicsItem.paint(self, p, opt, widget) + ### draw picture + if self.picture is None: + #print "no pic, draw.." + self.generatePicture() + p.drawPicture(QtCore.QPointF(0, 0), self.picture) + #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) + #p.drawLine(0, -100, 0, 100) + #p.drawLine(-100, 0, 100, 0) + #print "drawing Grid." + + + def generatePicture(self): + self.picture = QtGui.QPicture() + p = QtGui.QPainter() + p.begin(self.picture) + + dt = self.viewTransform().inverted()[0] + vr = self.getViewWidget().rect() + unit = self.pixelWidth(), self.pixelHeight() + dim = [vr.width(), vr.height()] + lvr = self.boundingRect() + ul = np.array([lvr.left(), lvr.top()]) + br = np.array([lvr.right(), lvr.bottom()]) + + texts = [] + + if ul[1] > br[1]: + x = ul[1] + ul[1] = br[1] + br[1] = x + for i in [2,1,0]: ## Draw three different scales of grid + dist = br-ul + nlTarget = 10.**i + d = 10. ** np.floor(np.log10(abs(dist/nlTarget))+0.5) + ul1 = np.floor(ul / d) * d + br1 = np.ceil(br / d) * d + dist = br1-ul1 + nl = (dist / d) + 0.5 + #print "level", i + #print " dim", dim + #print " dist", dist + #print " d", d + #print " nl", nl + for ax in range(0,2): ## Draw grid for both axes + ppl = dim[ax] / nl[ax] + c = np.clip(3.*(ppl-3), 0., 30.) + linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c)) + textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2)) + #linePen.setCosmetic(True) + #linePen.setWidth(1) + bx = (ax+1) % 2 + for x in range(0, int(nl[ax])): + linePen.setCosmetic(False) + if ax == 0: + linePen.setWidthF(self.pixelWidth()) + #print "ax 0 height", self.pixelHeight() + else: + linePen.setWidthF(self.pixelHeight()) + #print "ax 1 width", self.pixelWidth() + p.setPen(linePen) + p1 = np.array([0.,0.]) + p2 = np.array([0.,0.]) + p1[ax] = ul1[ax] + x * d[ax] + p2[ax] = p1[ax] + p1[bx] = ul[bx] + p2[bx] = br[bx] + ## don't draw lines that are out of bounds. + if p1[ax] < min(ul[ax], br[ax]) or p1[ax] > max(ul[ax], br[ax]): + continue + p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1])) + if i < 2: + p.setPen(textPen) + if ax == 0: + x = p1[0] + unit[0] + y = ul[1] + unit[1] * 8. + else: + x = ul[0] + unit[0]*3 + y = p1[1] + unit[1] + texts.append((QtCore.QPointF(x, y), "%g"%p1[ax])) + tr = self.deviceTransform() + #tr.scale(1.5, 1.5) + p.setWorldTransform(tr.inverted()[0]) + for t in texts: + x = tr.map(t[0]) + Point(0.5, 0.5) + p.drawText(x, t[1]) + p.end() diff --git a/graphicsItems/HistogramLUTItem.py b/graphicsItems/HistogramLUTItem.py new file mode 100644 index 00000000..19599720 --- /dev/null +++ b/graphicsItems/HistogramLUTItem.py @@ -0,0 +1,178 @@ +""" +GraphicsWidget displaying an image histogram along with gradient editor. Can be used to adjust the appearance of images. +""" + + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +from GraphicsWidget import GraphicsWidget +from ViewBox import * +from GradientEditorItem import * +from LinearRegionItem import * +from PlotDataItem import * +from AxisItem import * +from GridItem import * +from pyqtgraph.Point import Point +import pyqtgraph.functions as fn +import numpy as np + + +__all__ = ['HistogramLUTItem'] + + +class HistogramLUTItem(GraphicsWidget): + sigLookupTableChanged = QtCore.Signal(object) + sigLevelsChanged = QtCore.Signal(object) + sigLevelChangeFinished = QtCore.Signal(object) + + def __init__(self, image=None): + GraphicsWidget.__init__(self) + self.lut = None + self.imageItem = None + + self.layout = QtGui.QGraphicsGridLayout() + self.setLayout(self.layout) + self.layout.setContentsMargins(1,1,1,1) + self.layout.setSpacing(0) + self.vb = ViewBox() + self.vb.setMaximumWidth(152) + self.vb.setMinimumWidth(52) + self.vb.setMouseEnabled(x=False, y=True) + self.gradient = GradientEditorItem() + self.gradient.setOrientation('right') + self.gradient.loadPreset('grey') + self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) + self.region.setZValue(1000) + self.vb.addItem(self.region) + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, showValues=False) + self.layout.addItem(self.axis, 0, 0) + self.layout.addItem(self.vb, 0, 1) + self.layout.addItem(self.gradient, 0, 2) + self.range = None + self.gradient.setFlag(self.gradient.ItemStacksBehindParent) + self.vb.setFlag(self.gradient.ItemStacksBehindParent) + + #self.grid = GridItem() + #self.vb.addItem(self.grid) + + self.gradient.sigGradientChanged.connect(self.gradientChanged) + self.region.sigRegionChanged.connect(self.regionChanging) + self.region.sigRegionChangeFinished.connect(self.regionChanged) + self.vb.sigRangeChanged.connect(self.viewRangeChanged) + self.plot = PlotDataItem() + self.plot.rotate(90) + self.vb.addItem(self.plot) + self.autoHistogramRange() + + if image is not None: + self.setImageItem(image) + #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) + + + #def sizeHint(self, *args): + #return QtCore.QSizeF(115, 200) + + def paint(self, p, *args): + pen = self.region.lines[0].pen + rgn = self.getLevels() + p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) + p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) + gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) + for pen in [fn.mkPen('k', width=3), pen]: + p.setPen(pen) + p.drawLine(p1, gradRect.bottomLeft()) + p.drawLine(p2, gradRect.topLeft()) + p.drawLine(gradRect.topLeft(), gradRect.topRight()) + p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) + #p.drawRect(self.boundingRect()) + + + def setHistogramRange(self, mn, mx, padding=0.1): + """Set the Y range on the histogram plot. This disables auto-scaling.""" + self.vb.enableAutoRange(self.vb.YAxis, False) + self.vb.setYRange(mn, mx, padding) + + #d = mx-mn + #mn -= d*padding + #mx += d*padding + #self.range = [mn,mx] + #self.updateRange() + #self.vb.setMouseEnabled(False, True) + #self.region.setBounds([mn,mx]) + + def autoHistogramRange(self): + """Enable auto-scaling on the histogram plot.""" + self.vb.enableAutoRange(self.vb.XYAxes) + #self.range = None + #self.updateRange() + #self.vb.setMouseEnabled(False, False) + + #def updateRange(self): + #self.vb.autoRange() + #if self.range is not None: + #self.vb.setYRange(*self.range) + #vr = self.vb.viewRect() + + #self.region.setBounds([vr.top(), vr.bottom()]) + + def setImageItem(self, img): + self.imageItem = img + img.sigImageChanged.connect(self.imageChanged) + img.setLookupTable(self.getLookupTable) ## send function pointer, not the result + #self.gradientChanged() + self.regionChanged() + self.imageChanged(autoLevel=True) + #self.vb.autoRange() + + def viewRangeChanged(self): + self.update() + + def gradientChanged(self): + if self.imageItem is not None: + self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result + + self.lut = None + #if self.imageItem is not None: + #self.imageItem.setLookupTable(self.gradient.getLookupTable(512)) + self.sigLookupTableChanged.emit(self) + + def getLookupTable(self, img=None, n=None): + if n is None: + if img.dtype == np.uint8: + n = 256 + else: + n = 512 + if self.lut is None: + self.lut = self.gradient.getLookupTable(n) + return self.lut + + def regionChanged(self): + #if self.imageItem is not None: + #self.imageItem.setLevels(self.region.getRegion()) + self.sigLevelChangeFinished.emit(self) + #self.update() + + def regionChanging(self): + if self.imageItem is not None: + self.imageItem.setLevels(self.region.getRegion()) + self.sigLevelsChanged.emit(self) + self.update() + + def imageChanged(self, autoLevel=False, autoRange=False): + h = self.imageItem.getHistogram() + if h[0] is None: + return + self.plot.setData(*h, fillLevel=0.0, brush=(100, 100, 200)) + if autoLevel: + mn = h[0][0] + mx = h[0][-1] + self.region.setRegion([mn, mx]) + #self.updateRange() + #if autoRange: + #self.updateRange() + + def getLevels(self): + return self.region.getRegion() + + def setLevels(self, mn, mx): + self.region.setRegion([mn, mx]) \ No newline at end of file diff --git a/graphicsItems/ImageItem.old b/graphicsItems/ImageItem.old new file mode 100644 index 00000000..726814e0 --- /dev/null +++ b/graphicsItems/ImageItem.old @@ -0,0 +1,398 @@ +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +try: + import scipy.weave as weave + from scipy.weave import converters +except: + pass +import pyqtgraph.functions as fn +import pyqtgraph.debug as debug +from GraphicsObject import GraphicsObject + +__all__ = ['ImageItem'] +class ImageItem(GraphicsObject): + """ + GraphicsObject displaying an image. Optimized for rapid update (ie video display) + + """ + + + sigImageChanged = QtCore.Signal() + + ## performance gains from this are marginal, and it's rather unreliable. + useWeave = False + + def __init__(self, image=None, copy=True, parent=None, border=None, mode=None, *args): + #QObjectWorkaround.__init__(self) + GraphicsObject.__init__(self) + #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) + self.qimage = QtGui.QImage() + self.pixmap = None + self.paintMode = mode + #self.useWeave = True + self.blackLevel = None + self.whiteLevel = None + self.alpha = 1.0 + self.image = None + self.clipLevel = None + self.drawKernel = None + if border is not None: + border = fn.mkPen(border) + self.border = border + + #QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) + #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) + if image is not None: + self.updateImage(image, copy, autoRange=True) + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + + #self.item = QtGui.QGraphicsPixmapItem(parent=self) + + def setCompositionMode(self, mode): + self.paintMode = mode + self.update() + + def setAlpha(self, alpha): + self.alpha = alpha + self.updateImage() + + #def boundingRect(self): + #return self.pixmapItem.boundingRect() + #return QtCore.QRectF(0, 0, self.qimage.width(), self.qimage.height()) + + def width(self): + if self.pixmap is None: + return None + return self.pixmap.width() + + def height(self): + if self.pixmap is None: + return None + return self.pixmap.height() + + def boundingRect(self): + if self.pixmap is None: + return QtCore.QRectF(0., 0., 0., 0.) + return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) + + def setClipLevel(self, level=None): + self.clipLevel = level + + #def paint(self, p, opt, widget): + #pass + #if self.pixmap is not None: + #p.drawPixmap(0, 0, self.pixmap) + #print "paint" + + def setLevels(self, white=None, black=None): + if white is not None: + self.whiteLevel = white + if black is not None: + self.blackLevel = black + self.updateImage() + + def getLevels(self): + return self.whiteLevel, self.blackLevel + + def updateImage(self, *args, **kargs): + ## can we make any assumptions here that speed things up? + ## dtype, range, size are all the same? + defaults = { + 'autoRange': False, + } + defaults.update(kargs) + return self.setImage(*args, **defaults) + + def setImage(self, image=None, copy=True, autoRange=True, clipMask=None, white=None, black=None, axes=None): + prof = debug.Profiler('ImageItem.updateImage 0x%x' %id(self)) + #debug.printTrace() + if axes is None: + axh = {'x': 0, 'y': 1, 'c': 2} + else: + axh = axes + #print "Update image", black, white + if white is not None: + self.whiteLevel = white + if black is not None: + self.blackLevel = black + + gotNewData = False + if image is None: + if self.image is None: + return + else: + gotNewData = True + if self.image is None or image.shape != self.image.shape: + self.prepareGeometryChange() + if copy: + self.image = image.view(np.ndarray).copy() + else: + self.image = image.view(np.ndarray) + #print " image max:", self.image.max(), "min:", self.image.min() + prof.mark('1') + + # Determine scale factors + if autoRange or self.blackLevel is None: + if self.image.dtype is np.ubyte: + self.blackLevel = 0 + self.whiteLevel = 255 + else: + self.blackLevel = self.image.min() + self.whiteLevel = self.image.max() + #print "Image item using", self.blackLevel, self.whiteLevel + + if self.blackLevel != self.whiteLevel: + scale = 255. / (self.whiteLevel - self.blackLevel) + else: + scale = 0. + + prof.mark('2') + + ## Recolor and convert to 8 bit per channel + # Try using weave, then fall back to python + shape = self.image.shape + black = float(self.blackLevel) + white = float(self.whiteLevel) + + if black == 0 and white == 255 and self.image.dtype == np.ubyte: + im = self.image + elif self.image.dtype in [np.ubyte, np.uint16]: + # use lookup table instead + npts = 2**(self.image.itemsize * 8) + lut = self.getLookupTable(npts, black, white) + im = lut[self.image] + else: + im = self.applyColorScaling(self.image, black, scale) + + prof.mark('3') + + try: + im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte) + except: + print im.shape, axh + raise + alpha = np.clip(int(255 * self.alpha), 0, 255) + prof.mark('4') + # Fill image + if im.ndim == 2: + im2 = im.transpose(axh['y'], axh['x']) + im1[..., 0] = im2 + im1[..., 1] = im2 + im1[..., 2] = im2 + im1[..., 3] = alpha + elif im.ndim == 3: #color image + im2 = im.transpose(axh['y'], axh['x'], axh['c']) + if im2.shape[2] > 4: + raise Exception("ImageItem got image with more than 4 color channels (shape is %s; axes are %s)" % (str(im.shape), str(axh))) + ## [B G R A] Reorder colors + order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. + + for i in range(0, im.shape[axh['c']]): + im1[..., order[i]] = im2[..., i] + + ## fill in unused channels with 0 or alpha + for i in range(im.shape[axh['c']], 3): + im1[..., i] = 0 + if im.shape[axh['c']] < 4: + im1[..., 3] = alpha + + else: + raise Exception("Image must be 2 or 3 dimensions") + #self.im1 = im1 + # Display image + prof.mark('5') + if self.clipLevel is not None or clipMask is not None: + if clipMask is not None: + mask = clipMask.transpose() + else: + mask = (self.image < self.clipLevel).transpose() + im1[..., 0][mask] *= 0.5 + im1[..., 1][mask] *= 0.5 + im1[..., 2][mask] = 255 + prof.mark('6') + #print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape + self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :( + prof.mark('7') + qimage = QtGui.QImage(buffer(self.ims), im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) + prof.mark('8') + self.pixmap = QtGui.QPixmap.fromImage(qimage) + prof.mark('9') + ##del self.ims + #self.item.setPixmap(self.pixmap) + + self.update() + prof.mark('10') + + if gotNewData: + #self.emit(QtCore.SIGNAL('imageChanged')) + self.sigImageChanged.emit() + + prof.finish() + + def getLookupTable(self, num, black, white): + num = int(num) + black = int(black) + white = int(white) + if white < black: + b = black + black = white + white = b + key = (num, black, white) + lut = np.empty(num, dtype=np.ubyte) + lut[:black] = 0 + rng = lut[black:white] + try: + rng[:] = np.linspace(0, 255, white-black)[:len(rng)] + except: + print key, rng.shape + lut[white:] = 255 + return lut + + + def applyColorScaling(self, img, offset, scale): + try: + if not ImageItem.useWeave: + raise Exception('Skipping weave compile') + #sim = np.ascontiguousarray(self.image) ## should not be needed + sim = img.reshape(img.size) + #sim.shape = sim.size + im = np.empty(sim.shape, dtype=np.ubyte) + n = im.size + + code = """ + for( int i=0; i 255.0 ) + a = 255.0; + else if( a < 0.0 ) + a = 0.0; + im(i) = a; + } + """ + + weave.inline(code, ['sim', 'im', 'n', 'offset', 'scale'], type_converters=converters.blitz, compiler = 'gcc') + #sim.shape = shape + im.shape = img.shape + except: + if ImageItem.useWeave: + ImageItem.useWeave = False + #sys.excepthook(*sys.exc_info()) + #print "==============================================================================" + #print "Weave compile failed, falling back to slower version." + #img.shape = shape + im = ((img - offset) * scale).clip(0.,255.).astype(np.ubyte) + return im + + + def getPixmap(self): + return self.pixmap.copy() + + def getHistogram(self, bins=500, step=3): + """returns x and y arrays containing the histogram values for the current image. + The step argument causes pixels to be skipped when computing the histogram to save time.""" + if self.image is None: + return None,None + stepData = self.image[::step, ::step] + hist = np.histogram(stepData, bins=bins) + return hist[1][:-1], hist[0] + + def setPxMode(self, b): + """Set whether the item ignores transformations and draws directly to screen pixels.""" + self.setFlag(self.ItemIgnoresTransformations, b) + + def setScaledMode(self): + self.setPxMode(False) + + def mousePressEvent(self, ev): + if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: + self.drawAt(ev.pos(), ev) + ev.accept() + else: + ev.ignore() + + def mouseMoveEvent(self, ev): + #print "mouse move", ev.pos() + if self.drawKernel is not None: + self.drawAt(ev.pos(), ev) + + def mouseReleaseEvent(self, ev): + pass + + def tabletEvent(self, ev): + print ev.device() + print ev.pointerType() + print ev.pressure() + + def drawAt(self, pos, ev=None): + pos = [int(pos.x()), int(pos.y())] + dk = self.drawKernel + kc = self.drawKernelCenter + sx = [0,dk.shape[0]] + sy = [0,dk.shape[1]] + tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] + ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] + + for i in [0,1]: + dx1 = -min(0, tx[i]) + dx2 = min(0, self.image.shape[0]-tx[i]) + tx[i] += dx1+dx2 + sx[i] += dx1+dx2 + + dy1 = -min(0, ty[i]) + dy2 = min(0, self.image.shape[1]-ty[i]) + ty[i] += dy1+dy2 + sy[i] += dy1+dy2 + + #print sx + #print sy + #print tx + #print ty + #print self.image.shape + #print self.image[tx[0]:tx[1], ty[0]:ty[1]].shape + #print dk[sx[0]:sx[1], sy[0]:sy[1]].shape + ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1])) + ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) + #src = dk[sx[0]:sx[1], sy[0]:sy[1]] + #mask = self.drawMask[sx[0]:sx[1], sy[0]:sy[1]] + mask = self.drawMask + src = dk + #print self.image[ts].shape, src.shape + + if callable(self.drawMode): + self.drawMode(dk, self.image, mask, ss, ts, ev) + else: + src = src[ss] + if self.drawMode == 'set': + if mask is not None: + mask = mask[ss] + self.image[ts] = self.image[ts] * (1-mask) + src * mask + else: + self.image[ts] = src + elif self.drawMode == 'add': + self.image[ts] += src + else: + raise Exception("Unknown draw mode '%s'" % self.drawMode) + self.updateImage() + + def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): + self.drawKernel = kernel + self.drawKernelCenter = center + self.drawMode = mode + self.drawMask = mask + + def paint(self, p, *args): + + #QtGui.QGraphicsPixmapItem.paint(self, p, *args) + if self.pixmap is None: + return + if self.paintMode is not None: + p.setCompositionMode(self.paintMode) + p.drawPixmap(self.boundingRect(), self.pixmap, QtCore.QRectF(0, 0, self.pixmap.width(), self.pixmap.height())) + if self.border is not None: + p.setPen(self.border) + p.drawRect(self.boundingRect()) + + def pixelSize(self): + """return size of a single pixel in the image""" + br = self.sceneBoundingRect() + return br.width()/self.pixmap.width(), br.height()/self.pixmap.height() diff --git a/graphicsItems/ImageItem.py b/graphicsItems/ImageItem.py new file mode 100644 index 00000000..088b5891 --- /dev/null +++ b/graphicsItems/ImageItem.py @@ -0,0 +1,537 @@ +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +try: + import scipy.weave as weave + from scipy.weave import converters +except: + pass +import pyqtgraph.functions as fn +import pyqtgraph.debug as debug +from GraphicsObject import GraphicsObject + +__all__ = ['ImageItem'] +class ImageItem(GraphicsObject): + """ + GraphicsObject displaying an image. Optimized for rapid update (ie video display) + + """ + + + sigImageChanged = QtCore.Signal() + + ## performance gains from this are marginal, and it's rather unreliable. + useWeave = False + + def __init__(self, image=None, **kargs): + """ + See setImage for all allowed arguments. + """ + GraphicsObject.__init__(self) + #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) + #self.qimage = QtGui.QImage() + #self._pixmap = None + + self.image = None ## original image data + self.qimage = None ## rendered image for display + #self.clipMask = None + + self.paintMode = None + #self.useWeave = True + + self.levels = None ## [min, max] or [[redMin, redMax], ...] + self.lut = None + + #self.clipLevel = None + self.drawKernel = None + self.border = None + + if image is not None: + self.setImage(image, **kargs) + else: + self.setOpts(**kargs) + + def setCompositionMode(self, mode): + self.paintMode = mode + self.update() + + ## use setOpacity instead. + #def setAlpha(self, alpha): + #self.setOpacity(alpha) + #self.updateImage() + + def setBorder(self, b): + self.border = fn.mkPen(b) + self.update() + + def width(self): + if self.image is None: + return None + return self.image.shape[0] + + def height(self): + if self.image is None: + return None + return self.image.shape[1] + + def boundingRect(self): + if self.image is None: + return QtCore.QRectF(0., 0., 0., 0.) + return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) + + #def setClipLevel(self, level=None): + #self.clipLevel = level + #self.updateImage() + + #def paint(self, p, opt, widget): + #pass + #if self.pixmap is not None: + #p.drawPixmap(0, 0, self.pixmap) + #print "paint" + + def setLevels(self, levels, update=True): + """ + Set image scaling levels. Can be one of: + [blackLevel, whiteLevel] + [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]] + Only the first format is compatible with lookup tables. + """ + self.levels = levels + if update: + self.updateImage() + + def getLevels(self): + return self.levels + #return self.whiteLevel, self.blackLevel + + def setLookupTable(self, lut, update=True): + """ + Set the lookup table to use for this image. (see functions.makeARGB for more information on how this is used) + Optionally, lut can be a callable that accepts the current image as an argument and returns the lookup table to use.""" + self.lut = lut + if update: + self.updateImage() + + def setOpts(self, update=True, **kargs): + if 'lut' in kargs: + self.setLookupTable(kargs['lut'], update=update) + if 'levels' in kargs: + self.setLevels(kargs['levels'], update=update) + #if 'clipLevel' in kargs: + #self.setClipLevel(kargs['clipLevel']) + if 'opacity' in kargs: + self.setOpacity(kargs['opacity']) + if 'compositionMode' in kargs: + self.setCompositionMode(kargs['compositionMode']) + if 'border' in kargs: + self.setBorder(kargs['border']) + + def setRect(self, rect): + """Scale and translate the image to fit within rect.""" + self.resetTransform() + self.scale(rect.width() / self.width(), rect.height() / self.height()) + self.translate(rect.left(), rect.top()) + + def setImage(self, image=None, autoLevels=None, **kargs): + """ + Update the image displayed by this item. + Arguments: + image + autoLevels + lut + levels + opacity + compositionMode + border + + """ + prof = debug.Profiler('ImageItem.setImage', disabled=True) + + gotNewData = False + if image is None: + if self.image is None: + return + else: + gotNewData = True + if self.image is None or image.shape != self.image.shape: + self.prepareGeometryChange() + self.image = image.view(np.ndarray) + + prof.mark('1') + + if autoLevels is None: + if 'levels' in kargs: + autoLevels = False + else: + autoLevels = True + if autoLevels: + img = self.image + while img.size > 2**16: + img = img[::2, ::2] + mn, mx = img.min(), img.max() + if mn == mx: + mn = 0 + mx = 255 + kargs['levels'] = [mn,mx] + prof.mark('2') + + self.setOpts(update=False, **kargs) + prof.mark('3') + + self.qimage = None + self.update() + prof.mark('4') + + if gotNewData: + self.sigImageChanged.emit() + + + prof.finish() + + + + def updateImage(self, *args, **kargs): + ## used for re-rendering qimage from self.image. + + ## can we make any assumptions here that speed things up? + ## dtype, range, size are all the same? + defaults = { + 'autoLevels': False, + } + defaults.update(kargs) + return self.setImage(*args, **defaults) + + + + + def render(self): + prof = debug.Profiler('ImageItem.render', disabled=True) + if self.image is None: + return + if callable(self.lut): + lut = self.lut(self.image) + else: + lut = self.lut + + argb, alpha = fn.makeARGB(self.image, lut=lut, levels=self.levels) + self.qimage = fn.makeQImage(argb, alpha) + #self.pixmap = QtGui.QPixmap.fromImage(self.qimage) + prof.finish() + + + def paint(self, p, *args): + prof = debug.Profiler('ImageItem.paint', disabled=True) + if self.image is None: + return + if self.qimage is None: + self.render() + if self.paintMode is not None: + p.setCompositionMode(self.paintMode) + + p.drawImage(QtCore.QPointF(0,0), self.qimage) + if self.border is not None: + p.setPen(self.border) + p.drawRect(self.boundingRect()) + prof.finish() + + + def getHistogram(self, bins=500, step=3): + """returns x and y arrays containing the histogram values for the current image. + The step argument causes pixels to be skipped when computing the histogram to save time.""" + if self.image is None: + return None,None + stepData = self.image[::step, ::step] + hist = np.histogram(stepData, bins=bins) + return hist[1][:-1], hist[0] + + def setPxMode(self, b): + """Set whether the item ignores transformations and draws directly to screen pixels.""" + self.setFlag(self.ItemIgnoresTransformations, b) + + def setScaledMode(self): + self.setPxMode(False) + + def getPixmap(self): + if self.qimage is None: + self.render() + if self.qimage is None: + return None + return QtGui.QPixmap.fromImage(self.qimage) + + def pixelSize(self): + """return scene-size of a single pixel in the image""" + br = self.sceneBoundingRect() + if self.image is None: + return 1,1 + return br.width()/self.width(), br.height()/self.height() + + def mousePressEvent(self, ev): + if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: + self.drawAt(ev.pos(), ev) + ev.accept() + else: + ev.ignore() + + def mouseMoveEvent(self, ev): + #print "mouse move", ev.pos() + if self.drawKernel is not None: + self.drawAt(ev.pos(), ev) + + def mouseReleaseEvent(self, ev): + pass + + def tabletEvent(self, ev): + print ev.device() + print ev.pointerType() + print ev.pressure() + + def drawAt(self, pos, ev=None): + pos = [int(pos.x()), int(pos.y())] + dk = self.drawKernel + kc = self.drawKernelCenter + sx = [0,dk.shape[0]] + sy = [0,dk.shape[1]] + tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] + ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] + + for i in [0,1]: + dx1 = -min(0, tx[i]) + dx2 = min(0, self.image.shape[0]-tx[i]) + tx[i] += dx1+dx2 + sx[i] += dx1+dx2 + + dy1 = -min(0, ty[i]) + dy2 = min(0, self.image.shape[1]-ty[i]) + ty[i] += dy1+dy2 + sy[i] += dy1+dy2 + + #print sx + #print sy + #print tx + #print ty + #print self.image.shape + #print self.image[tx[0]:tx[1], ty[0]:ty[1]].shape + #print dk[sx[0]:sx[1], sy[0]:sy[1]].shape + ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1])) + ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) + #src = dk[sx[0]:sx[1], sy[0]:sy[1]] + #mask = self.drawMask[sx[0]:sx[1], sy[0]:sy[1]] + mask = self.drawMask + src = dk + #print self.image[ts].shape, src.shape + + if callable(self.drawMode): + self.drawMode(dk, self.image, mask, ss, ts, ev) + else: + src = src[ss] + if self.drawMode == 'set': + if mask is not None: + mask = mask[ss] + self.image[ts] = self.image[ts] * (1-mask) + src * mask + else: + self.image[ts] = src + elif self.drawMode == 'add': + self.image[ts] += src + else: + raise Exception("Unknown draw mode '%s'" % self.drawMode) + self.updateImage() + + def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): + self.drawKernel = kernel + self.drawKernelCenter = center + self.drawMode = mode + self.drawMask = mask + + + + + + #def setImage(self, image=None, copy=True, autoRange=True, clipMask=None, white=None, black=None, axes=None): + #prof = debug.Profiler('ImageItem.updateImage 0x%x' %id(self), disabled=True) + ##debug.printTrace() + #if axes is None: + #axh = {'x': 0, 'y': 1, 'c': 2} + #else: + #axh = axes + ##print "Update image", black, white + #if white is not None: + #self.whiteLevel = white + #if black is not None: + #self.blackLevel = black + + #gotNewData = False + #if image is None: + #if self.image is None: + #return + #else: + #gotNewData = True + #if self.image is None or image.shape != self.image.shape: + #self.prepareGeometryChange() + #if copy: + #self.image = image.view(np.ndarray).copy() + #else: + #self.image = image.view(np.ndarray) + ##print " image max:", self.image.max(), "min:", self.image.min() + #prof.mark('1') + + ## Determine scale factors + #if autoRange or self.blackLevel is None: + #if self.image.dtype is np.ubyte: + #self.blackLevel = 0 + #self.whiteLevel = 255 + #else: + #self.blackLevel = self.image.min() + #self.whiteLevel = self.image.max() + ##print "Image item using", self.blackLevel, self.whiteLevel + + #if self.blackLevel != self.whiteLevel: + #scale = 255. / (self.whiteLevel - self.blackLevel) + #else: + #scale = 0. + + #prof.mark('2') + + ### Recolor and convert to 8 bit per channel + ## Try using weave, then fall back to python + #shape = self.image.shape + #black = float(self.blackLevel) + #white = float(self.whiteLevel) + + #if black == 0 and white == 255 and self.image.dtype == np.ubyte: + #im = self.image + #elif self.image.dtype in [np.ubyte, np.uint16]: + ## use lookup table instead + #npts = 2**(self.image.itemsize * 8) + #lut = self.getLookupTable(npts, black, white) + #im = lut[self.image] + #else: + #im = self.applyColorScaling(self.image, black, scale) + + #prof.mark('3') + + #try: + #im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte) + #except: + #print im.shape, axh + #raise + #alpha = np.clip(int(255 * self.alpha), 0, 255) + #prof.mark('4') + ## Fill image + #if im.ndim == 2: + #im2 = im.transpose(axh['y'], axh['x']) + #im1[..., 0] = im2 + #im1[..., 1] = im2 + #im1[..., 2] = im2 + #im1[..., 3] = alpha + #elif im.ndim == 3: #color image + #im2 = im.transpose(axh['y'], axh['x'], axh['c']) + #if im2.shape[2] > 4: + #raise Exception("ImageItem got image with more than 4 color channels (shape is %s; axes are %s)" % (str(im.shape), str(axh))) + ### [B G R A] Reorder colors + #order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. + + #for i in range(0, im.shape[axh['c']]): + #im1[..., order[i]] = im2[..., i] + + ### fill in unused channels with 0 or alpha + #for i in range(im.shape[axh['c']], 3): + #im1[..., i] = 0 + #if im.shape[axh['c']] < 4: + #im1[..., 3] = alpha + + #else: + #raise Exception("Image must be 2 or 3 dimensions") + ##self.im1 = im1 + ## Display image + #prof.mark('5') + #if self.clipLevel is not None or clipMask is not None: + #if clipMask is not None: + #mask = clipMask.transpose() + #else: + #mask = (self.image < self.clipLevel).transpose() + #im1[..., 0][mask] *= 0.5 + #im1[..., 1][mask] *= 0.5 + #im1[..., 2][mask] = 255 + #prof.mark('6') + ##print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape + ##self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :( + #prof.mark('7') + #try: + #buf = im1.data + #except AttributeError: + #im1 = np.ascontiguousarray(im1) + #buf = im1.data + + #qimage = QtGui.QImage(buf, im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) + #self.qimage = qimage + #self.qimage.data = im1 + #self._pixmap = None + #prof.mark('8') + + ##self.pixmap = QtGui.QPixmap.fromImage(qimage) + #prof.mark('9') + ###del self.ims + ##self.item.setPixmap(self.pixmap) + + #self.update() + #prof.mark('10') + + #if gotNewData: + ##self.emit(QtCore.SIGNAL('imageChanged')) + #self.sigImageChanged.emit() + + #prof.finish() + + #def getLookupTable(self, num, black, white): + #num = int(num) + #black = int(black) + #white = int(white) + #if white < black: + #b = black + #black = white + #white = b + #key = (num, black, white) + #lut = np.empty(num, dtype=np.ubyte) + #lut[:black] = 0 + #rng = lut[black:white] + #try: + #rng[:] = np.linspace(0, 255, white-black)[:len(rng)] + #except: + #print key, rng.shape + #lut[white:] = 255 + #return lut + + + #def applyColorScaling(self, img, offset, scale): + #try: + #if not ImageItem.useWeave: + #raise Exception('Skipping weave compile') + ##sim = np.ascontiguousarray(self.image) ## should not be needed + #sim = img.reshape(img.size) + ##sim.shape = sim.size + #im = np.empty(sim.shape, dtype=np.ubyte) + #n = im.size + + #code = """ + #for( int i=0; i 255.0 ) + #a = 255.0; + #else if( a < 0.0 ) + #a = 0.0; + #im(i) = a; + #} + #""" + + #weave.inline(code, ['sim', 'im', 'n', 'offset', 'scale'], type_converters=converters.blitz, compiler = 'gcc') + ##sim.shape = shape + #im.shape = img.shape + #except: + #if ImageItem.useWeave: + #ImageItem.useWeave = False + ##sys.excepthook(*sys.exc_info()) + ##print "==============================================================================" + ##print "Weave compile failed, falling back to slower version." + ##img.shape = shape + #im = ((img - offset) * scale).clip(0.,255.).astype(np.ubyte) + #return im + diff --git a/graphicsItems/InfiniteLine.py b/graphicsItems/InfiniteLine.py new file mode 100644 index 00000000..ebf24502 --- /dev/null +++ b/graphicsItems/InfiniteLine.py @@ -0,0 +1,255 @@ +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.Point import Point +from UIGraphicsItem import UIGraphicsItem +import pyqtgraph.functions as fn +import numpy as np +import weakref + + +__all__ = ['InfiniteLine'] +class InfiniteLine(UIGraphicsItem): + """ + Displays a line of infinite length. + This line may be dragged to indicate a position in data coordinates. + """ + + sigDragged = QtCore.Signal(object) + sigPositionChangeFinished = QtCore.Signal(object) + sigPositionChanged = QtCore.Signal(object) + + def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): + """ + Initialization options: + pos - Position of the line. This can be a QPointF or a single value for vertical/horizontal lines. + angle - Angle of line in degrees. 0 is horizontal, 90 is vertical. + pen - Pen to use when drawing line + movable - If True, the line can be dragged to a new position by the user + bounds - Optional [min, max] bounding values. Bounds are only valid if the line is vertical or horizontal. + """ + + UIGraphicsItem.__init__(self) + + if bounds is None: ## allowed value boundaries for orthogonal lines + self.maxRange = [None, None] + else: + self.maxRange = bounds + self.moving = False + self.setMovable(movable) + self.p = [0, 0] + self.setAngle(angle) + if pos is None: + pos = Point(0,0) + self.setPos(pos) + + if pen is None: + pen = (200, 200, 100) + self.setPen(pen) + self.currentPen = self.pen + #self.setFlag(self.ItemSendsScenePositionChanges) + + def setMovable(self, m): + self.movable = m + self.setAcceptHoverEvents(m) + + def setBounds(self, bounds): + """Set the (minimum, maximum) allowable values when dragging.""" + self.maxRange = bounds + self.setValue(self.value()) + + def setPen(self, pen): + self.pen = fn.mkPen(pen) + self.currentPen = self.pen + self.update() + + def setAngle(self, angle): + """ + Takes angle argument in degrees. + 0 is horizontal; 90 is vertical. + + Note that the use of value() and setValue() changes if the line is + not vertical or horizontal. + """ + self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 + self.resetTransform() + self.rotate(self.angle) + self.update() + + def setPos(self, pos): + if type(pos) in [list, tuple]: + newPos = pos + elif isinstance(pos, QtCore.QPointF): + newPos = [pos.x(), pos.y()] + else: + if self.angle == 90: + newPos = [pos, 0] + elif self.angle == 0: + newPos = [0, pos] + else: + raise Exception("Must specify 2D coordinate for non-orthogonal lines.") + + ## check bounds (only works for orthogonal lines) + if self.angle == 90: + if self.maxRange[0] is not None: + newPos[0] = max(newPos[0], self.maxRange[0]) + if self.maxRange[1] is not None: + newPos[0] = min(newPos[0], self.maxRange[1]) + elif self.angle == 0: + if self.maxRange[0] is not None: + newPos[1] = max(newPos[1], self.maxRange[0]) + if self.maxRange[1] is not None: + newPos[1] = min(newPos[1], self.maxRange[1]) + + if self.p != newPos: + self.p = newPos + UIGraphicsItem.setPos(self, Point(self.p)) + self.update() + self.sigPositionChanged.emit(self) + + def getXPos(self): + return self.p[0] + + def getYPos(self): + return self.p[1] + + def getPos(self): + return self.p + + def value(self): + if self.angle%180 == 0: + return self.getYPos() + elif self.angle%180 == 90: + return self.getXPos() + else: + return self.getPos() + + def setValue(self, v): + self.setPos(v) + + ## broken in 4.7 + #def itemChange(self, change, val): + #if change in [self.ItemScenePositionHasChanged, self.ItemSceneHasChanged]: + #self.updateLine() + #print "update", change + #print self.getBoundingParents() + #else: + #print "ignore", change + #return GraphicsObject.itemChange(self, change, val) + + def boundingRect(self): + br = UIGraphicsItem.boundingRect(self) + + ## add a 4-pixel radius around the line for mouse interaction. + + #print "line bounds:", self, br + dt = self.deviceTransform() + if dt is None: + return QtCore.QRectF() + lineDir = Point(dt.map(Point(1, 0)) - dt.map(Point(0,0))) ## direction of line in pixel-space + orthoDir = Point(lineDir[1], -lineDir[0]) ## orthogonal to line in pixel-space + try: + norm = orthoDir.norm() ## direction of one pixel orthogonal to line + except ZeroDivisionError: + return br + + dti = dt.inverted()[0] + px = Point(dti.map(norm)-dti.map(Point(0,0))) ## orthogonal pixel mapped back to item coords + px = px[1] ## project to y-direction + + br.setBottom(-px*4) + br.setTop(px*4) + return br.normalized() + + def paint(self, p, *args): + UIGraphicsItem.paint(self, p, *args) + br = self.boundingRect() + p.setPen(self.currentPen) + p.drawLine(Point(br.right(), 0), Point(br.left(), 0)) + #p.drawRect(self.boundingRect()) + + def dataBounds(self, axis, frac=1.0): + if axis == 0: + return None ## x axis should never be auto-scaled + else: + return (0,0) + + #def mousePressEvent(self, ev): + #if self.movable and ev.button() == QtCore.Qt.LeftButton: + #ev.accept() + #self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) + #else: + #ev.ignore() + + #def mouseMoveEvent(self, ev): + #self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) + ##self.emit(QtCore.SIGNAL('dragged'), self) + #self.sigDragged.emit(self) + #self.hasMoved = True + + #def mouseReleaseEvent(self, ev): + #if self.hasMoved and ev.button() == QtCore.Qt.LeftButton: + #self.hasMoved = False + ##self.emit(QtCore.SIGNAL('positionChangeFinished'), self) + #self.sigPositionChangeFinished.emit(self) + + def mouseDragEvent(self, ev): + if self.movable and ev.button() == QtCore.Qt.LeftButton: + if ev.isStart(): + self.moving = True + self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) + self.startPosition = self.pos() + ev.accept() + + if not self.moving: + return + + #pressDelta = self.mapToParent(ev.buttonDownPos()) - Point(self.p) + self.setPos(self.cursorOffset + self.mapToParent(ev.pos())) + self.sigDragged.emit(self) + if ev.isFinish(): + self.moving = False + self.sigPositionChangeFinished.emit(self) + #else: + #print ev + + + def mouseClickEvent(self, ev): + if self.moving and ev.button() == QtCore.Qt.RightButton: + ev.accept() + self.setPos(self.startPosition) + self.moving = False + self.sigDragged.emit(self) + self.sigPositionChangeFinished.emit(self) + + def hoverEvent(self, ev): + if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): + self.currentPen = fn.mkPen(255, 0,0) + else: + self.currentPen = self.pen + self.update() + + #def hoverEnterEvent(self, ev): + #print "line hover enter" + #ev.ignore() + #self.updateHoverPen() + + #def hoverMoveEvent(self, ev): + #print "line hover move" + #ev.ignore() + #self.updateHoverPen() + + #def hoverLeaveEvent(self, ev): + #print "line hover leave" + #ev.ignore() + #self.updateHoverPen(False) + + #def updateHoverPen(self, hover=None): + #if hover is None: + #scene = self.scene() + #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) + + #if hover: + #self.currentPen = fn.mkPen(255, 0,0) + #else: + #self.currentPen = self.pen + #self.update() + diff --git a/graphicsItems/ItemGroup.py b/graphicsItems/ItemGroup.py new file mode 100644 index 00000000..a328a645 --- /dev/null +++ b/graphicsItems/ItemGroup.py @@ -0,0 +1,23 @@ +from pyqtgraph.Qt import QtGui, QtCore +from GraphicsObject import GraphicsObject + +__all__ = ['ItemGroup'] +class ItemGroup(GraphicsObject): + """ + Replacement for QGraphicsItemGroup + """ + + def __init__(self, *args): + GraphicsObject.__init__(self, *args) + if hasattr(self, "ItemHasNoContents"): + self.setFlag(self.ItemHasNoContents) + + def boundingRect(self): + return QtCore.QRectF() + + def paint(self, *args): + pass + + def addItem(self, item): + item.setParentItem(self) + diff --git a/graphicsItems/LabelItem.py b/graphicsItems/LabelItem.py new file mode 100644 index 00000000..c9d88dd6 --- /dev/null +++ b/graphicsItems/LabelItem.py @@ -0,0 +1,91 @@ +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph.functions as fn +from GraphicsWidget import GraphicsWidget + + +__all__ = ['LabelItem'] + +class LabelItem(GraphicsWidget): + """ + GraphicsWidget displaying text. + Used mainly as axis labels, titles, etc. + + Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use QGraphicsTextItem + with the flag ItemIgnoresTransformations set. + """ + + + def __init__(self, text, parent=None, **args): + GraphicsWidget.__init__(self, parent) + self.item = QtGui.QGraphicsTextItem(self) + self.opts = args + if 'color' not in args: + self.opts['color'] = 'CCC' + else: + if isinstance(args['color'], QtGui.QColor): + self.opts['color'] = fn.colorStr(args['color'])[:6] + self.sizeHint = {} + self.setText(text) + + + def setAttr(self, attr, value): + """Set default text properties. See setText() for accepted parameters.""" + self.opts[attr] = value + + def setText(self, text, **args): + """Set the text and text properties in the label. Accepts optional arguments for auto-generating + a CSS style string: + color: string (example: 'CCFF00') + size: string (example: '8pt') + bold: boolean + italic: boolean + """ + self.text = text + opts = self.opts.copy() + for k in args: + opts[k] = args[k] + + optlist = [] + if 'color' in opts: + optlist.append('color: #' + opts['color']) + if 'size' in opts: + optlist.append('font-size: ' + opts['size']) + if 'bold' in opts and opts['bold'] in [True, False]: + optlist.append('font-weight: ' + {True:'bold', False:'normal'}[opts['bold']]) + if 'italic' in opts and opts['italic'] in [True, False]: + optlist.append('font-style: ' + {True:'italic', False:'normal'}[opts['italic']]) + full = "%s" % ('; '.join(optlist), text) + #print full + self.item.setHtml(full) + self.updateMin() + + def resizeEvent(self, ev): + c1 = self.boundingRect().center() + c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos() + dif = c1 - c2 + self.item.moveBy(dif.x(), dif.y()) + #print c1, c2, dif, self.item.pos() + + def setAngle(self, angle): + self.angle = angle + self.item.resetTransform() + self.item.rotate(angle) + self.updateMin() + + def updateMin(self): + bounds = self.item.mapRectToParent(self.item.boundingRect()) + self.setMinimumWidth(bounds.width()) + self.setMinimumHeight(bounds.height()) + #print self.text, bounds.width(), bounds.height() + + #self.sizeHint = { + #QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), + #QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), + #QtCore.Qt.MaximumSize: (bounds.width()*2, bounds.height()*2), + #QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? + #} + + + #def sizeHint(self, hint, constraint): + #return self.sizeHint[hint] + diff --git a/graphicsItems/LinearRegionItem.py b/graphicsItems/LinearRegionItem.py new file mode 100644 index 00000000..1b546cb7 --- /dev/null +++ b/graphicsItems/LinearRegionItem.py @@ -0,0 +1,232 @@ +from pyqtgraph.Qt import QtGui, QtCore +from UIGraphicsItem import UIGraphicsItem +from InfiniteLine import InfiniteLine +import pyqtgraph.functions as fn + +__all__ = ['LinearRegionItem'] + +class LinearRegionItem(UIGraphicsItem): + """ + Used for marking a horizontal or vertical region in plots. + The region can be dragged and is bounded by lines which can be dragged individually. + """ + + sigRegionChangeFinished = QtCore.Signal(object) + sigRegionChanged = QtCore.Signal(object) + Vertical = 0 + Horizontal = 1 + + def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None): + UIGraphicsItem.__init__(self) + if orientation is None: + orientation = LinearRegionItem.Vertical + self.orientation = orientation + self.bounds = QtCore.QRectF() + self.blockLineSignal = False + self.moving = False + + if orientation == LinearRegionItem.Horizontal: + self.lines = [ + InfiniteLine(QtCore.QPointF(0, values[0]), 0, movable=movable, bounds=bounds), + InfiniteLine(QtCore.QPointF(0, values[1]), 0, movable=movable, bounds=bounds)] + elif orientation == LinearRegionItem.Vertical: + self.lines = [ + InfiniteLine(QtCore.QPointF(values[1], 0), 90, movable=movable, bounds=bounds), + InfiniteLine(QtCore.QPointF(values[0], 0), 90, movable=movable, bounds=bounds)] + else: + raise Exception('Orientation must be one of LinearRegionItem.Vertical or LinearRegionItem.Horizontal') + + + for l in self.lines: + l.setParentItem(self) + l.sigPositionChangeFinished.connect(self.lineMoveFinished) + l.sigPositionChanged.connect(self.lineMoved) + + if brush is None: + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) + self.setBrush(brush) + + self.setMovable(movable) + + def getRegion(self): + """Return the values at the edges of the region.""" + #if self.orientation[0] == 'h': + #r = (self.bounds.top(), self.bounds.bottom()) + #else: + #r = (self.bounds.left(), self.bounds.right()) + r = [self.lines[0].value(), self.lines[1].value()] + return (min(r), max(r)) + + def setRegion(self, rgn): + if self.lines[0].value() == rgn[0] and self.lines[1].value() == rgn[1]: + return + self.blockLineSignal = True + self.lines[0].setValue(rgn[0]) + self.blockLineSignal = False + self.lines[1].setValue(rgn[1]) + #self.blockLineSignal = False + self.lineMoved() + self.lineMoveFinished() + + def setBrush(self, br): + self.brush = fn.mkBrush(br) + self.currentBrush = self.brush + + def setBounds(self, bounds): + for l in self.lines: + l.setBounds(bounds) + + def setMovable(self, m): + for l in self.lines: + l.setMovable(m) + self.movable = m + self.setAcceptHoverEvents(m) + + def boundingRect(self): + br = UIGraphicsItem.boundingRect(self) + rng = self.getRegion() + if self.orientation == LinearRegionItem.Vertical: + br.setLeft(rng[0]) + br.setRight(rng[1]) + else: + br.setTop(rng[0]) + br.setBottom(rng[1]) + return br.normalized() + + def paint(self, p, *args): + UIGraphicsItem.paint(self, p, *args) + p.setBrush(self.currentBrush) + p.drawRect(self.boundingRect()) + + def dataBounds(self, axis, frac=1.0): + if axis == self.orientation: + return self.getRegion() + else: + return None + + def lineMoved(self): + if self.blockLineSignal: + return + self.prepareGeometryChange() + #self.emit(QtCore.SIGNAL('regionChanged'), self) + self.sigRegionChanged.emit(self) + + def lineMoveFinished(self): + #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) + self.sigRegionChangeFinished.emit(self) + + + #def updateBounds(self): + #vb = self.view().viewRect() + #vals = [self.lines[0].value(), self.lines[1].value()] + #if self.orientation[0] == 'h': + #vb.setTop(min(vals)) + #vb.setBottom(max(vals)) + #else: + #vb.setLeft(min(vals)) + #vb.setRight(max(vals)) + #if vb != self.bounds: + #self.bounds = vb + #self.rect.setRect(vb) + + #def mousePressEvent(self, ev): + #if not self.movable: + #ev.ignore() + #return + #for l in self.lines: + #l.mousePressEvent(ev) ## pass event to both lines so they move together + ##if self.movable and ev.button() == QtCore.Qt.LeftButton: + ##ev.accept() + ##self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) + ##else: + ##ev.ignore() + + #def mouseReleaseEvent(self, ev): + #for l in self.lines: + #l.mouseReleaseEvent(ev) + + #def mouseMoveEvent(self, ev): + ##print "move", ev.pos() + #if not self.movable: + #return + #self.lines[0].blockSignals(True) # only want to update once + #for l in self.lines: + #l.mouseMoveEvent(ev) + #self.lines[0].blockSignals(False) + ##self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) + ##self.emit(QtCore.SIGNAL('dragged'), self) + + def mouseDragEvent(self, ev): + if not self.movable or int(ev.button() & QtCore.Qt.LeftButton) == 0: + return + ev.accept() + + if ev.isStart(): + bdp = ev.buttonDownPos() + self.cursorOffsets = [l.pos() - bdp for l in self.lines] + self.startPositions = [l.pos() for l in self.lines] + self.moving = True + + if not self.moving: + return + + #delta = ev.pos() - ev.lastPos() + self.lines[0].blockSignals(True) # only want to update once + for i, l in enumerate(self.lines): + l.setPos(self.cursorOffsets[i] + ev.pos()) + #l.setPos(l.pos()+delta) + #l.mouseDragEvent(ev) + self.lines[0].blockSignals(False) + self.prepareGeometryChange() + + if ev.isFinish(): + self.moving = False + self.sigRegionChangeFinished.emit(self) + else: + self.sigRegionChanged.emit(self) + + def mouseClickEvent(self, ev): + if self.moving and ev.button() == QtCore.Qt.RightButton: + ev.accept() + for i, l in enumerate(self.lines): + l.setPos(self.startPositions[i]) + self.moving = False + self.sigRegionChanged.emit(self) + self.sigRegionChangeFinished.emit(self) + + + def hoverEvent(self, ev): + if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): + c = self.brush.color() + c.setAlpha(c.alpha() * 2) + self.currentBrush = fn.mkBrush(c) + else: + self.currentBrush = self.brush + self.update() + + #def hoverEnterEvent(self, ev): + #print "rgn hover enter" + #ev.ignore() + #self.updateHoverBrush() + + #def hoverMoveEvent(self, ev): + #print "rgn hover move" + #ev.ignore() + #self.updateHoverBrush() + + #def hoverLeaveEvent(self, ev): + #print "rgn hover leave" + #ev.ignore() + #self.updateHoverBrush(False) + + #def updateHoverBrush(self, hover=None): + #if hover is None: + #scene = self.scene() + #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) + + #if hover: + #self.currentBrush = fn.mkBrush(255, 0,0,100) + #else: + #self.currentBrush = self.brush + #self.update() + diff --git a/MultiPlotItem.py b/graphicsItems/MultiPlotItem.py similarity index 74% rename from MultiPlotItem.py rename to graphicsItems/MultiPlotItem.py index 2f73c9e5..aa10c525 100644 --- a/MultiPlotItem.py +++ b/graphicsItems/MultiPlotItem.py @@ -6,8 +6,7 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ from numpy import ndarray -from graphicsItems import * -from PlotItem import * +import GraphicsLayout try: from metaarray import * @@ -16,17 +15,13 @@ except: #raise HAVE_METAARRAY = False - -class MultiPlotItem(QtGui.QGraphicsWidget): - def __init__(self, parent=None): - QtGui.QGraphicsWidget.__init__(self, parent) - self.layout = QtGui.QGraphicsGridLayout() - self.layout.setContentsMargins(1,1,1,1) - self.setLayout(self.layout) - self.layout.setHorizontalSpacing(0) - self.layout.setVerticalSpacing(4) - self.plots = [] +__all__ = ['MultiPlotItem'] +class MultiPlotItem(GraphicsLayout.GraphicsLayout): + """ + Automaticaly generates a grid of plots from a multi-dimensional array + """ + def plot(self, data): #self.layout.clear() self.plots = [] @@ -42,11 +37,12 @@ class MultiPlotItem(QtGui.QGraphicsWidget): break #print "Plotting using axis %d as columns (%d plots)" % (ax, data.shape[ax]) for i in range(data.shape[ax]): - pi = PlotItem() + pi = self.addPlot() + self.nextRow() sl = [slice(None)] * 2 sl[ax] = i pi.plot(data[tuple(sl)]) - self.layout.addItem(pi, i, 0) + #self.layout.addItem(pi, i, 0) self.plots.append((pi, i, 0)) title = None units = None @@ -67,5 +63,7 @@ class MultiPlotItem(QtGui.QGraphicsWidget): for p in self.plots: p[0].close() self.plots = None - for i in range(self.layout.count()): - self.layout.removeAt(i) \ No newline at end of file + self.clear() + + + diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py new file mode 100644 index 00000000..91fb8661 --- /dev/null +++ b/graphicsItems/PlotCurveItem.py @@ -0,0 +1,444 @@ +from pyqtgraph.Qt import QtGui, QtCore +from scipy.fftpack import fft +import numpy as np +import scipy.stats +from GraphicsObject import GraphicsObject +import pyqtgraph.functions as fn +from pyqtgraph import debug +from pyqtgraph.Point import Point +import struct + +__all__ = ['PlotCurveItem'] +class PlotCurveItem(GraphicsObject): + + + """Class representing a single plot curve. Provides: + - Fast data update + - FFT display mode + - shadow pen + - mouse interaction + """ + + sigPlotChanged = QtCore.Signal(object) + sigClicked = QtCore.Signal(object) + + def __init__(self, y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, color=None, clickable=False): + GraphicsObject.__init__(self, parent) + self.clear() + self.path = None + self.fillPath = None + if pen is None: + if color is None: + self.setPen((200,200,200)) + else: + self.setPen(color) + else: + self.setPen(pen) + + self.shadowPen = shadowPen + if y is not None: + self.updateData(y, x, copy) + + ## this is disastrous for performance. + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + + self.fillLevel = fillLevel + self.brush = brush + + self.metaData = {} + self.opts = { + 'spectrumMode': False, + 'logMode': [False, False], + 'pointMode': False, + 'pointStyle': None, + 'downsample': False, + 'alphaHint': 1.0, + 'alphaMode': False + } + + self.setClickable(clickable) + #self.fps = None + + def implements(self, interface=None): + ints = ['plotData'] + if interface is None: + return ints + return interface in ints + + def setClickable(self, s): + self.clickable = s + + + def getData(self): + if self.xData is None: + return (None, None) + if self.xDisp is None: + nanMask = np.isnan(self.xData) | np.isnan(self.yData) + if any(nanMask): + x = self.xData[~nanMask] + y = self.yData[~nanMask] + else: + x = self.xData + y = self.yData + ds = self.opts['downsample'] + if ds > 1: + x = x[::ds] + #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing + y = y[::ds] + if self.opts['spectrumMode']: + f = fft(y) / len(y) + y = abs(f[1:len(f)/2]) + dt = x[-1] - x[0] + x = np.linspace(0, 0.5*len(x)/dt, len(y)) + if self.opts['logMode'][0]: + x = np.log10(x) + if self.opts['logMode'][1]: + y = np.log10(y) + self.xDisp = x + self.yDisp = y + #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() + #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() + return self.xDisp, self.yDisp + + #def generateSpecData(self): + #f = fft(self.yData) / len(self.yData) + #self.ySpec = abs(f[1:len(f)/2]) + #dt = self.xData[-1] - self.xData[0] + #self.xSpec = linspace(0, 0.5*len(self.xData)/dt, len(self.ySpec)) + + def dataBounds(self, ax, frac=1.0): + (x, y) = self.getData() + if x is None or len(x) == 0: + return (0, 0) + + if ax == 0: + d = x + elif ax == 1: + d = y + + if frac >= 1.0: + return (d.min(), d.max()) + elif frac <= 0.0: + raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) + else: + return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + + def setMeta(self, data): + self.metaData = data + + def meta(self): + return self.metaData + + def setPen(self, pen): + self.pen = fn.mkPen(pen) + self.update() + + def setColor(self, color): + self.pen.setColor(color) + self.update() + + def setAlpha(self, alpha, auto): + self.opts['alphaHint'] = alpha + self.opts['alphaMode'] = auto + self.update() + + def setSpectrumMode(self, mode): + self.opts['spectrumMode'] = mode + self.xDisp = self.yDisp = None + self.path = None + self.update() + + def setLogMode(self, mode): + self.opts['logMode'] = mode + self.xDisp = self.yDisp = None + self.path = None + self.update() + + def setPointMode(self, mode): + self.opts['pointMode'] = mode + self.update() + + def setShadowPen(self, pen): + self.shadowPen = pen + self.update() + + def setDownsampling(self, ds): + if self.opts['downsample'] != ds: + self.opts['downsample'] = ds + self.xDisp = self.yDisp = None + self.path = None + self.update() + + def setData(self, x, y, copy=False): + """For Qwt compatibility""" + self.updateData(y, x, copy) + + def updateData(self, data, x=None, copy=False): + prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) + if isinstance(data, list): + data = np.array(data) + if isinstance(x, list): + x = np.array(x) + if not isinstance(data, np.ndarray) or data.ndim > 2: + raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape)) + if x == None: + if 'complex' in str(data.dtype): + raise Exception("Can not plot complex data types.") + else: + if 'complex' in str(data.dtype)+str(x.dtype): + raise Exception("Can not plot complex data types.") + + if data.ndim == 2: ### If data is 2D array, then assume x and y values are in first two columns or rows. + if x is not None: + raise Exception("Plot data may be 2D only if no x argument is supplied.") + ax = 0 + if data.shape[0] > 2 and data.shape[1] == 2: + ax = 1 + ind = [slice(None), slice(None)] + ind[ax] = 0 + y = data[tuple(ind)] + ind[ax] = 1 + x = data[tuple(ind)] + elif data.ndim == 1: + y = data + prof.mark("data checks") + + self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly + ## Test this bug with test_PlotWidget and zoom in on the animated plot + + self.prepareGeometryChange() + if copy: + self.yData = y.view(np.ndarray).copy() + else: + self.yData = y.view(np.ndarray) + + if x is None: + self.xData = np.arange(0, self.yData.shape[0]) + else: + if copy: + self.xData = x.view(np.ndarray).copy() + else: + self.xData = x.view(np.ndarray) + prof.mark('copy') + + + if self.xData.shape != self.yData.shape: + raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) + + self.path = None + self.xDisp = self.yDisp = None + + prof.mark('set') + self.update() + prof.mark('update') + #self.emit(QtCore.SIGNAL('plotChanged'), self) + self.sigPlotChanged.emit(self) + prof.mark('emit') + #prof.finish() + #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) + prof.mark('set cache mode') + prof.finish() + + def generatePath(self, x, y): + prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True) + path = QtGui.QPainterPath() + + ## Create all vertices in path. The method used below creates a binary format so that all + ## vertices can be read in at once. This binary format may change in future versions of Qt, + ## so the original (slower) method is left here for emergencies: + #path.moveTo(x[0], y[0]) + #for i in range(1, y.shape[0]): + # path.lineTo(x[i], y[i]) + + ## Speed this up using >> operator + ## Format is: + ## numVerts(i4) 0(i4) + ## x(f8) y(f8) 0(i4) <-- 0 means this vertex does not connect + ## x(f8) y(f8) 1(i4) <-- 1 means this vertex connects to the previous vertex + ## ... + ## 0(i4) + ## + ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') + + n = x.shape[0] + # create empty array, pad with extra space on either end + arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')]) + # write first two integers + prof.mark('allocate empty') + arr.data[12:20] = struct.pack('>ii', n, 0) + prof.mark('pack header') + # Fill array with vertex values + arr[1:-1]['x'] = x + arr[1:-1]['y'] = y + arr[1:-1]['c'] = 1 + prof.mark('fill array') + # write last 0 + lastInd = 20*(n+1) + arr.data[lastInd:lastInd+4] = struct.pack('>i', 0) + prof.mark('footer') + # create datastream object and stream into path + buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here + prof.mark('create buffer') + ds = QtCore.QDataStream(buf) + prof.mark('create datastream') + ds >> path + prof.mark('load') + + prof.finish() + return path + + + def shape(self): + if self.path is None: + try: + self.path = self.generatePath(*self.getData()) + except: + return QtGui.QPainterPath() + return self.path + + def boundingRect(self): + (x, y) = self.getData() + if x is None or y is None or len(x) == 0 or len(y) == 0: + return QtCore.QRectF() + + + if self.shadowPen is not None: + lineWidth = (max(self.pen.width(), self.shadowPen.width()) + 1) + else: + lineWidth = (self.pen.width()+1) + + + pixels = self.pixelVectors() + if pixels is None: + pixels = [Point(0,0), Point(0,0)] + xmin = x.min() - pixels[0].x() * lineWidth + xmax = x.max() + pixels[0].x() * lineWidth + ymin = y.min() - abs(pixels[1].y()) * lineWidth + ymax = y.max() + abs(pixels[1].y()) * lineWidth + + + return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) + + def paint(self, p, opt, widget): + prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True) + if self.xData is None: + return + #if self.opts['spectrumMode']: + #if self.specPath is None: + + #self.specPath = self.generatePath(*self.getData()) + #path = self.specPath + #else: + x = None + y = None + if self.path is None: + x,y = self.getData() + if x is None or len(x) == 0 or y is None or len(y) == 0: + return + self.path = self.generatePath(x,y) + self.fillPath = None + + + path = self.path + prof.mark('generate path') + + if self.brush is not None: + if self.fillPath is None: + if x is None: + x,y = self.getData() + p2 = QtGui.QPainterPath(self.path) + p2.lineTo(x[-1], self.fillLevel) + p2.lineTo(x[0], self.fillLevel) + p2.closeSubpath() + self.fillPath = p2 + + p.fillPath(self.fillPath, fn.mkBrush(self.brush)) + + if self.shadowPen is not None: + sp = QtGui.QPen(self.shadowPen) + else: + sp = None + + ## Copy pens and apply alpha adjustment + cp = QtGui.QPen(self.pen) + for pen in [sp, cp]: + if pen is None: + continue + c = pen.color() + c.setAlpha(c.alpha() * self.opts['alphaHint']) + pen.setColor(c) + #pen.setCosmetic(True) + + if self.shadowPen is not None: + p.setPen(sp) + p.drawPath(path) + p.setPen(cp) + p.drawPath(path) + prof.mark('drawPath') + + #print "Render hints:", int(p.renderHints()) + prof.finish() + #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) + #p.drawRect(self.boundingRect()) + + + def clear(self): + self.xData = None ## raw values + self.yData = None + self.xDisp = None ## display values (after log / fft) + self.yDisp = None + self.path = None + #del self.xData, self.yData, self.xDisp, self.yDisp, self.path + + #def mousePressEvent(self, ev): + ##GraphicsObject.mousePressEvent(self, ev) + #if not self.clickable: + #ev.ignore() + #if ev.button() != QtCore.Qt.LeftButton: + #ev.ignore() + #self.mousePressPos = ev.pos() + #self.mouseMoved = False + + #def mouseMoveEvent(self, ev): + ##GraphicsObject.mouseMoveEvent(self, ev) + #self.mouseMoved = True + ##print "move" + + #def mouseReleaseEvent(self, ev): + ##GraphicsObject.mouseReleaseEvent(self, ev) + #if not self.mouseMoved: + #self.sigClicked.emit(self) + + def mouseClickEvent(self, ev): + if not self.clickable or ev.button() != QtCore.Qt.LeftButton: + return + ev.accept() + self.sigClicked.emit(self) + + + +class ROIPlotItem(PlotCurveItem): + """Plot curve that monitors an ROI and image for changes to automatically replot.""" + def __init__(self, roi, data, img, axes=(0,1), xVals=None, color=None): + self.roi = roi + self.roiData = data + self.roiImg = img + self.axes = axes + self.xVals = xVals + PlotCurveItem.__init__(self, self.getRoiData(), x=self.xVals, color=color) + #roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) + roi.sigRegionChanged.connect(self.roiChangedEvent) + #self.roiChangedEvent() + + def getRoiData(self): + d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes) + if d is None: + return + while d.ndim > 1: + d = d.mean(axis=1) + return d + + def roiChangedEvent(self): + d = self.getRoiData() + self.updateData(d, self.xVals) + diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py new file mode 100644 index 00000000..91ea77a7 --- /dev/null +++ b/graphicsItems/PlotDataItem.py @@ -0,0 +1,534 @@ +try: + import metaarray + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + +from pyqtgraph.Qt import QtCore +from GraphicsObject import GraphicsObject +from PlotCurveItem import PlotCurveItem +from ScatterPlotItem import ScatterPlotItem +import numpy as np +import scipy +import pyqtgraph.functions as fn + +class PlotDataItem(GraphicsObject): + """GraphicsItem for displaying plot curves, scatter plots, or both.""" + + sigPlotChanged = QtCore.Signal(object) + sigClicked = QtCore.Signal(object) + + def __init__(self, *args, **kargs): + """ + There are many different ways to create a PlotDataItem: + + Data initialization: (x,y data only) + + =================================== ====================================== + PlotDataItem(xValues, yValues) x and y values may be any sequence (including ndarray) of real numbers + PlotDataItem(yValues) y values only -- x will be automatically set to range(len(y)) + PlotDataItem(x=xValues, y=yValues) x and y given by keyword arguments + PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where x=data[:,0] and y=data[:,1] + =================================== ====================================== + + Data initialization: (x,y data AND may include spot style) + + =========================== ========================================= + PlotDataItem(recarray) numpy array with dtype=[('x', float), ('y', float), ...] + PlotDataItem(list-of-dicts) [{'x': x, 'y': y, ...}, ...] + PlotDataItem(dict-of-lists) {'x': [...], 'y': [...], ...} + PlotDataItem(MetaArray) 1D array of Y values with X sepecified as axis values + OR 2D array with a column 'y' and extra columns as needed. + =========================== ========================================= + + Line style keyword + ========== ================================================ + pen pen to use for drawing line between points. Default is solid grey, 1px width. Use None to disable line drawing. + shadowPen pen for secondary line to draw behind the primary line. disabled by default. + fillLevel fill the area between the curve and fillLevel + fillBrush fill to use when fillLevel is specified + ========== ================================================ + + Point style keyword arguments: + + ============ ================================================ + symbol symbol to use for drawing points OR list of symbols, one per point. Default is no symbol. + options are o, s, t, d, + + symbolPen outline pen for drawing points OR list of pens, one per point + symbolBrush brush for filling points OR list of brushes, one per point + symbolSize diameter of symbols OR list of diameters + pxMode (bool) If True, then symbolSize is specified in pixels. If False, then symbolSize is + specified in data coordinates. + ============ ================================================ + + Optimization keyword arguments: + + ========== ================================================ + identical spots are all identical. The spot image will be rendered only once and repeated for every point + decimate (int) decimate data + ========== ================================================ + + Meta-info keyword arguments: + + ========== ================================================ + name name of dataset. This would appear in a legend + ========== ================================================ + """ + GraphicsObject.__init__(self) + self.setFlag(self.ItemHasNoContents) + self.xData = None + self.yData = None + self.curves = [] + self.scatters = [] + self.clear() + self.opts = { + 'fftMode': False, + 'logMode': [False, False], + 'downsample': False, + 'alphaHint': 1.0, + 'alphaMode': False, + + 'pen': (200,200,200), + 'shadowPen': None, + 'fillLevel': None, + 'brush': None, + + 'symbol': None, + 'symbolSize': 10, + 'symbolPen': (200,200,200), + 'symbolBrush': (50, 50, 150), + 'identical': False, + } + self.setData(*args, **kargs) + + def implements(self, interface=None): + ints = ['plotData'] + if interface is None: + return ints + return interface in ints + + def boundingRect(self): + return QtCore.QRectF() ## let child items handle this + + def setAlpha(self, alpha, auto): + self.opts['alphaHint'] = alpha + self.opts['alphaMode'] = auto + self.setOpacity(alpha) + #self.update() + + def setFftMode(self, mode): + self.opts['fftMode'] = mode + self.xDisp = self.yDisp = None + self.updateItems() + + def setLogMode(self, mode): + self.opts['logMode'] = mode + self.xDisp = self.yDisp = None + self.updateItems() + + def setPointMode(self, mode): + self.opts['pointMode'] = mode + self.update() + + def setPen(self, pen): + """ + | Sets the pen used to draw lines between points. + | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() ` + """ + self.opts['pen'] = fn.mkPen(pen) + for c in self.curves: + c.setPen(pen) + self.update() + + def setShadowPen(self, pen): + """ + | Sets the shadow pen used to draw lines between points (this is for enhancing contrast or + emphacizing data). + | This line is drawn behind the primary pen (see :func:`setPen() `) + and should generally be assigned greater width than the primary pen. + | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() ` + """ + self.opts['shadowPen'] = pen + for c in self.curves: + c.setPen(pen) + self.update() + + def setDownsampling(self, ds): + if self.opts['downsample'] != ds: + self.opts['downsample'] = ds + self.xDisp = self.yDisp = None + self.updateItems() + + def setData(self, *args, **kargs): + """ + Clear any data displayed by this item and display new data. + See :func:`__init__() ` for details; it accepts the same arguments. + """ + + self.clear() + + y = None + x = None + if len(args) == 1: + data = args[0] + dt = dataType(data) + if dt == 'empty': + return + elif dt == 'listOfValues': + y = np.array(data) + elif dt == 'Nx2array': + x = data[:,0] + y = data[:,1] + elif dt == 'recarray' or dt == 'dictOfLists': + if 'x' in data: + x = np.array(data['x']) + if 'y' in data: + y = np.array(data['y']) + elif dt == 'listOfDicts': + if 'x' in data[0]: + x = np.array([d.get('x',None) for d in data]) + if 'y' in data[0]: + y = np.array([d.get('y',None) for d in data]) + elif dt == 'MetaArray': + y = data.view(np.ndarray) + x = data.xvals(0).view(np.ndarray) + else: + raise Exception('Invalid data type %s' % type(data)) + + elif len(args) == 2: + seq = ('listOfValues', 'MetaArray') + if dataType(args[0]) not in seq or dataType(args[1]) not in seq: + raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1])))) + if not isinstance(args[0], np.ndarray): + x = np.array(args[0]) + else: + x = args[0].view(np.ndarray) + if not isinstance(args[1], np.ndarray): + y = np.array(args[1]) + else: + y = args[1].view(np.ndarray) + + if 'x' in kargs: + x = kargs['x'] + if 'y' in kargs: + y = kargs['y'] + + + ## pull in all style arguments. + ## Use self.opts to fill in anything not present in kargs. + + + ## if symbol pen/brush are given with no symbol, then assume symbol is 'o' + if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs): + kargs['symbol'] = 'o' + + for k in self.opts.keys(): + if k in kargs: + self.opts[k] = kargs[k] + + #curveArgs = {} + #for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: + #if k in kargs: + #self.opts[k] = kargs[k] + #curveArgs[k] = self.opts[k] + + #scatterArgs = {} + #for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol')]: + #if k in kargs: + #self.opts[k] = kargs[k] + #scatterArgs[v] = self.opts[k] + + + if y is None: + return + if y is not None and x is None: + x = np.arange(len(y)) + + if isinstance(x, list): + x = np.array(x) + if isinstance(y, list): + y = np.array(y) + + self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by + self.yData = y.view(np.ndarray) + + self.updateItems() + view = self.getViewBox() + if view is not None: + view.itemBoundsChanged(self) ## inform view so it can update its range if it wants + self.sigPlotChanged.emit(self) + + + def updateItems(self): + for c in self.curves+self.scatters: + if c.scene() is not None: + c.scene().removeItem(c) + + curveArgs = {} + for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: + curveArgs[k] = self.opts[k] + + scatterArgs = {} + for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol')]: + scatterArgs[v] = self.opts[k] + + x,y = self.getData() + + if curveArgs['pen'] is not None or curveArgs['brush'] is not None: + curve = PlotCurveItem(x=x, y=y, **curveArgs) + curve.setParentItem(self) + self.curves.append(curve) + + if scatterArgs['symbol'] is not None: + sp = ScatterPlotItem(x=x, y=y, **scatterArgs) + sp.setParentItem(self) + self.scatters.append(sp) + + + def getData(self): + if self.xData is None: + return (None, None) + if self.xDisp is None: + nanMask = np.isnan(self.xData) | np.isnan(self.yData) + if any(nanMask): + x = self.xData[~nanMask] + y = self.yData[~nanMask] + else: + x = self.xData + y = self.yData + ds = self.opts['downsample'] + if ds > 1: + x = x[::ds] + #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing + y = y[::ds] + if self.opts['fftMode']: + f = np.fft.fft(y) / len(y) + y = abs(f[1:len(f)/2]) + dt = x[-1] - x[0] + x = np.linspace(0, 0.5*len(x)/dt, len(y)) + if self.opts['logMode'][0]: + x = np.log10(x) + if self.opts['logMode'][1]: + y = np.log10(y) + self.xDisp = x + self.yDisp = y + #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() + #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() + return self.xDisp, self.yDisp + + def dataBounds(self, ax, frac=1.0): + (x, y) = self.getData() + if x is None or len(x) == 0: + return (0, 0) + + if ax == 0: + d = x + elif ax == 1: + d = y + + if frac >= 1.0: + return (np.min(d), np.max(d)) + elif frac <= 0.0: + raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) + else: + return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + + + def clear(self): + for i in self.curves+self.scatters: + if i.scene() is not None: + i.scene().removeItem(i) + self.curves = [] + self.scatters = [] + self.xData = None + self.yData = None + self.xDisp = None + self.yDisp = None + + def appendData(self, *args, **kargs): + pass + + +def dataType(obj): + if hasattr(obj, '__len__') and len(obj) == 0: + return 'empty' + if isSequence(obj): + first = obj[0] + if isinstance(obj, np.ndarray): + if HAVE_METAARRAY and isinstance(obj, metaarray.MetaArray): + return 'MetaArray' + if obj.ndim == 1: + if obj.dtype.names is None: + return 'listOfValues' + else: + return 'recarray' + elif obj.ndim == 2 and obj.dtype.names is None and obj.shape[1] == 2: + return 'Nx2array' + else: + raise Exception('array shape must be (N,) or (N,2); got %s instead' % str(obj.shape)) + elif isinstance(first, dict): + return 'listOfDict' + else: + return 'listOfValues' + elif isinstance(obj, dict): + return 'dict' + + +def isSequence(obj): + return isinstance(obj, list) or isinstance(obj, np.ndarray) + + + +#class TableData: + #""" + #Class for presenting multiple forms of tabular data through a consistent interface. + #May contain: + #- numpy record array + #- list-of-dicts (all dicts are _not_ required to have the same keys) + #- dict-of-lists + #- dict (single record) + #Note: if all the values in this record are lists, it will be interpreted as multiple records + + #Data can be accessed and modified by column, by row, or by value + #data[columnName] + #data[rowId] + #data[columnName, rowId] = value + #data[columnName] = [value, value, ...] + #data[rowId] = {columnName: value, ...} + #""" + + #def __init__(self, data): + #self.data = data + #if isinstance(data, np.ndarray): + #self.mode = 'array' + #elif isinstance(data, list): + #self.mode = 'list' + #elif isinstance(data, dict): + #types = set(map(type, data.values())) + ### dict may be a dict-of-lists or a single record + #types -= set([list, np.ndarray]) ## if dict contains any non-sequence values, it is probably a single record. + #if len(types) != 0: + #self.data = [self.data] + #self.mode = 'list' + #else: + #self.mode = 'dict' + #elif isinstance(data, TableData): + #self.data = data.data + #self.mode = data.mode + #else: + #raise TypeError(type(data)) + + #for fn in ['__getitem__', '__setitem__']: + #setattr(self, fn, getattr(self, '_TableData'+fn+self.mode)) + + #def originalData(self): + #return self.data + + #def toArray(self): + #if self.mode == 'array': + #return self.data + #if len(self) < 1: + ##return np.array([]) ## need to return empty array *with correct columns*, but this is very difficult, so just return None + #return None + #rec1 = self[0] + #dtype = functions.suggestRecordDType(rec1) + ##print rec1, dtype + #arr = np.empty(len(self), dtype=dtype) + #arr[0] = tuple(rec1.values()) + #for i in xrange(1, len(self)): + #arr[i] = tuple(self[i].values()) + #return arr + + #def __getitem__array(self, arg): + #if isinstance(arg, tuple): + #return self.data[arg[0]][arg[1]] + #else: + #return self.data[arg] + + #def __getitem__list(self, arg): + #if isinstance(arg, basestring): + #return [d.get(arg, None) for d in self.data] + #elif isinstance(arg, int): + #return self.data[arg] + #elif isinstance(arg, tuple): + #arg = self._orderArgs(arg) + #return self.data[arg[0]][arg[1]] + #else: + #raise TypeError(type(arg)) + + #def __getitem__dict(self, arg): + #if isinstance(arg, basestring): + #return self.data[arg] + #elif isinstance(arg, int): + #return dict([(k, v[arg]) for k, v in self.data.iteritems()]) + #elif isinstance(arg, tuple): + #arg = self._orderArgs(arg) + #return self.data[arg[1]][arg[0]] + #else: + #raise TypeError(type(arg)) + + #def __setitem__array(self, arg, val): + #if isinstance(arg, tuple): + #self.data[arg[0]][arg[1]] = val + #else: + #self.data[arg] = val + + #def __setitem__list(self, arg, val): + #if isinstance(arg, basestring): + #if len(val) != len(self.data): + #raise Exception("Values (%d) and data set (%d) are not the same length." % (len(val), len(self.data))) + #for i, rec in enumerate(self.data): + #rec[arg] = val[i] + #elif isinstance(arg, int): + #self.data[arg] = val + #elif isinstance(arg, tuple): + #arg = self._orderArgs(arg) + #self.data[arg[0]][arg[1]] = val + #else: + #raise TypeError(type(arg)) + + #def __setitem__dict(self, arg, val): + #if isinstance(arg, basestring): + #if len(val) != len(self.data[arg]): + #raise Exception("Values (%d) and data set (%d) are not the same length." % (len(val), len(self.data[arg]))) + #self.data[arg] = val + #elif isinstance(arg, int): + #for k in self.data: + #self.data[k][arg] = val[k] + #elif isinstance(arg, tuple): + #arg = self._orderArgs(arg) + #self.data[arg[1]][arg[0]] = val + #else: + #raise TypeError(type(arg)) + + #def _orderArgs(self, args): + ### return args in (int, str) order + #if isinstance(args[0], basestring): + #return (args[1], args[0]) + #else: + #return args + + #def __iter__(self): + #for i in xrange(len(self)): + #yield self[i] + + #def __len__(self): + #if self.mode == 'array' or self.mode == 'list': + #return len(self.data) + #else: + #return max(map(len, self.data.values())) + + #def columnNames(self): + #"""returns column names in no particular order""" + #if self.mode == 'array': + #return self.data.dtype.names + #elif self.mode == 'list': + #names = set() + #for row in self.data: + #names.update(row.keys()) + #return list(names) + #elif self.mode == 'dict': + #return self.data.keys() + + #def keys(self): + #return self.columnNames() \ No newline at end of file diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py new file mode 100644 index 00000000..2e15f016 --- /dev/null +++ b/graphicsItems/PlotItem/PlotItem.py @@ -0,0 +1,1389 @@ +# -*- coding: utf-8 -*- +""" +PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers. +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +This class is one of the workhorses of pyqtgraph. It implements a graphics item with +plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want +a widget that can be added to your GUI, see PlotWidget instead. + +This class is very heavily featured: + - Automatically creates and manages PlotCurveItems + - Fast display and update of plots + - Manages zoom/pan ViewBox, scale, and label elements + - Automatic scaling when data changes + - Control panel with a huge feature set including averaging, decimation, + display, power spectrum, svg/png export, plot linking, and more. +""" +#from graphicsItems import * +from plotConfigTemplate import * +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import pyqtgraph.functions as fn +from pyqtgraph.widgets.FileDialog import FileDialog +import weakref +#from types import * +import numpy as np +#from .. PlotCurveItem import PlotCurveItem +#from .. ScatterPlotItem import ScatterPlotItem +from .. PlotDataItem import PlotDataItem +from .. ViewBox import ViewBox +from .. AxisItem import AxisItem +from .. LabelItem import LabelItem +from .. GraphicsWidget import GraphicsWidget +from .. ButtonItem import ButtonItem +from pyqtgraph.WidgetGroup import WidgetGroup +import collections + +__all__ = ['PlotItem'] + +#try: + #from WidgetGroup import * + #HAVE_WIDGETGROUP = True +#except: + #HAVE_WIDGETGROUP = False + +try: + from metaarray import * + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + + + + +class PlotItem(GraphicsWidget): + + sigYRangeChanged = QtCore.Signal(object, object) + sigXRangeChanged = QtCore.Signal(object, object) + sigRangeChanged = QtCore.Signal(object, object) + + """Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox.""" + lastFileDir = None + managers = {} + + def __init__(self, parent=None, name=None, labels=None, title=None, **kargs): + GraphicsWidget.__init__(self, parent) + + self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + + ## Set up control buttons + path = os.path.dirname(__file__) + #self.ctrlBtn = ButtonItem(os.path.join(path, 'ctrl.png'), 14, self) + #self.ctrlBtn.clicked.connect(self.ctrlBtnClicked) + self.autoImageFile = os.path.join(path, 'auto.png') + self.lockImageFile = os.path.join(path, 'lock.png') + self.autoBtn = ButtonItem(self.autoImageFile, 14, self) + self.autoBtn.mode = 'auto' + self.autoBtn.clicked.connect(self.autoBtnClicked) + + self.layout = QtGui.QGraphicsGridLayout() + self.layout.setContentsMargins(1,1,1,1) + self.setLayout(self.layout) + self.layout.setHorizontalSpacing(0) + self.layout.setVerticalSpacing(0) + + self.vb = ViewBox(name=name) + #self.vb.sigXRangeChanged.connect(self.xRangeChanged) + #self.vb.sigYRangeChanged.connect(self.yRangeChanged) + #self.vb.sigRangeChangedManually.connect(self.enableManualScale) + #self.vb.sigRangeChanged.connect(self.viewRangeChanged) + + self.layout.addItem(self.vb, 2, 1) + self.alpha = 1.0 + self.autoAlpha = True + self.spectrumMode = False + + #self.autoScale = [True, True] + + ## Create and place scale items + self.scales = { + 'top': {'item': AxisItem(orientation='top', linkView=self.vb), 'pos': (1, 1)}, + 'bottom': {'item': AxisItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)}, + 'left': {'item': AxisItem(orientation='left', linkView=self.vb), 'pos': (2, 0)}, + 'right': {'item': AxisItem(orientation='right', linkView=self.vb), 'pos': (2, 2)} + } + for k in self.scales: + item = self.scales[k]['item'] + self.layout.addItem(item, *self.scales[k]['pos']) + item.setZValue(-1000) + item.setFlag(item.ItemNegativeZStacksBehindParent) + + self.titleLabel = LabelItem('', size='11pt') + self.layout.addItem(self.titleLabel, 0, 1) + self.setTitle(None) ## hide + + + for i in range(4): + self.layout.setRowPreferredHeight(i, 0) + self.layout.setRowMinimumHeight(i, 0) + self.layout.setRowSpacing(i, 0) + self.layout.setRowStretchFactor(i, 1) + + for i in range(3): + self.layout.setColumnPreferredWidth(i, 0) + self.layout.setColumnMinimumWidth(i, 0) + self.layout.setColumnSpacing(i, 0) + self.layout.setColumnStretchFactor(i, 1) + self.layout.setRowStretchFactor(2, 100) + self.layout.setColumnStretchFactor(1, 100) + + + ## Wrap a few methods from viewBox + for m in [ + 'setXRange', 'setYRange', 'setXLink', 'setYLink', + 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', + 'enableAutoRange', 'disableAutoRange']: + setattr(self, m, getattr(self.vb, m)) + + self.items = [] + self.curves = [] + self.itemMeta = weakref.WeakKeyDictionary() + self.dataItems = [] + self.paramList = {} + self.avgCurves = {} + + ### Set up context menu + + w = QtGui.QWidget() + self.ctrl = c = Ui_Form() + c.setupUi(w) + dv = QtGui.QDoubleValidator(self) + + menuItems = [ + ('Fourier Transform', c.powerSpectrumGroup), + ('Downsample', c.decimateGroup), + ('Average', c.averageGroup), + ('Alpha', c.alphaGroup), + ('Grid', c.gridGroup), + ('Points', c.pointsGroup), + ] + + + self.ctrlMenu = QtGui.QMenu() + + self.ctrlMenu.setTitle('Plot Options') + self.subMenus = [] + for name, grp in menuItems: + sm = QtGui.QMenu(name) + act = QtGui.QWidgetAction(self) + act.setDefaultWidget(grp) + sm.addAction(act) + self.subMenus.append(sm) + self.ctrlMenu.addMenu(sm) + + + exportOpts = collections.OrderedDict([ + ('SVG - Full Plot', self.saveSvgClicked), + ('SVG - Curves Only', self.saveSvgCurvesClicked), + ('Image', self.saveImgClicked), + ('CSV', self.saveCsvClicked), + ]) + + self.vb.menu.setExportMethods(exportOpts) + + #self.menuAction = QtGui.QWidgetAction(self) + #self.menuAction.setDefaultWidget(w) + #self.ctrlMenu.addAction(self.menuAction) + + #if HAVE_WIDGETGROUP: + self.stateGroup = WidgetGroup() + for name, w in menuItems: + self.stateGroup.autoAdd(w) + + self.fileDialog = None + + #self.xLinkPlot = None + #self.yLinkPlot = None + #self.linksBlocked = False + + #self.setAcceptHoverEvents(True) + + ## Connect control widgets + #c.xMinText.editingFinished.connect(self.setManualXScale) + #c.xMaxText.editingFinished.connect(self.setManualXScale) + #c.yMinText.editingFinished.connect(self.setManualYScale) + #c.yMaxText.editingFinished.connect(self.setManualYScale) + + #c.xManualRadio.clicked.connect(lambda: self.updateXScale()) + #c.yManualRadio.clicked.connect(lambda: self.updateYScale()) + + #c.xAutoRadio.clicked.connect(self.updateXScale) + #c.yAutoRadio.clicked.connect(self.updateYScale) + + #c.xAutoPercentSpin.valueChanged.connect(self.replot) + #c.yAutoPercentSpin.valueChanged.connect(self.replot) + + c.alphaGroup.toggled.connect(self.updateAlpha) + c.alphaSlider.valueChanged.connect(self.updateAlpha) + c.autoAlphaCheck.toggled.connect(self.updateAlpha) + + c.gridGroup.toggled.connect(self.updateGrid) + c.gridAlphaSlider.valueChanged.connect(self.updateGrid) + + c.powerSpectrumGroup.toggled.connect(self.updateSpectrumMode) + #c.saveSvgBtn.clicked.connect(self.saveSvgClicked) + #c.saveSvgCurvesBtn.clicked.connect(self.saveSvgCurvesClicked) + #c.saveImgBtn.clicked.connect(self.saveImgClicked) + #c.saveCsvBtn.clicked.connect(self.saveCsvClicked) + + #self.ctrl.xLinkCombo.currentIndexChanged.connect(self.xLinkComboChanged) + #self.ctrl.yLinkCombo.currentIndexChanged.connect(self.yLinkComboChanged) + + c.downsampleSpin.valueChanged.connect(self.updateDownsampling) + + self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked) + self.ctrl.averageGroup.toggled.connect(self.avgToggled) + + self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) + self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) + #c.xMouseCheck.toggled.connect(self.mouseCheckChanged) + #c.yMouseCheck.toggled.connect(self.mouseCheckChanged) + + #self.xLinkPlot = None + #self.yLinkPlot = None + #self.linksBlocked = False + self.manager = None + + self.hideAxis('right') + self.hideAxis('top') + self.showAxis('left') + self.showAxis('bottom') + + #if name is not None: + #self.registerPlot(name) + + if labels is not None: + for k in labels: + if isinstance(labels[k], basestring): + labels[k] = (labels[k],) + self.setLabel(k, *labels[k]) + + if title is not None: + self.setTitle(title) + + if len(kargs) > 0: + self.plot(**kargs) + + self.enableAutoRange() + + def implements(self, interface=None): + return interface in ['ViewBoxWrapper'] + + def getViewBox(self): + return self.vb + + + #def paint(self, *args): + #prof = debug.Profiler('PlotItem.paint', disabled=True) + #QtGui.QGraphicsWidget.paint(self, *args) + #prof.finish() + + + def close(self): + #print "delete", self + ## Most of this crap is needed to avoid PySide trouble. + ## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets) + ## the solution is to manually remove all widgets before scene.clear() is called + if self.ctrlMenu is None: ## already shut down + return + self.ctrlMenu.setParent(None) + self.ctrlMenu = None + + #self.ctrlBtn.setParent(None) + #self.ctrlBtn = None + #self.autoBtn.setParent(None) + #self.autoBtn = None + + for k in self.scales: + i = self.scales[k]['item'] + i.close() + + self.scales = None + self.scene().removeItem(self.vb) + self.vb = None + + ## causes invalid index errors: + #for i in range(self.layout.count()): + #self.layout.removeAt(i) + + #for p in self.proxies: + #try: + #p.setWidget(None) + #except RuntimeError: + #break + #self.scene().removeItem(p) + #self.proxies = [] + + #self.menuAction.releaseWidget(self.menuAction.defaultWidget()) + #self.menuAction.setParent(None) + #self.menuAction = None + + #if self.manager is not None: + #self.manager.sigWidgetListChanged.disconnect(self.updatePlotList) + #self.manager.removeWidget(self.name) + #else: + #print "no manager" + + def registerPlot(self, name): + self.vb.register(name) + #self.name = name + #win = str(self.window()) + ##print "register", name, win + #if win not in PlotItem.managers: + #PlotItem.managers[win] = PlotWidgetManager() + #self.manager = PlotItem.managers[win] + #self.manager.addWidget(self, name) + ##QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList) + #self.manager.sigWidgetListChanged.connect(self.updatePlotList) + #self.updatePlotList() + + #def updatePlotList(self): + #"""Update the list of all plotWidgets in the "link" combos""" + ##print "update plot list", self + #try: + #for sc in [self.ctrl.xLinkCombo, self.ctrl.yLinkCombo]: + #current = unicode(sc.currentText()) + #sc.blockSignals(True) + #try: + #sc.clear() + #sc.addItem("") + #if self.manager is not None: + #for w in self.manager.listWidgets(): + ##print w + #if w == self.name: + #continue + #sc.addItem(w) + #if w == current: + #sc.setCurrentIndex(sc.count()-1) + #finally: + #sc.blockSignals(False) + #if unicode(sc.currentText()) != current: + #sc.currentItemChanged.emit() + #except: + #import gc + #refs= gc.get_referrers(self) + #print " error during update of", self + #print " Referrers are:", refs + #raise + + def updateGrid(self, *args): + g = self.ctrl.gridGroup.isChecked() + if g: + g = self.ctrl.gridAlphaSlider.value() + for k in self.scales: + self.scales[k]['item'].setGrid(g) + + def viewGeometry(self): + """return the screen geometry of the viewbox""" + v = self.scene().views()[0] + b = self.vb.mapRectToScene(self.vb.boundingRect()) + wr = v.mapFromScene(b).boundingRect() + pos = v.mapToGlobal(v.pos()) + wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) + return wr + + + + + + #def viewRangeChanged(self, vb, range): + ##self.emit(QtCore.SIGNAL('viewChanged'), *args) + #self.sigRangeChanged.emit(self, range) + + #def blockLink(self, b): + #self.linksBlocked = b + + #def xLinkComboChanged(self): + #self.setXLink(str(self.ctrl.xLinkCombo.currentText())) + + #def yLinkComboChanged(self): + #self.setYLink(str(self.ctrl.yLinkCombo.currentText())) + + #def setXLink(self, plot=None): + #"""Link this plot's X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" + #if isinstance(plot, basestring): + #if self.manager is None: + #return + #if self.xLinkPlot is not None: + #self.manager.unlinkX(self, self.xLinkPlot) + #plot = self.manager.getWidget(plot) + #if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): + #plot = plot.getPlotItem() + #self.xLinkPlot = plot + #if plot is not None: + #self.setManualXScale() + #self.manager.linkX(self, plot) + + + + #def setYLink(self, plot=None): + #"""Link this plot's Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" + #if isinstance(plot, basestring): + #if self.manager is None: + #return + #if self.yLinkPlot is not None: + #self.manager.unlinkY(self, self.yLinkPlot) + #plot = self.manager.getWidget(plot) + #if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): + #plot = plot.getPlotItem() + #self.yLinkPlot = plot + #if plot is not None: + #self.setManualYScale() + #self.manager.linkY(self, plot) + + #def linkXChanged(self, plot): + #"""Called when a linked plot has changed its X scale""" + ##print "update from", plot + #if self.linksBlocked: + #return + #pr = plot.vb.viewRect() + #pg = plot.viewGeometry() + #if pg is None: + ##print " return early" + #return + #sg = self.viewGeometry() + #upp = float(pr.width()) / pg.width() + #x1 = pr.left() + (sg.x()-pg.x()) * upp + #x2 = x1 + sg.width() * upp + #plot.blockLink(True) + #self.setManualXScale() + #self.setXRange(x1, x2, padding=0) + #plot.blockLink(False) + #self.replot() + + #def linkYChanged(self, plot): + #"""Called when a linked plot has changed its Y scale""" + #if self.linksBlocked: + #return + #pr = plot.vb.viewRect() + #pg = plot.vb.boundingRect() + #sg = self.vb.boundingRect() + #upp = float(pr.height()) / pg.height() + #y1 = pr.bottom() + (sg.y()-pg.y()) * upp + #y2 = y1 + sg.height() * upp + #plot.blockLink(True) + #self.setManualYScale() + #self.setYRange(y1, y2, padding=0) + #plot.blockLink(False) + #self.replot() + + + def avgToggled(self, b): + if b: + self.recomputeAverages() + for k in self.avgCurves: + self.avgCurves[k][1].setVisible(b) + + def avgParamListClicked(self, item): + name = str(item.text()) + self.paramList[name] = (item.checkState() == QtCore.Qt.Checked) + self.recomputeAverages() + + def recomputeAverages(self): + if not self.ctrl.averageGroup.isChecked(): + return + for k in self.avgCurves: + self.removeItem(self.avgCurves[k][1]) + self.avgCurves = {} + for c in self.curves: + self.addAvgCurve(c) + self.replot() + + def addAvgCurve(self, curve): + """Add a single curve into the pool of curves averaged together""" + + ## If there are plot parameters, then we need to determine which to average together. + remKeys = [] + addKeys = [] + if self.ctrl.avgParamList.count() > 0: + + ### First determine the key of the curve to which this new data should be averaged + for i in range(self.ctrl.avgParamList.count()): + item = self.ctrl.avgParamList.item(i) + if item.checkState() == QtCore.Qt.Checked: + remKeys.append(str(item.text())) + else: + addKeys.append(str(item.text())) + + if len(remKeys) < 1: ## In this case, there would be 1 average plot for each data plot; not useful. + return + + p = self.itemMeta.get(curve,{}).copy() + for k in p: + if type(k) is tuple: + p['.'.join(k)] = p[k] + del p[k] + for rk in remKeys: + if rk in p: + del p[rk] + for ak in addKeys: + if ak not in p: + p[ak] = None + key = tuple(p.items()) + + ### Create a new curve if needed + if key not in self.avgCurves: + plot = PlotDataItem() + plot.setPen(fn.mkPen([0, 200, 0])) + plot.setShadowPen(fn.mkPen([0, 0, 0, 100], width=3)) + plot.setAlpha(1.0, False) + plot.setZValue(100) + self.addItem(plot, skipAverage=True) + self.avgCurves[key] = [0, plot] + self.avgCurves[key][0] += 1 + (n, plot) = self.avgCurves[key] + + ### Average data together + (x, y) = curve.getData() + if plot.yData is not None: + newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n) + plot.setData(plot.xData, newData) + else: + plot.setData(x, y) + + + #def mouseCheckChanged(self): + #state = [self.ctrl.xMouseCheck.isChecked(), self.ctrl.yMouseCheck.isChecked()] + #self.vb.setMouseEnabled(*state) + + #def xRangeChanged(self, _, range): + #if any(np.isnan(range)) or any(np.isinf(range)): + #raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) + #self.ctrl.xMinText.setText('%0.5g' % range[0]) + #self.ctrl.xMaxText.setText('%0.5g' % range[1]) + + ### automatically change unit scale + #maxVal = max(abs(range[0]), abs(range[1])) + #(scale, prefix) = fn.siScale(maxVal) + ##for l in ['top', 'bottom']: + ##if self.getLabel(l).isVisible(): + ##self.setLabel(l, unitPrefix=prefix) + ##self.getScale(l).setScale(scale) + ##else: + ##self.setLabel(l, unitPrefix='') + ##self.getScale(l).setScale(1.0) + + ##self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) + #self.sigXRangeChanged.emit(self, range) + + #def yRangeChanged(self, _, range): + #if any(np.isnan(range)) or any(np.isinf(range)): + #raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) + #self.ctrl.yMinText.setText('%0.5g' % range[0]) + #self.ctrl.yMaxText.setText('%0.5g' % range[1]) + + ### automatically change unit scale + #maxVal = max(abs(range[0]), abs(range[1])) + #(scale, prefix) = fn.siScale(maxVal) + ##for l in ['left', 'right']: + ##if self.getLabel(l).isVisible(): + ##self.setLabel(l, unitPrefix=prefix) + ##self.getScale(l).setScale(scale) + ##else: + ##self.setLabel(l, unitPrefix='') + ##self.getScale(l).setScale(1.0) + ##self.emit(QtCore.SIGNAL('yRangeChanged'), self, range) + #self.sigYRangeChanged.emit(self, range) + + def autoBtnClicked(self): + if self.autoBtn.mode == 'auto': + self.enableAutoRange() + else: + self.disableAutoRange() + + def enableAutoScale(self): + """ + Enable auto-scaling. The plot will continuously scale to fit the boundaries of its data. + """ + print "Warning: enableAutoScale is deprecated. Use enableAutoRange(axis, enable) instead." + self.vb.enableAutoRange(self.vb.XYAxes) + #self.ctrl.xAutoRadio.setChecked(True) + #self.ctrl.yAutoRadio.setChecked(True) + + #self.autoBtn.setImageFile(self.lockImageFile) + #self.autoBtn.mode = 'lock' + #self.updateXScale() + #self.updateYScale() + #self.replot() + + #def updateXScale(self): + #"""Set plot to autoscale or not depending on state of radio buttons""" + #if self.ctrl.xManualRadio.isChecked(): + #self.setManualXScale() + #else: + #self.setAutoXScale() + #self.replot() + + #def updateYScale(self, b=False): + #"""Set plot to autoscale or not depending on state of radio buttons""" + #if self.ctrl.yManualRadio.isChecked(): + #self.setManualYScale() + #else: + #self.setAutoYScale() + #self.replot() + + #def enableManualScale(self, v=[True, True]): + #if v[0]: + #self.autoScale[0] = False + #self.ctrl.xManualRadio.setChecked(True) + ##self.setManualXScale() + #if v[1]: + #self.autoScale[1] = False + #self.ctrl.yManualRadio.setChecked(True) + ##self.setManualYScale() + ##self.autoBtn.enable() + #self.autoBtn.setImageFile(self.autoImageFile) + #self.autoBtn.mode = 'auto' + ##self.replot() + + #def setManualXScale(self): + #self.autoScale[0] = False + #x1 = float(self.ctrl.xMinText.text()) + #x2 = float(self.ctrl.xMaxText.text()) + #self.ctrl.xManualRadio.setChecked(True) + #self.setXRange(x1, x2, padding=0) + #self.autoBtn.show() + ##self.replot() + + #def setManualYScale(self): + #self.autoScale[1] = False + #y1 = float(self.ctrl.yMinText.text()) + #y2 = float(self.ctrl.yMaxText.text()) + #self.ctrl.yManualRadio.setChecked(True) + #self.setYRange(y1, y2, padding=0) + #self.autoBtn.show() + ##self.replot() + + #def setAutoXScale(self): + #self.autoScale[0] = True + #self.ctrl.xAutoRadio.setChecked(True) + ##self.replot() + + #def setAutoYScale(self): + #self.autoScale[1] = True + #self.ctrl.yAutoRadio.setChecked(True) + ##self.replot() + + def addItem(self, item, *args, **kargs): + self.items.append(item) + self.vb.addItem(item, *args) + if hasattr(item, 'implements') and item.implements('plotData'): + self.dataItems.append(item) + #self.plotChanged() + + params = kargs.get('params', {}) + self.itemMeta[item] = params + #item.setMeta(params) + self.curves.append(item) + #self.addItem(c) + + if isinstance(item, PlotDataItem): + ## configure curve for this plot + (alpha, auto) = self.alphaState() + item.setAlpha(alpha, auto) + item.setFftMode(self.ctrl.powerSpectrumGroup.isChecked()) + item.setDownsampling(self.downsampleMode()) + item.setPointMode(self.pointMode()) + + ## Hide older plots if needed + self.updateDecimation() + + ## Add to average if needed + self.updateParamList() + if self.ctrl.averageGroup.isChecked() and 'skipAverage' not in kargs: + self.addAvgCurve(item) + + #c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged) + #item.sigPlotChanged.connect(self.plotChanged) + #self.plotChanged() + + def addDataItem(self, item, *args): + print "PlotItem.addDataItem is deprecated. Use addItem instead." + self.addItem(item, *args) + + def addCurve(self, c, params=None): + print "PlotItem.addCurve is deprecated. Use addItem instead." + self.addItem(item, params) + + + def removeItem(self, item): + if not item in self.items: + return + self.items.remove(item) + if item in self.dataItems: + self.dataItems.remove(item) + + if item.scene() is not None: + self.vb.removeItem(item) + if item in self.curves: + self.curves.remove(item) + self.updateDecimation() + self.updateParamList() + #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) + #item.sigPlotChanged.connect(self.plotChanged) + + def clear(self): + for i in self.items[:]: + self.removeItem(i) + self.avgCurves = {} + + def clearPlots(self): + for i in self.curves[:]: + self.removeItem(i) + self.avgCurves = {} + + + def plot(self, *args, **kargs): + """ + Add and return a new plot. + See PlotDataItem.__init__ for data arguments + + Extra allowed arguments are: + clear - clear all plots before displaying new data + params - meta-parameters to associate with this data + """ + + + + #if y is not None: + #data = y + #if data2 is not None: + #x = data + #data = data2 + #if decimate is not None and decimate > 1: + #data = data[::decimate] + #if x is not None: + #x = x[::decimate] + ## print 'plot with decimate = %d' % (decimate) + clear = kargs.get('clear', False) + params = kargs.get('params', None) + + if clear: + self.clear() + + item = PlotDataItem(*args, **kargs) + + if params is None: + params = {} + #if HAVE_METAARRAY and isinstance(data, MetaArray): + #curve = self._plotMetaArray(data, x=x, **kargs) + #elif isinstance(data, np.ndarray): + #curve = self._plotArray(data, x=x, **kargs) + #elif isinstance(data, list): + #if x is not None: + #x = np.array(x) + #curve = self._plotArray(np.array(data), x=x, **kargs) + #elif data is None: + #curve = PlotCurveItem(**kargs) + #else: + #raise Exception('Not sure how to plot object of type %s' % type(data)) + + #print data, curve + self.addItem(item, params=params) + #if pen is not None: + #curve.setPen(fn.mkPen(pen)) + + return item + + def scatterPlot(self, *args, **kargs): + print "PlotItem.scatterPlot is deprecated. Use PlotItem.plot instead." + return self.plot(*args, **kargs) + #sp = ScatterPlotItem(*args, **kargs) + #self.addItem(sp) + #return sp + + + + #def plotChanged(self, curve=None): + ## Recompute auto range if needed + #args = {} + #for ax in [0, 1]: + #print "range", ax + #if self.autoScale[ax]: + #percentScale = [self.ctrl.xAutoPercentSpin.value(), self.ctrl.yAutoPercentSpin.value()][ax] * 0.01 + #mn = None + #mx = None + #for c in self.curves + [c[1] for c in self.avgCurves.values()] + self.dataItems: + #if not c.isVisible(): + #continue + #cmn, cmx = c.getRange(ax, percentScale) + ##print " ", c, cmn, cmx + #if mn is None or cmn < mn: + #mn = cmn + #if mx is None or cmx > mx: + #mx = cmx + #if mn is None or mx is None or any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])): + #continue + #if mn == mx: + #mn -= 1 + #mx += 1 + #if ax == 0: + #args['xRange'] = [mn, mx] + #else: + #args['yRange'] = [mn, mx] + + #if len(args) > 0: + ##print args + #self.setRange(**args) + + def replot(self): + #self.plotChanged() + self.update() + + def updateParamList(self): + self.ctrl.avgParamList.clear() + ## Check to see that each parameter for each curve is present in the list + #print "\nUpdate param list", self + #print "paramList:", self.paramList + for c in self.curves: + #print " curve:", c + for p in self.itemMeta.get(c, {}).keys(): + #print " param:", p + if type(p) is tuple: + p = '.'.join(p) + + ## If the parameter is not in the list, add it. + matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly) + #print " matches:", matches + if len(matches) == 0: + i = QtGui.QListWidgetItem(p) + if p in self.paramList and self.paramList[p] is True: + #print " set checked" + i.setCheckState(QtCore.Qt.Checked) + else: + #print " set unchecked" + i.setCheckState(QtCore.Qt.Unchecked) + self.ctrl.avgParamList.addItem(i) + else: + i = matches[0] + + self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) + #print "paramList:", self.paramList + + + ## This is bullshit. + def writeSvgCurves(self, fileName=None): + if fileName is None: + self.fileDialog = FileDialog() + if PlotItem.lastFileDir is not None: + self.fileDialog.setDirectory(PlotItem.lastFileDir) + self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.show() + self.fileDialog.fileSelected.connect(self.writeSvg) + return + #if fileName is None: + #fileName = QtGui.QFileDialog.getSaveFileName() + if isinstance(fileName, tuple): + raise Exception("Not implemented yet..") + fileName = str(fileName) + PlotItem.lastFileDir = os.path.dirname(fileName) + + rect = self.vb.viewRect() + xRange = rect.left(), rect.right() + + svg = "" + fh = open(fileName, 'w') + + dx = max(rect.right(),0) - min(rect.left(),0) + ymn = min(rect.top(), rect.bottom()) + ymx = max(rect.top(), rect.bottom()) + dy = max(ymx,0) - min(ymn,0) + sx = 1. + sy = 1. + while dx*sx < 10: + sx *= 1000 + while dy*sy < 10: + sy *= 1000 + sy *= -1 + + #fh.write('\n' % (rect.left()*sx, rect.top()*sx, rect.width()*sy, rect.height()*sy)) + fh.write('\n') + fh.write('\n' % (rect.left()*sx, rect.right()*sx)) + fh.write('\n' % (rect.top()*sy, rect.bottom()*sy)) + + + for item in self.curves: + if isinstance(item, PlotCurveItem): + color = fn.colorStr(item.pen.color()) + opacity = item.pen.color().alpha() / 255. + color = color[:6] + x, y = item.getData() + mask = (x > xRange[0]) * (x < xRange[1]) + mask[:-1] += mask[1:] + m2 = mask.copy() + mask[1:] += m2[:-1] + x = x[mask] + y = y[mask] + + x *= sx + y *= sy + + #fh.write('\n' % color) + fh.write('') + #fh.write("") + for item in self.dataItems: + if isinstance(item, ScatterPlotItem): + + pRect = item.boundingRect() + vRect = pRect.intersected(rect) + + for point in item.points(): + pos = point.pos() + if not rect.contains(pos): + continue + color = fn.colorStr(point.brush.color()) + opacity = point.brush.color().alpha() / 255. + color = color[:6] + x = pos.x() * sx + y = pos.y() * sy + + fh.write('\n' % (x, y, color, opacity)) + #fh.write('') + + ## get list of curves, scatter plots + + + fh.write("\n") + + + + def writeSvg(self, fileName=None): + if fileName is None: + fileName = QtGui.QFileDialog.getSaveFileName() + fileName = str(fileName) + PlotItem.lastFileDir = os.path.dirname(fileName) + + self.svg = QtSvg.QSvgGenerator() + self.svg.setFileName(fileName) + res = 120. + view = self.scene().views()[0] + bounds = view.viewport().rect() + bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) + + self.svg.setResolution(res) + self.svg.setViewBox(bounds) + + self.svg.setSize(QtCore.QSize(bounds.width(), bounds.height())) + + painter = QtGui.QPainter(self.svg) + view.render(painter, bounds) + + painter.end() + + ## Workaround to set pen widths correctly + import re + data = open(fileName).readlines() + for i in range(len(data)): + line = data[i] + m = re.match(r'(= split: + curves[i].show() + else: + if self.ctrl.forgetTracesCheck.isChecked(): + curves[i].clear() + self.removeItem(curves[i]) + else: + curves[i].hide() + + + def updateAlpha(self, *args): + (alpha, auto) = self.alphaState() + for c in self.curves: + c.setAlpha(alpha**2, auto) + + #self.replot(autoRange=False) + + def alphaState(self): + enabled = self.ctrl.alphaGroup.isChecked() + auto = self.ctrl.autoAlphaCheck.isChecked() + alpha = float(self.ctrl.alphaSlider.value()) / self.ctrl.alphaSlider.maximum() + if auto: + alpha = 1.0 ## should be 1/number of overlapping plots + if not enabled: + auto = False + alpha = 1.0 + return (alpha, auto) + + def pointMode(self): + if self.ctrl.pointsGroup.isChecked(): + if self.ctrl.autoPointsCheck.isChecked(): + mode = None + else: + mode = True + else: + mode = False + return mode + + def wheelEvent(self, ev): + # disables default panning the whole scene by mousewheel + ev.accept() + + def resizeEvent(self, ev): + if self.autoBtn is None: ## already closed down + return + btnRect = self.mapRectFromItem(self.autoBtn, self.autoBtn.boundingRect()) + y = self.size().height() - btnRect.height() + self.autoBtn.setPos(0, y) + + #def hoverMoveEvent(self, ev): + #self.mousePos = ev.pos() + #self.mouseScreenPos = ev.screenPos() + + + #def ctrlBtnClicked(self): + #self.ctrlMenu.popup(self.mouseScreenPos) + + def getMenu(self): + return self.ctrlMenu + + def getContextMenus(self, event): + ## called when another item is displaying its context menu; we get to add extras to the end of the menu. + return self.ctrlMenu + + + def getLabel(self, key): + pass + + def _checkScaleKey(self, key): + if key not in self.scales: + raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(self.scales.keys()))) + + def getScale(self, key): + self._checkScaleKey(key) + return self.scales[key]['item'] + + def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args): + """ + Set the label for an axis. Basic HTML formatting is allowed. + Arguments: + axis - must be one of 'left', 'bottom', 'right', or 'top' + text - text to display along the axis. HTML allowed. + units - units to display after the title. If units are given, + then an SI prefix will be automatically appended + and the axis values will be scaled accordingly. + (ie, use 'V' instead of 'mV'; 'm' will be added automatically) + """ + self.getScale(axis).setLabel(text=text, units=units, **args) + + def showLabel(self, axis, show=True): + """ + Show or hide one of the plot's axis labels (the axis itself will be unaffected). + axis must be one of 'left', 'bottom', 'right', or 'top' + """ + self.getScale(axis).showLabel(show) + + def setTitle(self, title=None, **args): + """ + Set the title of the plot. Basic HTML formatting is allowed. + If title is None, then the title will be hidden. + """ + if title is None: + self.titleLabel.setVisible(False) + self.layout.setRowFixedHeight(0, 0) + self.titleLabel.setMaximumHeight(0) + else: + self.titleLabel.setMaximumHeight(30) + self.layout.setRowFixedHeight(0, 30) + self.titleLabel.setVisible(True) + self.titleLabel.setText(title, **args) + + def showAxis(self, axis, show=True): + """ + Show or hide one of the plot's axes. + axis must be one of 'left', 'bottom', 'right', or 'top' + """ + s = self.getScale(axis) + p = self.scales[axis]['pos'] + if show: + s.show() + else: + s.hide() + + def hideAxis(self, axis): + self.showAxis(axis, False) + + def showScale(self, *args, **kargs): + print "Deprecated. use showAxis() instead" + return self.showAxis(*args, **kargs) + + def hideButtons(self): + #self.ctrlBtn.hide() + self.autoBtn.hide() + + + def _plotArray(self, arr, x=None, **kargs): + if arr.ndim != 1: + raise Exception("Array must be 1D to plot (shape is %s)" % arr.shape) + if x is None: + x = np.arange(arr.shape[0]) + if x.ndim != 1: + raise Exception("X array must be 1D to plot (shape is %s)" % x.shape) + c = PlotCurveItem(arr, x=x, **kargs) + return c + + + + def _plotMetaArray(self, arr, x=None, autoLabel=True, **kargs): + inf = arr.infoCopy() + if arr.ndim != 1: + raise Exception('can only automatically plot 1 dimensional arrays.') + ## create curve + try: + xv = arr.xvals(0) + #print 'xvals:', xv + except: + if x is None: + xv = np.arange(arr.shape[0]) + else: + xv = x + c = PlotCurveItem(**kargs) + c.setData(x=xv, y=arr.view(np.ndarray)) + + if autoLabel: + name = arr._info[0].get('name', None) + units = arr._info[0].get('units', None) + self.setLabel('bottom', text=name, units=units) + + name = arr._info[1].get('name', None) + units = arr._info[1].get('units', None) + self.setLabel('left', text=name, units=units) + + return c + + def saveSvgClicked(self): + self.writeSvg() + + def saveSvgCurvesClicked(self): + self.writeSvgCurves() + + def saveImgClicked(self): + self.writeImage() + + def saveCsvClicked(self): + self.writeCsv() + + +#class PlotWidgetManager(QtCore.QObject): + + #sigWidgetListChanged = QtCore.Signal(object) + + #"""Used for managing communication between PlotWidgets""" + #def __init__(self): + #QtCore.QObject.__init__(self) + #self.widgets = weakref.WeakValueDictionary() # Don't keep PlotWidgets around just because they are listed here + + #def addWidget(self, w, name): + #self.widgets[name] = w + ##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) + #self.sigWidgetListChanged.emit(self.widgets.keys()) + + #def removeWidget(self, name): + #if name in self.widgets: + #del self.widgets[name] + ##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) + #self.sigWidgetListChanged.emit(self.widgets.keys()) + #else: + #print "plot %s not managed" % name + + + #def listWidgets(self): + #return self.widgets.keys() + + #def getWidget(self, name): + #if name not in self.widgets: + #return None + #else: + #return self.widgets[name] + + #def linkX(self, p1, p2): + ##QtCore.QObject.connect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) + #p1.sigXRangeChanged.connect(p2.linkXChanged) + ##QtCore.QObject.connect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) + #p2.sigXRangeChanged.connect(p1.linkXChanged) + #p1.linkXChanged(p2) + ##p2.setManualXScale() + + #def unlinkX(self, p1, p2): + ##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) + #p1.sigXRangeChanged.disconnect(p2.linkXChanged) + ##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) + #p2.sigXRangeChanged.disconnect(p1.linkXChanged) + + #def linkY(self, p1, p2): + ##QtCore.QObject.connect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) + #p1.sigYRangeChanged.connect(p2.linkYChanged) + ##QtCore.QObject.connect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) + #p2.sigYRangeChanged.connect(p1.linkYChanged) + #p1.linkYChanged(p2) + ##p2.setManualYScale() + + #def unlinkY(self, p1, p2): + ##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) + #p1.sigYRangeChanged.disconnect(p2.linkYChanged) + ##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) + #p2.sigYRangeChanged.disconnect(p1.linkYChanged) diff --git a/graphicsItems/PlotItem/__init__.py b/graphicsItems/PlotItem/__init__.py new file mode 100644 index 00000000..e0db43af --- /dev/null +++ b/graphicsItems/PlotItem/__init__.py @@ -0,0 +1 @@ +from PlotItem import PlotItem diff --git a/graphicsItems/PlotItem/auto.png b/graphicsItems/PlotItem/auto.png new file mode 100644 index 0000000000000000000000000000000000000000..a27ff4f82b6e31f2819061a53ba72d4b322dc26d GIT binary patch literal 1022 zcmV^J4|8T_gkr2rEb%1y+EsAi5$F z6^a6Zm<{4b{(-IT$`sImw8eJYQn(>RHV8WC_#q^8EE0#dLYAnABJEZo1<0|z1#9Gq zacovOaj)`x-@JLB@te`i5W_HFnj}eY4a4{wz#jlc0K7tTtODRGfHOkKXF(9Yn+{?E zNRsryFpMt%-ZUwxll>rsd=vzs_y|xe7PEwqg0+{XX{c7KP01Jvh2Zn~D2))36$GJ3 zwzjsqYPH%o0Ivby`uZB8C_+&bsH)l&0ES_}aU62F9QykDO!)^P`E-26-k`O8e6 z&*!0OTEhY3@i>Nuhbis!^b}`jXAR?gJ|9+BSFI4A{DhEEvn7h6YjOZsTU$dU5}~y7 z^K&GV$%b*7ra=@%tgNh<65j&gz3EgG#Wgw4^S0ywFfuZNXf)~?uPBOjlA|s7wbiQC zs;8RI&(Ayh78e&?<5g9)F78(?t`Qashr>vvQjY%V>1hN4f#(c~X91+sX|OE&*sj%T zHo$Z`{oesBEG$s%?d@&by75wm-vWrmVu;0JlqN|Mip3(O4Gj%paB#5s0QIvkl}b1{ zIe{$8wynFgwA5SxmSt_Vm1P-*VW3tb{Th(sdbd7jd&vj>3t`+Mx}?owJb8nvAl zzXedg_*7NJ&d!dlUws(>Z-2qx$l2Q3icBU$X@n5$@9(<~3;>gplUQC}ww1>-z{JD^ zIyyS&0KXqSJw2WPSeAt#2v}cVckI)!1dR?JujW<;$3M{9+lzQSPHDHdx465zYdBya z5I`^(d~6R441ni($IFT%z}(y%2qBb~$z*VOdD##kkw{=;V}sIixtt@wlO^!`{m5ps zly-G><(eD-4i67;b8|y!$z&4k?d{$H#>dCe+1W{H^?TpRbX`ZGKwYrW*4BpE*;z+` z3h;1sW*7#5eedhK4q28x0RTXuP=K!Mlx7}K48vH@iAoD0#OVy1KfMNF=D> zWd~F$C0o$RZJh6k)418x_AmtgMxg zXCY%qyWNsP1VI$676i93_`#s^5(Wz!3mXNE#qQf;A$9{Irg9?;At)qHW{d1(<1@;8 z$%y-}X3jnLp5MLq+?jiXX_{D4RrP~un!f-8z)ye&dz1q{1Aho1ehq~}e_IX00#H>o zY?|h8z;`wUeMwm%#LuBnC|LnWCX*k95TB~O48tIw&)X7Fk|dl?=M$R{;$tWjN{WMn zgYO?7AJaewz}3|iilU%t8pUGK767-~jnC&pmSuW-do6QT2=Rk+YHDgp2r*#=Uszb+ z_V$)isbouVfKsVMHk;+-E)a>Z%*W zFbot$sW$aHfFG?!P19cNJU>6r+S(fD=jY7L&D9N|XZYWaNr)jf|yBBax4 zT3T9IUS4K$aCcN1JqoBySqD9R#pJ; z`~8)yuMQ67E!f)HVtjnuUI2jI-Cd&5DA(85uMW^C#H+{f`1qKKi3!5tFtf9>#9}cX z9v*CksF?%RDijJ_TwGM%Z*Feb+1a7BwY4ThG#dS;ocQVwk)>2BMI;ijZ6&@Lz;1}T z0PKXQ8^A7zh5+nY;0^WG&Dpmmjl4(^HtXfe>zOl{A|6+viz)o zs8_nK6OYG<$K&X_4wNS;%W}2b@9$oiynku-zbH;Ey+?We4}W%#BJQ5X8UO$Q07*qo IM6N<$f(oslRsaA1 literal 0 HcmV?d00001 diff --git a/graphicsItems/PlotItem/icons.svg b/graphicsItems/PlotItem/icons.svg new file mode 100644 index 00000000..cfdfeba4 --- /dev/null +++ b/graphicsItems/PlotItem/icons.svg @@ -0,0 +1,135 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + A + + + + + + + + + + + diff --git a/graphicsItems/PlotItem/lock.png b/graphicsItems/PlotItem/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..3f85dde05d29a80e9cba8d9d339f88d7f2a960e8 GIT binary patch literal 913 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 0 and axis < len(mask): + mv = mask[axis] + mask[:] = 0 + mask[axis] = mv + s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor + + center = Point(self.childGroup.transform().inverted()[0].map(ev.pos())) + #center = ev.pos() + + self.scaleBy(s, center) + self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + ev.accept() + + + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + ev.accept() + self.raiseContextMenu(ev) + + def raiseContextMenu(self, ev): + #print "viewbox.raiseContextMenu called." + + #menu = self.getMenu(ev) + menu = self.getMenu(ev) + self.scene().addParentContextMenus(self, menu, ev) + #print "2:", [str(a.text()) for a in self.menu.actions()] + pos = ev.screenPos() + #pos2 = ev.scenePos() + #print "3:", [str(a.text()) for a in self.menu.actions()] + #self.sigActionPositionChanged.emit(pos2) + + menu.popup(QtCore.QPoint(pos.x(), pos.y())) + #print "4:", [str(a.text()) for a in self.menu.actions()] + + def getMenu(self, ev): + self._menuCopy = self.menu.copy() ## temporary storage to prevent menu disappearing + return self._menuCopy + + def getContextMenus(self, event): + return self.menu.subMenus() + #return [self.getMenu(event)] + + + def mouseDragEvent(self, ev): + ev.accept() ## we accept all buttons + + pos = ev.pos() + lastPos = ev.lastPos() + dif = pos - lastPos + dif = dif * -1 + + ## Ignore axes if mouse is disabled + mask = np.array(self.state['mouseEnabled'], dtype=np.float) + + ## Scale or translate based on mouse button + if ev.button() & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton): + if self.state['mouseMode'] == ViewBox.RectMode: + if ev.isFinish(): ## This is the final move in the drag; change the view scale now + #print "finish" + self.rbScaleBox.hide() + #ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos)) + ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos)) + ax = self.childGroup.mapRectFromParent(ax) + self.showAxRect(ax) + self.axHistoryPointer += 1 + self.axHistory = self.axHistory[:self.axHistoryPointer] + [ax] + else: + ## update shape of scale box + self.updateScaleBox(ev.buttonDownPos(), ev.pos()) + else: + tr = dif*mask + tr = self.mapToView(tr) - self.mapToView(Point(0,0)) + self.translateBy(tr) + self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + elif ev.button() & QtCore.Qt.RightButton: + #print "vb.rightDrag" + if self.state['aspectLocked'] is not False: + mask[0] = 0 + + dif = ev.screenPos() - ev.lastScreenPos() + dif = np.array([dif.x(), dif.y()]) + dif[0] *= -1 + s = ((mask * 0.02) + 1) ** dif + center = Point(self.childGroup.transform().inverted()[0].map(ev.buttonDownPos(QtCore.Qt.RightButton))) + #center = Point(ev.buttonDownPos(QtCore.Qt.RightButton)) + self.scaleBy(s, center) + self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + + def keyPressEvent(self, ev): + """ + This routine should capture key presses in the current view box. + Key presses are used only when mouse mode is RectMode + The following events are implemented: + ctrl-A : zooms out to the default "full" view of the plot + ctrl-+ : moves forward in the zooming stack (if it exists) + ctrl-- : moves backward in the zooming stack (if it exists) + + """ + #print ev.key() + #print 'I intercepted a key press, but did not accept it' + + ## not implemented yet ? + #self.keypress.sigkeyPressEvent.emit() + + ev.accept() + if ev.text() == '-': + self.scaleHistory(-1) + elif ev.text() in ['+', '=']: + self.scaleHistory(1) + elif ev.key() == QtCore.Qt.Key_Backspace: + self.scaleHistory(len(self.axHistory)) + else: + ev.ignore() + + def scaleHistory(self, d): + ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d)) + if ptr != self.axHistoryPointer: + self.axHistoryPointer = ptr + self.showAxRect(self.axHistory[ptr]) + + + def updateScaleBox(self, p1, p2): + r = QtCore.QRectF(p1, p2) + r = self.childGroup.mapRectFromParent(r) + self.rbScaleBox.setPos(r.topLeft()) + self.rbScaleBox.resetTransform() + self.rbScaleBox.scale(r.width(), r.height()) + self.rbScaleBox.show() + + def showAxRect(self, ax): + self.setRange(ax.normalized()) # be sure w, h are correct coordinates + self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + + #def mouseRect(self): + #vs = self.viewScale() + #vr = self.state['viewRange'] + ## Convert positions from screen (view) pixel coordinates to axis coordinates + #ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]), + #(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1]) + #return(ax) + + def allChildren(self, item=None): + """Return a list of all children and grandchildren of this ViewBox""" + if item is None: + item = self.childGroup + + children = [item] + for ch in item.childItems(): + children.extend(self.allChildren(ch)) + return children + + + + def childrenBoundingRect(self, frac=None): + """Return the bounding range of all children. + [[xmin, xmax], [ymin, ymax]] + Values may be None if there are no specific bounds for an axis. + """ + + #items = self.allChildren() + items = self.addedItems + + #if item is None: + ##print "children bounding rect:" + #item = self.childGroup + + range = [None, None] + + for item in items: + if not item.isVisible(): + continue + + #print "=========", item + useX = True + useY = True + if hasattr(item, 'dataBounds'): + if frac is None: + frac = (1.0, 1.0) + xr = item.dataBounds(0, frac=frac[0]) + yr = item.dataBounds(1, frac=frac[1]) + if xr is None: + useX = False + xr = (0,0) + if yr is None: + useY = False + yr = (0,0) + + bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) + #print " item real:", bounds + else: + if int(item.flags() & item.ItemHasNoContents) > 0: + continue + #print " empty" + else: + bounds = item.boundingRect() + #bounds = [[item.left(), item.top()], [item.right(), item.bottom()]] + #print " item:", bounds + #bounds = QtCore.QRectF(bounds[0][0], bounds[1][0], bounds[0][1]-bounds[0][0], bounds[1][1]-bounds[1][0]) + bounds = self.mapFromItemToView(item, bounds).boundingRect() + #print " ", bounds + + + if not any([useX, useY]): + continue + + if useX != useY: ## != means xor + ang = item.transformAngle() + if ang == 0 or ang == 180: + pass + elif ang == 90 or ang == 270: + tmp = useX + useY = useX + useX = tmp + else: + continue ## need to check for item rotations and decide how best to apply this boundary. + + + if useY: + if range[1] is not None: + range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])] + #bounds.setTop(min(bounds.top(), chb.top())) + #bounds.setBottom(max(bounds.bottom(), chb.bottom())) + else: + range[1] = [bounds.top(), bounds.bottom()] + #bounds.setTop(chb.top()) + #bounds.setBottom(chb.bottom()) + if useX: + if range[0] is not None: + range[0] = [min(bounds.left(), range[0][0]), max(bounds.right(), range[0][1])] + #bounds.setLeft(min(bounds.left(), chb.left())) + #bounds.setRight(max(bounds.right(), chb.right())) + else: + range[0] = [bounds.left(), bounds.right()] + #bounds.setLeft(chb.left()) + #bounds.setRight(chb.right()) + + tr = self.targetRange() + if range[0] is None: + range[0] = tr[0] + if range[1] is None: + range[1] = tr[1] + + bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0]) + return bounds + + + + def updateMatrix(self, changed=None): + if changed is None: + changed = [False, False] + #print "udpateMatrix:" + #print " range:", self.range + tr = self.targetRect() + bounds = self.rect() #boundingRect() + #print bounds + + ## set viewRect, given targetRect and possibly aspect ratio constraint + if self.state['aspectLocked'] is False or bounds.height() == 0: + self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] + else: + viewRatio = bounds.width() / bounds.height() + targetRatio = self.state['aspectLocked'] * tr.width() / tr.height() + if targetRatio > viewRatio: + ## target is wider than view + dy = 0.5 * (tr.width() / (self.state['aspectLocked'] * viewRatio) - tr.height()) + if dy != 0: + changed[1] = True + self.state['viewRange'] = [self.state['targetRange'][0][:], [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]] + else: + dx = 0.5 * (tr.height() * viewRatio * self.state['aspectLocked'] - tr.width()) + if dx != 0: + changed[0] = True + self.state['viewRange'] = [[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], self.state['targetRange'][1][:]] + + vr = self.viewRect() + #print " bounds:", bounds + if vr.height() == 0 or vr.width() == 0: + return + scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height()) + if not self.state['yInverted']: + scale = scale * Point(1, -1) + m = QtGui.QTransform() + + ## First center the viewport at 0 + #self.childGroup.resetTransform() + #self.resetTransform() + #center = self.transform().inverted()[0].map(bounds.center()) + center = bounds.center() + #print " transform to center:", center + #if self.state['yInverted']: + #m.translate(center.x(), -center.y()) + #print " inverted; translate", center.x(), center.y() + #else: + m.translate(center.x(), center.y()) + #print " not inverted; translate", center.x(), -center.y() + + ## Now scale and translate properly + m.scale(scale[0], scale[1]) + st = Point(vr.center()) + #st = translate + m.translate(-st[0], -st[1]) + + self.childGroup.setTransform(m) + #self.setTransform(m) + #self.prepareGeometryChange() + + #self.currentScale = scale + + if changed[0]: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + if changed[1]: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) + if any(changed): + self.sigRangeChanged.emit(self, self.state['viewRange']) + + def paint(self, p, opt, widget): + if self.border is not None: + bounds = self.shape() + p.setPen(self.border) + #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) + p.drawPath(bounds) + + def saveSvg(self): + pass + + def saveImage(self): + pass + + def savePrint(self): + printer = QtGui.QPrinter() + if QtGui.QPrintDialog(printer).exec_() == QtGui.QDialog.Accepted: + p = QtGui.QPainter(printer) + p.setRenderHint(p.Antialiasing) + self.scene().render(p) + p.end() + + def updateViewLists(self): + def cmpViews(a, b): + wins = 100 * cmp(a.window() is self.window(), b.window() is self.window()) + alpha = cmp(a.name, b.name) + return wins + alpha + + ## make a sorted list of all named views + nv = ViewBox.NamedViews.values() + nv.sort(cmpViews) + + if self in nv: + nv.remove(self) + names = [v.name for v in nv] + self.menu.setViewList(names) + + @staticmethod + def updateAllViewLists(): + for v in ViewBox.AllViews: + v.updateViewLists() + + + + +from ViewBoxMenu import ViewBoxMenu diff --git a/graphicsItems/ViewBox/ViewBoxMenu.py b/graphicsItems/ViewBox/ViewBoxMenu.py new file mode 100644 index 00000000..d45d89e9 --- /dev/null +++ b/graphicsItems/ViewBox/ViewBoxMenu.py @@ -0,0 +1,222 @@ +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.WidgetGroup import WidgetGroup +from axisCtrlTemplate import Ui_Form as AxisCtrlTemplate + +class ViewBoxMenu(QtGui.QMenu): + def __init__(self, view): + QtGui.QMenu.__init__(self) + + self.view = view + self.valid = False ## tells us whether the ui needs to be updated + + self.setTitle("ViewBox options") + self.viewAll = QtGui.QAction("View All", self) + self.viewAll.triggered.connect(self.autoRange) + self.addAction(self.viewAll) + + self.axes = [] + self.ctrl = [] + self.widgetGroups = [] + self.dv = QtGui.QDoubleValidator(self) + for axis in 'XY': + m = QtGui.QMenu() + m.setTitle("%s Axis" % axis) + w = QtGui.QWidget() + ui = AxisCtrlTemplate() + ui.setupUi(w) + a = QtGui.QWidgetAction(self) + a.setDefaultWidget(w) + m.addAction(a) + self.addMenu(m) + self.axes.append(m) + self.ctrl.append(ui) + wg = WidgetGroup(w) + self.widgetGroups.append(w) + + connects = [ + (ui.mouseCheck.toggled, 'MouseToggled'), + (ui.manualRadio.clicked, 'ManualClicked'), + (ui.minText.editingFinished, 'MinTextChanged'), + (ui.maxText.editingFinished, 'MaxTextChanged'), + (ui.autoRadio.clicked, 'AutoClicked'), + (ui.autoPercentSpin.valueChanged, 'AutoSpinChanged'), + (ui.linkCombo.currentIndexChanged, 'LinkComboChanged'), + ] + + for sig, fn in connects: + sig.connect(getattr(self, axis.lower()+fn)) + + self.export = QtGui.QMenu("Export") + self.setExportMethods(view.exportMethods) + self.addMenu(self.export) + + self.leftMenu = QtGui.QMenu("Mouse Mode") + group = QtGui.QActionGroup(self) + pan = self.leftMenu.addAction("3 button", self.set3ButtonMode) + zoom = self.leftMenu.addAction("1 button", self.set1ButtonMode) + pan.setCheckable(True) + zoom.setCheckable(True) + pan.setActionGroup(group) + zoom.setActionGroup(group) + self.mouseModes = [pan, zoom] + self.addMenu(self.leftMenu) + + self.view.sigStateChanged.connect(self.viewStateChanged) + + self.updateState() + + def copy(self): + m = QtGui.QMenu() + for sm in self.subMenus(): + if isinstance(sm, QtGui.QMenu): + m.addMenu(sm) + else: + m.addAction(sm) + m.setTitle(self.title()) + return m + + def subMenus(self): + if not self.valid: + self.updateState() + return [self.viewAll] + self.axes + [self.export, self.leftMenu] + + + def setExportMethods(self, methods): + self.exportMethods = methods + self.export.clear() + for opt, fn in methods.iteritems(): + self.export.addAction(opt, self.exportMethod) + + + def viewStateChanged(self): + self.valid = False + if self.ctrl[0].minText.isVisible() or self.ctrl[1].minText.isVisible(): + self.updateState() + + def updateState(self): + state = self.view.getState(copy=False) + if state['mouseMode'] == ViewBox.PanMode: + self.mouseModes[0].setChecked(True) + else: + self.mouseModes[1].setChecked(True) + + + for i in [0,1]: + tr = state['targetRange'][i] + self.ctrl[i].minText.setText("%0.5g" % tr[0]) + self.ctrl[i].maxText.setText("%0.5g" % tr[1]) + if state['autoRange'][i] is not False: + self.ctrl[i].autoRadio.setChecked(True) + else: + self.ctrl[i].manualRadio.setChecked(True) + self.ctrl[i].mouseCheck.setChecked(state['mouseEnabled'][i]) + + c = self.ctrl[i].linkCombo + c.blockSignals(True) + try: + view = state['linkedViews'][i] + if view is None: + view = '' + ind = c.findText(view) + if ind == -1: + ind = 0 + c.setCurrentIndex(ind) + finally: + c.blockSignals(False) + + + self.valid = True + + + def autoRange(self): + self.view.autoRange() ## don't let signal call this directly--it'll add an unwanted argument + + def xMouseToggled(self, b): + self.view.setMouseEnabled(x=b) + + def xManualClicked(self): + self.view.enableAutoRange(ViewBox.XAxis, False) + + def xMinTextChanged(self): + self.ctrl[0].manualRadio.setChecked(True) + self.view.setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0) + + def xMaxTextChanged(self): + self.ctrl[0].manualRadio.setChecked(True) + self.view.setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0) + + def xAutoClicked(self): + val = self.ctrl[0].autoPercentSpin.value() * 0.01 + self.view.enableAutoRange(ViewBox.XAxis, val) + + def xAutoSpinChanged(self, val): + self.ctrl[0].autoRadio.setChecked(True) + self.view.enableAutoRange(ViewBox.XAxis, val*0.01) + + def xLinkComboChanged(self, ind): + self.view.setXLink(str(self.ctrl[0].linkCombo.currentText())) + + + + def yMouseToggled(self, b): + self.view.setMouseEnabled(y=b) + + def yManualClicked(self): + self.view.enableAutoRange(ViewBox.YAxis, False) + + def yMinTextChanged(self): + self.ctrl[1].manualRadio.setChecked(True) + self.view.setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0) + + def yMaxTextChanged(self): + self.ctrl[1].manualRadio.setChecked(True) + self.view.setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0) + + def yAutoClicked(self): + val = self.ctrl[1].autoPercentSpin.value() * 0.01 + self.view.enableAutoRange(ViewBox.YAxis, val) + + def yAutoSpinChanged(self, val): + self.ctrl[1].autoRadio.setChecked(True) + self.view.enableAutoRange(ViewBox.YAxis, val*0.01) + + def yLinkComboChanged(self, ind): + self.view.setYLink(str(self.ctrl[1].linkCombo.currentText())) + + + def exportMethod(self): + act = self.sender() + self.exportMethods[str(act.text())]() + + + def set3ButtonMode(self): + self.view.setLeftButtonAction('pan') + + def set1ButtonMode(self): + self.view.setLeftButtonAction('rect') + + + def setViewList(self, views): + views = [''] + views + for i in [0,1]: + c = self.ctrl[i].linkCombo + current = unicode(c.currentText()) + c.blockSignals(True) + changed = True + try: + c.clear() + for v in views: + c.addItem(v) + if v == current: + changed = False + c.setCurrentIndex(c.count()-1) + finally: + c.blockSignals(False) + + if changed: + c.setCurrentIndex(0) + c.currentIndexChanged.emit(c.currentIndex()) + +from ViewBox import ViewBox + + \ No newline at end of file diff --git a/graphicsItems/ViewBox/__init__.py b/graphicsItems/ViewBox/__init__.py new file mode 100644 index 00000000..448283ef --- /dev/null +++ b/graphicsItems/ViewBox/__init__.py @@ -0,0 +1 @@ +from ViewBox import ViewBox diff --git a/graphicsItems/ViewBox/axisCtrlTemplate.py b/graphicsItems/ViewBox/axisCtrlTemplate.py new file mode 100644 index 00000000..b229bf3d --- /dev/null +++ b/graphicsItems/ViewBox/axisCtrlTemplate.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'axisCtrlTemplate.ui' +# +# Created: Fri Jan 20 12:41:24 2012 +# by: PyQt4 UI code generator 4.8.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(182, 120) + Form.setMaximumSize(QtCore.QSize(200, 16777215)) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.mouseCheck = QtGui.QCheckBox(Form) + self.mouseCheck.setChecked(True) + self.mouseCheck.setObjectName(_fromUtf8("mouseCheck")) + self.gridLayout.addWidget(self.mouseCheck, 0, 1, 1, 2) + self.manualRadio = QtGui.QRadioButton(Form) + self.manualRadio.setObjectName(_fromUtf8("manualRadio")) + self.gridLayout.addWidget(self.manualRadio, 1, 0, 1, 1) + self.minText = QtGui.QLineEdit(Form) + self.minText.setObjectName(_fromUtf8("minText")) + self.gridLayout.addWidget(self.minText, 1, 1, 1, 1) + self.maxText = QtGui.QLineEdit(Form) + self.maxText.setObjectName(_fromUtf8("maxText")) + self.gridLayout.addWidget(self.maxText, 1, 2, 1, 1) + self.autoRadio = QtGui.QRadioButton(Form) + self.autoRadio.setChecked(True) + self.autoRadio.setObjectName(_fromUtf8("autoRadio")) + self.gridLayout.addWidget(self.autoRadio, 2, 0, 1, 1) + self.autoPercentSpin = QtGui.QSpinBox(Form) + self.autoPercentSpin.setEnabled(True) + self.autoPercentSpin.setMinimum(1) + self.autoPercentSpin.setMaximum(100) + self.autoPercentSpin.setSingleStep(1) + self.autoPercentSpin.setProperty(_fromUtf8("value"), 100) + self.autoPercentSpin.setObjectName(_fromUtf8("autoPercentSpin")) + self.gridLayout.addWidget(self.autoPercentSpin, 2, 1, 1, 2) + self.autoPanCheck = QtGui.QCheckBox(Form) + self.autoPanCheck.setObjectName(_fromUtf8("autoPanCheck")) + self.gridLayout.addWidget(self.autoPanCheck, 3, 1, 1, 2) + self.linkCombo = QtGui.QComboBox(Form) + self.linkCombo.setObjectName(_fromUtf8("linkCombo")) + self.gridLayout.addWidget(self.linkCombo, 4, 1, 1, 2) + self.label = QtGui.QLabel(Form) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout.addWidget(self.label, 4, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.mouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse Enabled", None, QtGui.QApplication.UnicodeUTF8)) + self.manualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) + self.minText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.maxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) + self.autoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) + self.autoPanCheck.setText(QtGui.QApplication.translate("Form", "Auto Pan Only", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/graphicsItems/ViewBox/axisCtrlTemplate.ui b/graphicsItems/ViewBox/axisCtrlTemplate.ui new file mode 100644 index 00000000..f01a3f80 --- /dev/null +++ b/graphicsItems/ViewBox/axisCtrlTemplate.ui @@ -0,0 +1,110 @@ + + + Form + + + + 0 + 0 + 182 + 120 + + + + + 200 + 16777215 + + + + Form + + + + 0 + + + + + Mouse Enabled + + + true + + + + + + + Manual + + + + + + + 0 + + + + + + + 0 + + + + + + + Auto + + + true + + + + + + + true + + + % + + + 1 + + + 100 + + + 1 + + + 100 + + + + + + + Auto Pan Only + + + + + + + + + + Link Axis: + + + + + + + + diff --git a/graphicsItems/__init__.py b/graphicsItems/__init__.py new file mode 100644 index 00000000..8e411816 --- /dev/null +++ b/graphicsItems/__init__.py @@ -0,0 +1,21 @@ +### just import everything from sub-modules + +#import os + +#d = os.path.split(__file__)[0] +#files = [] +#for f in os.listdir(d): + #if os.path.isdir(os.path.join(d, f)): + #files.append(f) + #elif f[-3:] == '.py' and f != '__init__.py': + #files.append(f[:-3]) + +#for modName in files: + #mod = __import__(modName, globals(), locals(), fromlist=['*']) + #if hasattr(mod, '__all__'): + #names = mod.__all__ + #else: + #names = [n for n in dir(mod) if n[0] != '_'] + #for k in names: + ##print modName, k + #globals()[k] = getattr(mod, k) diff --git a/graphicsWindows.py b/graphicsWindows.py index 8b8e8678..f2cf87f4 100644 --- a/graphicsWindows.py +++ b/graphicsWindows.py @@ -5,9 +5,11 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from PyQt4 import QtCore, QtGui -from PlotWidget import * -from ImageView import * +from Qt import QtCore, QtGui +from widgets.PlotWidget import * +from imageview import * +from widgets.GraphicsLayoutWidget import GraphicsLayoutWidget +from widgets.GraphicsView import GraphicsView QAPP = None def mkQApp(): @@ -15,49 +17,12 @@ def mkQApp(): global QAPP QAPP = QtGui.QApplication([]) -class GraphicsLayoutWidget(GraphicsView): - def __init__(self): - GraphicsView.__init__(self) - self.items = {} - self.currentRow = 0 - self.currentCol = 0 - - def nextRow(self): - """Advance to next row for automatic item placement""" - self.currentRow += 1 - self.currentCol = 0 - - def nextCol(self, colspan=1): - """Advance to next column, while returning the current column number - (generally only for internal use)""" - self.currentCol += colspan - return self.currentCol-colspan - - def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs): - plot = PlotItem(**kargs) - self.addItem(plot, row, col, rowspan, colspan) - return plot - - def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): - if row not in self.items: - self.items[row] = {} - self.items[row][col] = item - - if row is None: - row = self.currentRow - if col is None: - col = self.nextCol(colspan) - self.centralLayout.addItem(item, row, col, rowspan, colspan) - - def getItem(self, row, col): - return self.items[row][col] - class GraphicsWindow(GraphicsLayoutWidget): - def __init__(self, title=None, size=(800,600)): + def __init__(self, title=None, size=(800,600), **kargs): mkQApp() self.win = QtGui.QMainWindow() - GraphicsLayoutWidget.__init__(self) + GraphicsLayoutWidget.__init__(self, **kargs) self.win.setCentralWidget(self) self.win.resize(*size) if title is not None: @@ -83,19 +48,6 @@ class TabWindow(QtGui.QMainWindow): raise NameError(attr) -#class PlotWindow(QtGui.QMainWindow): - #def __init__(self, title=None, **kargs): - #mkQApp() - #QtGui.QMainWindow.__init__(self) - #self.cw = PlotWidget(**kargs) - #self.setCentralWidget(self.cw) - #for m in ['plot', 'autoRange', 'addItem', 'removeItem', 'setLabel', 'clear', 'viewRect']: - #setattr(self, m, getattr(self.cw, m)) - #if title is not None: - #self.setWindowTitle(title) - #self.show() - - class PlotWindow(PlotWidget): def __init__(self, title=None, **kargs): mkQApp() diff --git a/graphicsWindows.pyc b/graphicsWindows.pyc new file mode 100644 index 0000000000000000000000000000000000000000..415b490152399763e69e5d88fec199297f1cb2fb GIT binary patch literal 4242 zcmc&%&2Ah;5U!bBukH13?8FchiHQkf6^J(pIRZ-JpeRbR*kg+XOK3FSX|HEJJL62Z zvoSIk94-iuxNze+xbXtKOD;SBd{sTWv-v^XmbJU4|E9aT>ifECDu2z@f4Tgk)u!TC z!Sg1H{sST++JST^aw&1B=hBW#y$bDA$l=}!B~{ukY7@3rrKCo^N!ppTb0#R6qTV#^ zOryO(;S5DJdX9x>DViiTNqbmmihE9pzS^0iXqr@oy7Mwn9}X-~G(&1pdS{2dXDFH@ zwIsds!`@}WPFsydKF6OB^-dlh?#FHYC{ClSuh$Nby(^x#o~2J!8mqLeymk_5t+dz2 zw0*CgtI#Mf^itLL`tlMUX5ZULGV>^oI?8xi?%nH!9rb0b`fK&|?C>~`JNw4Fe)ZZ_ z&wFrmpuF|4cNlikED7s(Vr}wx_sFQoJ4z#!dk^nzHy>ZS<|T1krCP0dTS^TZ=9%&K zutP7)l^3UbSuZqkmaf&~zwv9hIFzYDY?^hbTS@Hjgv z)VHIvBcptdAq;qfbrk&}gdyrW)OFcIfW*NR6ZOO8&2J7RCD!jwy zt;0i{Qchr|&=IHFgs{4)qS4AyTiLv|vC%*Wl|t{L+1$^1s+qu`&3=|2G<#th>mUht zo9!%8&9MFCMl*?bn@1*2nuo_vjCi}Lp27Nw(#^Fs{x1h!2ZNZhcffisU_;Gnk#o*9 z^LP%dXR$N1Pz6J`QS@673V0SiL{;X>a}cFM{+8Gw_AoVm42!n;9bTKC_fhmy2w3+N zyTNJJ!Y4#Qg@AE(aWNv1WmDptXZB!YGXpT&+42*(ah?a zW7yx@;qJcy+QV=GXrOtb1aws48+ND4$mYwZw1FU-LBcz%qv#&QOUMQyN1$6B;Fe)O z!KlPwAn5J_(*SVI!h8tpKzd)r^nU=_7ksljeI~XzW>b#NSGZwq+kKy5YMsRX_Rl~x z_%Qwwi)9vkjh5aUm*ZIRRg@tP7o9o5;w;eEX6?DKs$%o&sKDlIJx93jQoc$!e%zEX5rHn-&26d7fvvkU$J1B<2ZYw~t}(20@`; z5PXEDW?+)D z5EJnTk$@2e998HPSpl-aCpYO)8G^nq#=G^`L(ohaf>_~}5VOH)LK%lFZMkp68q8R^ zMTM_|OvDy}>IHtAPwbZjW+RLcnD0Nyi+ZJ_$4TQG*kAK~G8#@i#|9M9^$ox*RUIoP zJ!ttc${=1a%_P-*C1P;OAduu6vw$Q?h;TVX@Nk<2A8!if3l#kV;>~!pnCc?t0M~$( ziw_21%%y`G!g`=8XvMqqnYP@`AXDl 1: data = data.mean(axis=1) - self.roiCurve.setData(y=data, x=self.tVals) + if image.ndim == 3: + self.roiCurve.setData(y=data, x=self.tVals) + else: + self.roiCurve.setData(y=data, x=range(len(data))) + #self.ui.roiPlot.replot() def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None): @@ -390,15 +418,16 @@ class ImageView(QtGui.QWidget): self.imageDisp = None + prof.mark('3') + + self.currentIndex = 0 + self.updateImage() if levels is None and autoLevels: self.autoLevels() if levels is not None: ## this does nothing since getProcessedImage sets these values again. self.levelMax = levels[1] self.levelMin = levels[0] - prof.mark('3') - self.currentIndex = 0 - self.updateImage() if self.ui.roiBtn.isChecked(): self.roiChanged() prof.mark('4') @@ -436,29 +465,31 @@ class ImageView(QtGui.QWidget): self.roiClicked() prof.mark('7') prof.finish() - + + def autoLevels(self): - image = self.getProcessedImage() + #image = self.getProcessedImage() + self.setLevels(self.levelMin, self.levelMax) - #self.ui.whiteSlider.setValue(self.ui.whiteSlider.maximum()) - #self.ui.blackSlider.setValue(0) - - self.ui.gradientWidget.setTickValue(self.ticks[0], 0.0) - self.ui.gradientWidget.setTickValue(self.ticks[1], 1.0) - self.imageItem.setLevels(white=self.whiteLevel(), black=self.blackLevel()) + #self.ui.histogram.imageChanged(autoLevel=True) + def setLevels(self, min, max): + self.ui.histogram.setLevels(min, max) + def autoRange(self): image = self.getProcessedImage() #self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) - self.ui.graphicsView.setRange(self.imageItem.sceneBoundingRect(), padding=0., lockAspect=True) + self.view.setRange(self.imageItem.boundingRect(), padding=0.) def getProcessedImage(self): if self.imageDisp is None: image = self.normalize(self.image) self.imageDisp = image self.levelMin, self.levelMax = map(float, ImageView.quickMinMax(self.imageDisp)) + self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) + return self.imageDisp @staticmethod @@ -536,14 +567,14 @@ class ImageView(QtGui.QWidget): #print "update:", image.ndim, image.max(), image.min(), self.blackLevel(), self.whiteLevel() if self.axes['t'] is None: #self.ui.timeSlider.hide() - self.imageItem.updateImage(image, white=self.whiteLevel(), black=self.blackLevel()) - self.ui.roiPlot.hide() - self.ui.roiBtn.hide() + self.imageItem.updateImage(image) + #self.ui.roiPlot.hide() + #self.ui.roiBtn.hide() else: - self.ui.roiBtn.show() + #self.ui.roiBtn.show() self.ui.roiPlot.show() #self.ui.timeSlider.show() - self.imageItem.updateImage(image[self.currentIndex], white=self.whiteLevel(), black=self.blackLevel()) + self.imageItem.updateImage(image[self.currentIndex]) def timeIndex(self, slider): @@ -574,11 +605,12 @@ class ImageView(QtGui.QWidget): #print ind return ind, t - def whiteLevel(self): - return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[1]) - #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum() + #def whiteLevel(self): + #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[1]) + ##return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum() - def blackLevel(self): - return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0]) - #return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() - \ No newline at end of file + #def blackLevel(self): + #return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0]) + ##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value() + + \ No newline at end of file diff --git a/ImageViewTemplate.py b/imageview/ImageViewTemplate.py similarity index 80% rename from ImageViewTemplate.py rename to imageview/ImageViewTemplate.py index fe283a74..cf00ed7f 100644 --- a/ImageViewTemplate.py +++ b/imageview/ImageViewTemplate.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './lib/util/pyqtgraph/ImageViewTemplate.ui' +# Form implementation generated from reading ui file 'ImageViewTemplate.ui' # -# Created: Wed May 18 20:44:20 2011 +# Created: Tue Jan 17 23:09:04 2012 # by: PyQt4 UI code generator 4.8.3 # # WARNING! All changes made in this file will be lost! @@ -18,57 +18,53 @@ class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) Form.resize(726, 588) - self.verticalLayout = QtGui.QVBoxLayout(Form) - self.verticalLayout.setSpacing(0) - self.verticalLayout.setMargin(0) - self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.gridLayout_3 = QtGui.QGridLayout(Form) + self.gridLayout_3.setMargin(0) + self.gridLayout_3.setSpacing(0) + self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) self.splitter = QtGui.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Vertical) self.splitter.setObjectName(_fromUtf8("splitter")) self.layoutWidget = QtGui.QWidget(self.splitter) self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.gridLayout = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout.setMargin(0) self.gridLayout.setSpacing(0) self.gridLayout.setMargin(0) self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.graphicsView = GraphicsView(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(10) - sizePolicy.setVerticalStretch(10) - sizePolicy.setHeightForWidth(self.graphicsView.sizePolicy().hasHeightForWidth()) - self.graphicsView.setSizePolicy(sizePolicy) self.graphicsView.setObjectName(_fromUtf8("graphicsView")) - self.gridLayout.addWidget(self.graphicsView, 1, 0, 3, 1) + self.gridLayout.addWidget(self.graphicsView, 0, 0, 2, 1) + self.histogram = HistogramLUTWidget(self.layoutWidget) + self.histogram.setObjectName(_fromUtf8("histogram")) + self.gridLayout.addWidget(self.histogram, 0, 1, 1, 2) self.roiBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) self.roiBtn.setSizePolicy(sizePolicy) - self.roiBtn.setMaximumSize(QtCore.QSize(30, 16777215)) self.roiBtn.setCheckable(True) self.roiBtn.setObjectName(_fromUtf8("roiBtn")) - self.gridLayout.addWidget(self.roiBtn, 3, 3, 1, 1) - self.gradientWidget = GradientWidget(self.layoutWidget) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(100) - sizePolicy.setHeightForWidth(self.gradientWidget.sizePolicy().hasHeightForWidth()) - self.gradientWidget.setSizePolicy(sizePolicy) - self.gradientWidget.setObjectName(_fromUtf8("gradientWidget")) - self.gridLayout.addWidget(self.gradientWidget, 1, 3, 1, 1) + self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) self.normBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) self.normBtn.setSizePolicy(sizePolicy) - self.normBtn.setMaximumSize(QtCore.QSize(30, 16777215)) self.normBtn.setCheckable(True) self.normBtn.setObjectName(_fromUtf8("normBtn")) - self.gridLayout.addWidget(self.normBtn, 2, 3, 1, 1) - self.normGroup = QtGui.QGroupBox(self.layoutWidget) + self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1) + self.roiPlot = PlotWidget(self.splitter) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) + self.roiPlot.setSizePolicy(sizePolicy) + self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) + self.roiPlot.setObjectName(_fromUtf8("roiPlot")) + self.gridLayout_3.addWidget(self.splitter, 0, 0, 1, 1) + self.normGroup = QtGui.QGroupBox(Form) self.normGroup.setObjectName(_fromUtf8("normGroup")) self.gridLayout_2 = QtGui.QGridLayout(self.normGroup) self.gridLayout_2.setMargin(0) @@ -136,24 +132,15 @@ class Ui_Form(object): self.normTBlurSpin = QtGui.QDoubleSpinBox(self.normGroup) self.normTBlurSpin.setObjectName(_fromUtf8("normTBlurSpin")) self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) - self.gridLayout.addWidget(self.normGroup, 0, 0, 1, 4) - self.roiPlot = PlotWidget(self.splitter) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) - self.roiPlot.setSizePolicy(sizePolicy) - self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) - self.roiPlot.setObjectName(_fromUtf8("roiPlot")) - self.verticalLayout.addWidget(self.splitter) + self.gridLayout_3.addWidget(self.normGroup, 1, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.roiBtn.setText(QtGui.QApplication.translate("Form", "R", None, QtGui.QApplication.UnicodeUTF8)) - self.normBtn.setText(QtGui.QApplication.translate("Form", "N", None, QtGui.QApplication.UnicodeUTF8)) + self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8)) + self.normBtn.setText(QtGui.QApplication.translate("Form", "Norm", None, QtGui.QApplication.UnicodeUTF8)) self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8)) self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8)) self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8)) @@ -168,6 +155,6 @@ class Ui_Form(object): self.normTimeRangeCheck.setText(QtGui.QApplication.translate("Form", "Time range", None, QtGui.QApplication.UnicodeUTF8)) self.normFrameCheck.setText(QtGui.QApplication.translate("Form", "Frame", None, QtGui.QApplication.UnicodeUTF8)) -from GraphicsView import GraphicsView -from pyqtgraph.GradientWidget import GradientWidget -from PlotWidget import PlotWidget +from pyqtgraph.widgets.GraphicsView import GraphicsView +from pyqtgraph.widgets.PlotWidget import PlotWidget +from pyqtgraph.widgets.HistogramLUTWidget import HistogramLUTWidget diff --git a/imageview/ImageViewTemplate.ui b/imageview/ImageViewTemplate.ui new file mode 100644 index 00000000..497c0c59 --- /dev/null +++ b/imageview/ImageViewTemplate.ui @@ -0,0 +1,252 @@ + + + Form + + + + 0 + 0 + 726 + 588 + + + + Form + + + + 0 + + + 0 + + + + + Qt::Vertical + + + + + 0 + + + + + + + + + + + + 0 + 1 + + + + ROI + + + true + + + + + + + + 0 + 1 + + + + Norm + + + true + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + + + + + Normalization + + + + 0 + + + 0 + + + + + Subtract + + + + + + + Divide + + + false + + + + + + + + 75 + true + + + + Operation: + + + + + + + + 75 + true + + + + Mean: + + + + + + + + 75 + true + + + + Blur: + + + + + + + ROI + + + + + + + + + + X + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Y + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + T + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Off + + + true + + + + + + + Time range + + + + + + + Frame + + + + + + + + + + + + + + PlotWidget + QWidget +
pyqtgraph.widgets.PlotWidget
+ 1 +
+ + GraphicsView + QGraphicsView +
pyqtgraph.widgets.GraphicsView
+
+ + HistogramLUTWidget + QGraphicsView +
pyqtgraph.widgets.HistogramLUTWidget
+
+
+ + +
diff --git a/imageview/__init__.py b/imageview/__init__.py new file mode 100644 index 00000000..7bbbe122 --- /dev/null +++ b/imageview/__init__.py @@ -0,0 +1,6 @@ +""" +Widget used for display and analysis of 2D and 3D image data. +Includes ROI plotting over time and image normalization. +""" + +from ImageView import ImageView diff --git a/parametertree/Parameter.py b/parametertree/Parameter.py new file mode 100644 index 00000000..39edb880 --- /dev/null +++ b/parametertree/Parameter.py @@ -0,0 +1,465 @@ +from pyqtgraph.Qt import QtGui, QtCore +import collections, os, weakref, re +from ParameterItem import ParameterItem + +PARAM_TYPES = {} + + +def registerParameterType(name, cls, override=False): + global PARAM_TYPES + if name in PARAM_TYPES and not override: + raise Exception("Parameter type '%s' already exists (use override=True to replace)" % name) + PARAM_TYPES[name] = cls + + + +class Parameter(QtCore.QObject): + """Tree of name=value pairs (modifiable or not) + - Value may be integer, float, string, bool, color, or list selection + - Optionally, a custom widget may be specified for a property + - Any number of extra columns may be added for other purposes + - Any values may be reset to a default value + - Parameters may be grouped / nested + + For more Parameter types, see ParameterTree.parameterTypes module. + """ + ## name, type, limits, etc. + ## can also carry UI hints (slider vs spinbox, etc.) + + sigValueChanged = QtCore.Signal(object, object) ## self, value emitted when value is finished being edited + sigValueChanging = QtCore.Signal(object, object) ## self, value emitted as value is being edited + + sigChildAdded = QtCore.Signal(object, object, object) ## self, child, index + sigChildRemoved = QtCore.Signal(object, object) ## self, child + sigParentChanged = QtCore.Signal(object, object) ## self, parent + sigLimitsChanged = QtCore.Signal(object, object) ## self, limits + sigDefaultChanged = QtCore.Signal(object, object) ## self, default + sigNameChanged = QtCore.Signal(object, object) ## self, name + sigOptionsChanged = QtCore.Signal(object, object) ## self, {opt:val, ...} + + ## Emitted when anything changes about this parameter at all. + ## The second argument is a string indicating what changed ('value', 'childAdded', etc..) + ## The third argument can be any extra information about the change + sigStateChanged = QtCore.Signal(object, object, object) ## self, change, info + + ## emitted when any child in the tree changes state + ## (but only if monitorChildren() is called) + sigTreeStateChanged = QtCore.Signal(object, object) # self, changes + # changes = [(param, change, info), ...] + + # bad planning. + #def __new__(cls, *args, **opts): + #try: + #cls = PARAM_TYPES[opts['type']] + #except KeyError: + #pass + #return QtCore.QObject.__new__(cls, *args, **opts) + + @staticmethod + def create(**opts): + """ + Create a new Parameter (or subclass) instance using opts['type'] to select the + appropriate class. + + Use registerParameterType() to add new class types. + """ + cls = PARAM_TYPES[opts['type']] + return cls(**opts) + + def __init__(self, **opts): + QtCore.QObject.__init__(self) + + self.opts = { + 'readonly': False, + 'visible': True, + 'enabled': True, + 'renamable': False, + 'removable': False, + 'strictNaming': False, # forces name to be usable as a python variable + } + self.opts.update(opts) + + self.childs = [] + self.names = {} ## map name:child + self.items = weakref.WeakKeyDictionary() ## keeps track of tree items representing this parameter + self._parent = None + self.treeStateChanges = [] ## cache of tree state changes to be delivered on next emit + self.blockTreeChangeEmit = 0 + #self.monitoringChildren = False ## prevent calling monitorChildren more than once + + if 'value' not in self.opts: + self.opts['value'] = None + + if 'name' not in self.opts or not isinstance(self.opts['name'], basestring): + raise Exception("Parameter must have a string name specified in opts.") + self.setName(opts['name']) + + for chOpts in self.opts.get('children', []): + #print self, "Add child:", type(chOpts), id(chOpts) + self.addChild(chOpts) + + if 'value' in self.opts and 'default' not in self.opts: + self.opts['default'] = self.opts['value'] + + ## Connect all state changed signals to the general sigStateChanged + self.sigValueChanged.connect(lambda param, data: self.emitStateChanged('value', data)) + self.sigChildAdded.connect(lambda param, *data: self.emitStateChanged('childAdded', data)) + self.sigChildRemoved.connect(lambda param, data: self.emitStateChanged('childRemoved', data)) + self.sigParentChanged.connect(lambda param, data: self.emitStateChanged('parent', data)) + self.sigLimitsChanged.connect(lambda param, data: self.emitStateChanged('limits', data)) + self.sigDefaultChanged.connect(lambda param, data: self.emitStateChanged('default', data)) + self.sigNameChanged.connect(lambda param, data: self.emitStateChanged('name', data)) + self.sigOptionsChanged.connect(lambda param, data: self.emitStateChanged('options', data)) + + #self.watchParam(self) ## emit treechange signals if our own state changes + + def name(self): + return self.opts['name'] + + def setName(self, name): + """Attempt to change the name of this parameter; return the actual name. + (The parameter may reject the name change or automatically pick a different name)""" + if self.opts['strictNaming']: + if len(name) < 1 or re.search(r'\W', name) or re.match(r'\d', name[0]): + raise Exception("Parameter name '%s' is invalid. (Must contain only alphanumeric and underscore characters and may not start with a number)" % name) + parent = self.parent() + if parent is not None: + name = parent._renameChild(self, name) ## first ask parent if it's ok to rename + if self.opts['name'] != name: + self.opts['name'] = name + self.sigNameChanged.emit(self, name) + return name + + def childPath(self, child): + """Return the path of parameter names from self to child.""" + path = [] + while child is not self: + path.insert(0, child.name()) + child = child.parent() + return path + + def setValue(self, value, blockSignal=None): + ## return the actual value that was set + ## (this may be different from the value that was requested) + #print self, "Set value:", value, self.opts['value'], self.opts['value'] == value + try: + if blockSignal is not None: + self.sigValueChanged.disconnect(blockSignal) + if self.opts['value'] == value: + return value + self.opts['value'] = value + self.sigValueChanged.emit(self, value) + finally: + if blockSignal is not None: + self.sigValueChanged.connect(blockSignal) + + return value + + def value(self): + return self.opts['value'] + + def getValues(self): + """Return a tree of all values that are children of this parameter""" + vals = collections.OrderedDict() + for ch in self: + vals[ch.name()] = (ch.value(), ch.getValues()) + return vals + + def saveState(self): + """Return a structure representing the entire state of the parameter tree.""" + state = self.opts.copy() + state['children'] = {ch.name(): ch.saveState() for ch in self} + return state + + def defaultValue(self): + return self.opts['default'] + + def setDefault(self, val): + self.opts['default'] = val + self.sigDefaultChanged.emit(self, val) + + def setToDefault(self): + if self.hasDefault(): + self.setValue(self.defaultValue()) + + def hasDefault(self): + return 'default' in self.opts + + def valueIsDefault(self): + return self.value() == self.defaultValue() + + def setLimits(self, limits): + if 'limits' in self.opts and self.opts['limits'] == limits: + return + self.opts['limits'] = limits + self.sigLimitsChanged.emit(self, limits) + return limits + + def writable(self): + return not self.opts.get('readonly', False) + + def setOpts(self, **opts): + """For setting any arbitrary options.""" + changed = collections.OrderedDict() + for k in opts: + if k == 'value': + self.setValue(opts[k]) + elif k == 'name': + self.setName(opts[k]) + elif k == 'limits': + self.setLimits(opts[k]) + elif k == 'default': + self.setDefault(opts[k]) + elif k not in self.opts or self.opts[k] != opts[k]: + self.opts[k] = opts[k] + changed[k] = opts[k] + + if len(changed) > 0: + self.sigOptionsChanged.emit(self, changed) + + def emitStateChanged(self, changeDesc, data): + ## Emits stateChanged signal and + ## requests emission of new treeStateChanged signal + self.sigStateChanged.emit(self, changeDesc, data) + #self.treeStateChanged(self, changeDesc, data) + self.treeStateChanges.append((self, changeDesc, data)) + self.emitTreeChanges() + + def makeTreeItem(self, depth): + """Return a TreeWidgetItem suitable for displaying/controlling the content of this parameter. + Most subclasses will want to override this function. + """ + if hasattr(self, 'itemClass'): + #print "Param:", self, "Make item from itemClass:", self.itemClass + return self.itemClass(self, depth) + else: + return ParameterItem(self, depth=depth) + + + def addChild(self, child): + """Add another parameter to the end of this parameter's child list.""" + return self.insertChild(len(self.childs), child) + + def insertChild(self, pos, child): + """Insert a new child at pos. + If pos is a Parameter, then insert at the position of that Parameter. + If child is a dict, then a parameter is constructed as Parameter(**child) + """ + if isinstance(child, dict): + child = Parameter.create(**child) + + name = child.name() + if name in self.names: + if child.opts.get('autoIncrementName', False): + name = self.incrementName(name) + child.setName(name) + else: + raise Exception("Already have child named %s" % str(name)) + if isinstance(pos, Parameter): + pos = self.childs.index(pos) + + if child.parent() is not None: + child.remove() + + self.names[name] = child + self.childs.insert(pos, child) + + child.parentChanged(self) + self.sigChildAdded.emit(self, child, pos) + child.sigTreeStateChanged.connect(self.treeStateChanged) + return child + + def removeChild(self, child): + name = child.name() + if name not in self.names or self.names[name] is not child: + raise Exception("Parameter %s is not my child; can't remove." % str(child)) + + del self.names[name] + self.childs.pop(self.childs.index(child)) + child.parentChanged(None) + self.sigChildRemoved.emit(self, child) + child.sigTreeStateChanged.disconnect(self.treeStateChanged) + + def clearChildren(self): + for ch in self.childs[:]: + self.removeChild(ch) + + def parentChanged(self, parent): + self._parent = parent + self.sigParentChanged.emit(self, parent) + + def parent(self): + return self._parent + + def remove(self): + """Remove self from parent's child list""" + parent = self.parent() + if parent is None: + raise Exception("Cannot remove; no parent.") + parent.removeChild(self) + + def incrementName(self, name): + ## return an unused name by adding a number to the name given + base, num = re.match('(.*)(\d*)', name).groups() + numLen = len(num) + if numLen == 0: + num = 2 + numLen = 1 + else: + num = int(num) + while True: + newName = base + ("%%0%dd"%numLen) % num + if newName not in self.childs: + return newName + num += 1 + + def __iter__(self): + for ch in self.childs: + yield ch + + def __getitem__(self, names): + """Get the value of a child parameter""" + if not isinstance(names, tuple): + names = (names,) + return self.param(*names).value() + + def __setitem__(self, names, value): + """Set the value of a child parameter""" + if isinstance(names, basestring): + names = (names,) + return self.param(*names).setValue(value) + + def param(self, *names): + """Return a child parameter. + Accepts the name of the child or a tuple (path, to, child)""" + try: + param = self.names[names[0]] + except KeyError: + raise Exception("Parameter %s has no child named %s" % (self.name(), names[0])) + + if len(names) > 1: + return param.param(*names[1:]) + else: + return param + + def __repr__(self): + return "<%s '%s' at 0x%x>" % (self.__class__.__name__, self.name(), id(self)) + + def __getattr__(self, attr): + #print type(self), attr + if attr in self.names: + return self.param(attr) + else: + raise AttributeError(attr) + + def _renameChild(self, child, name): + ## Only to be called from Parameter.rename + if name in self.names: + return child.name() + self.names[name] = child + del self.names[child.name()] + return name + + def registerItem(self, item): + self.items[item] = None + + def hide(self): + self.show(False) + + def show(self, s=True): + self.opts['visible'] = s + self.sigOptionsChanged.emit(self, {'visible': s}) + + + #def monitorChildren(self): + #if self.monitoringChildren: + #raise Exception("Already monitoring children.") + #self.watchParam(self) + #self.monitoringChildren = True + + #def watchParam(self, param): + #param.sigChildAdded.connect(self.grandchildAdded) + #param.sigChildRemoved.connect(self.grandchildRemoved) + #param.sigStateChanged.connect(self.grandchildChanged) + #for ch in param: + #self.watchParam(ch) + + #def unwatchParam(self, param): + #param.sigChildAdded.disconnect(self.grandchildAdded) + #param.sigChildRemoved.disconnect(self.grandchildRemoved) + #param.sigStateChanged.disconnect(self.grandchildChanged) + #for ch in param: + #self.unwatchParam(ch) + + #def grandchildAdded(self, parent, child): + #self.watchParam(child) + + #def grandchildRemoved(self, parent, child): + #self.unwatchParam(child) + + #def grandchildChanged(self, param, change, data): + ##self.sigTreeStateChanged.emit(self, param, change, data) + #self.emitTreeChange((param, change, data)) + + def treeChangeBlocker(self): + """ + Return an object that can be used to temporarily block and accumulate + sigTreeStateChanged signals. This is meant to be used when numerous changes are + about to be made to the tree and only one change signal should be + emitted at the end. + + Example: + with param.treeChangeBlocker(): + param.addChild(...) + param.removeChild(...) + param.setValue(...) + """ + return SignalBlocker(self.blockTreeChangeSignal, self.unblockTreeChangeSignal) + + def blockTreeChangeSignal(self): + """ + Used to temporarily block and accumulate tree change signals. + *You must remember to unblock*, so it is advisable to use treeChangeBlocker() instead. + """ + self.blockTreeChangeEmit += 1 + + def unblockTreeChangeSignal(self): + """Unblocks enission of sigTreeStateChanged and flushes the changes out through a single signal.""" + self.blockTreeChangeEmit -= 1 + self.emitTreeChanges() + + + def treeStateChanged(self, param, changes): + """ + Called when the state of any sub-parameter has changed. + Arguments: + param: the immediate child whose tree state has changed. + note that the change may have originated from a grandchild. + changes: list of tuples describing all changes that have been made + in this event: (param, changeDescr, data) + + This function can be extended to react to tree state changes. + """ + self.treeStateChanges.extend(changes) + self.emitTreeChanges() + + def emitTreeChanges(self): + if self.blockTreeChangeEmit == 0: + changes = self.treeStateChanges + self.treeStateChanges = [] + self.sigTreeStateChanged.emit(self, changes) + + +class SignalBlocker: + def __init__(self, enterFn, exitFn): + self.enterFn = enterFn + self.exitFn = exitFn + + def __enter__(self): + self.enterFn() + + def __exit__(self, exc_type, exc_value, tb): + self.exitFn() + + + \ No newline at end of file diff --git a/parametertree/ParameterItem.py b/parametertree/ParameterItem.py new file mode 100644 index 00000000..605e6317 --- /dev/null +++ b/parametertree/ParameterItem.py @@ -0,0 +1,148 @@ +from pyqtgraph.Qt import QtGui, QtCore +import collections, os, weakref, re + +class ParameterItem(QtGui.QTreeWidgetItem): + """ + Abstract ParameterTree item. + Used to represent the state of a Parameter from within a ParameterTree. + - Sets first column of item to name + - generates context menu if item is renamable or removable + - handles child added / removed events + - provides virtual functions for handling changes from parameter + For more ParameterItem types, see ParameterTree.parameterTypes module. + """ + + def __init__(self, param, depth=0): + QtGui.QTreeWidgetItem.__init__(self, [param.name(), '']) + + self.param = param + self.param.registerItem(self) ## let parameter know this item is connected to it (for debugging) + self.depth = depth + + param.sigValueChanged.connect(self.valueChanged) + param.sigChildAdded.connect(self.childAdded) + param.sigChildRemoved.connect(self.childRemoved) + param.sigNameChanged.connect(self.nameChanged) + param.sigLimitsChanged.connect(self.limitsChanged) + param.sigDefaultChanged.connect(self.defaultChanged) + param.sigOptionsChanged.connect(self.optsChanged) + + + opts = param.opts + + ## Generate context menu for renaming/removing parameter + self.contextMenu = QtGui.QMenu() + self.contextMenu.addSeparator() + flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + if opts.get('renamable', False): + flags |= QtCore.Qt.ItemIsEditable + self.contextMenu.addAction('Rename').triggered.connect(self.editName) + if opts.get('removable', False): + self.contextMenu.addAction("Remove").triggered.connect(self.param.remove) + + ## handle movable / dropEnabled options + if opts.get('movable', False): + flags |= QtCore.Qt.ItemIsDragEnabled + if opts.get('dropEnabled', False): + flags |= QtCore.Qt.ItemIsDropEnabled + self.setFlags(flags) + + ## flag used internally during name editing + self.ignoreNameColumnChange = False + + + def valueChanged(self, param, val): + ## called when the parameter's value has changed + pass + + def isFocusable(self): + """Return True if this item should be included in the tab-focus order""" + return False + + def setFocus(self): + """Give input focus to this item. + Can be reimplemented to display editor widgets, etc. + """ + pass + + def focusNext(self, forward=True): + """Give focus to the next (or previous) focusable item in the parameter tree""" + self.treeWidget().focusNext(self, forward=forward) + + + def treeWidgetChanged(self): + """Called when this item is added or removed from a tree. + Expansion, visibility, and column widgets must all be configured AFTER + the item is added to a tree, not during __init__. + """ + self.setHidden(not self.param.opts.get('visible', True)) + self.setExpanded(self.param.opts.get('expanded', True)) + + def childAdded(self, param, child, pos): + item = child.makeTreeItem(depth=self.depth+1) + self.insertChild(pos, item) + item.treeWidgetChanged() + + for i, ch in enumerate(child): + item.childAdded(child, ch, i) + + def childRemoved(self, param, child): + for i in range(self.childCount()): + item = self.child(i) + if item.param is child: + self.takeChild(i) + break + + def contextMenuEvent(self, ev): + if not self.param.opts.get('removable', False) and not self.param.opts.get('renamable', False): + return + + self.contextMenu.popup(ev.globalPos()) + + def columnChangedEvent(self, col): + """Called when the text in a column has been edited. + By default, we only use changes to column 0 to rename the parameter. + """ + if col == 0: + if self.ignoreNameColumnChange: + return + try: + newName = self.param.setName(str(self.text(col))) + except: + self.setText(0, self.param.name()) + raise + + try: + self.ignoreNameColumnChange = True + self.nameChanged(self, newName) ## If the parameter rejects the name change, we need to set it back. + finally: + self.ignoreNameColumnChange = False + + def nameChanged(self, param, name): + ## called when the parameter's name has changed. + self.setText(0, name) + + def limitsChanged(self, param, limits): + """Called when the parameter's limits have changed""" + pass + + def defaultChanged(self, param, default): + """Called when the parameter's default value has changed""" + pass + + def optsChanged(self, param, opts): + """Called when any options are changed that are not + name, value, default, or limits""" + #print opts + if 'visible' in opts: + self.setHidden(not opts['visible']) + + def editName(self): + self.treeWidget().editItem(self, 0) + + def selected(self, sel): + """Called when this item has been selected (sel=True) OR deselected (sel=False)""" + pass + + + diff --git a/parametertree/ParameterTree.py b/parametertree/ParameterTree.py new file mode 100644 index 00000000..6f90de07 --- /dev/null +++ b/parametertree/ParameterTree.py @@ -0,0 +1,108 @@ +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.widgets.TreeWidget import TreeWidget +import collections, os, weakref, re +#import functions as fn + + + +class ParameterTree(TreeWidget): + """Widget used to display or control data from a ParameterSet""" + + def __init__(self, parent=None): + TreeWidget.__init__(self, parent) + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setHorizontalScrollMode(self.ScrollPerPixel) + self.setAnimated(False) + self.setColumnCount(2) + self.setHeaderLabels(["Parameter", "Value"]) + self.setRootIsDecorated(False) + self.setAlternatingRowColors(True) + self.paramSet = None + self.header().setResizeMode(QtGui.QHeaderView.ResizeToContents) + self.itemChanged.connect(self.itemChangedEvent) + self.lastSel = None + self.setRootIsDecorated(False) + + def setParameters(self, param, root=None, depth=0, showTop=True): + item = param.makeTreeItem(depth=depth) + if root is None: + root = self.invisibleRootItem() + ## Hide top-level item + if not showTop: + item.setText(0, '') + item.setSizeHint(0, QtCore.QSize(1,1)) + item.setSizeHint(1, QtCore.QSize(1,1)) + depth -= 1 + root.addChild(item) + item.treeWidgetChanged() + + for ch in param: + self.setParameters(ch, root=item, depth=depth+1) + + def focusNext(self, item, forward=True): + ## Give input focus to the next (or previous) item after 'item' + while True: + parent = item.parent() + if parent is None: + return + nextItem = self.nextFocusableChild(parent, item, forward=forward) + if nextItem is not None: + nextItem.setFocus() + self.setCurrentItem(nextItem) + return + item = parent + + def focusPrevious(self, item): + self.focusNext(item, forward=False) + + def nextFocusableChild(self, root, startItem=None, forward=True): + if startItem is None: + if forward: + index = 0 + else: + index = root.childCount()-1 + else: + if forward: + index = root.indexOfChild(startItem) + 1 + else: + index = root.indexOfChild(startItem) - 1 + + if forward: + inds = range(index, root.childCount()) + else: + inds = range(index, -1, -1) + + for i in inds: + item = root.child(i) + if hasattr(item, 'isFocusable') and item.isFocusable(): + return item + else: + item = self.nextFocusableChild(item, forward=forward) + if item is not None: + return item + return None + + def contextMenuEvent(self, ev): + item = self.currentItem() + if hasattr(item, 'contextMenuEvent'): + item.contextMenuEvent(ev) + + def itemChangedEvent(self, item, col): + if hasattr(item, 'columnChangedEvent'): + item.columnChangedEvent(col) + + def selectionChanged(self, *args): + sel = self.selectedItems() + if len(sel) != 1: + sel = None + if self.lastSel is not None: + self.lastSel.selected(False) + if sel is None: + self.lastSel = None + return + self.lastSel = sel[0] + if hasattr(sel[0], 'selected'): + sel[0].selected(True) + return TreeWidget.selectionChanged(self, *args) + + diff --git a/parametertree/__init__.py b/parametertree/__init__.py new file mode 100644 index 00000000..b5912f57 --- /dev/null +++ b/parametertree/__init__.py @@ -0,0 +1,5 @@ +from Parameter import Parameter, registerParameterType +from ParameterTree import ParameterTree +from ParameterItem import ParameterItem + +import parameterTypes as types \ No newline at end of file diff --git a/parametertree/__main__.py b/parametertree/__main__.py new file mode 100644 index 00000000..a3f0b11a --- /dev/null +++ b/parametertree/__main__.py @@ -0,0 +1,140 @@ +## tests for ParameterTree + +## make sure pyqtgraph is in path +import sys,os +md = os.path.abspath(os.path.dirname(__file__)) +sys.path.append(os.path.join(md, '..', '..')) + +from pyqtgraph.Qt import QtCore, QtGui +import collections, user +app = QtGui.QApplication([]) +import pyqtgraph.parametertree.parameterTypes as pTypes +from pyqtgraph.parametertree import Parameter, ParameterTree, ParameterItem, registerParameterType + + +## test subclassing parameters +## This parameter automatically generates two child parameters which are always reciprocals of each other +class ComplexParameter(Parameter): + def __init__(self, **opts): + opts['type'] = 'bool' + opts['value'] = True + Parameter.__init__(self, **opts) + + self.addChild({'name': 'A = 1/B', 'type': 'float', 'value': 7, 'suffix': 'Hz', 'siPrefix': True}) + self.addChild({'name': 'B = 1/A', 'type': 'float', 'value': 1/7., 'suffix': 's', 'siPrefix': True}) + self.a = self.param('A = 1/B') + self.b = self.param('B = 1/A') + self.a.sigValueChanged.connect(self.aChanged) + self.b.sigValueChanged.connect(self.bChanged) + + def aChanged(self): + try: + self.b.sigValueChanged.disconnect(self.bChanged) + self.b.setValue(1.0 / self.a.value()) + finally: + self.b.sigValueChanged.connect(self.bChanged) + + def bChanged(self): + try: + self.a.sigValueChanged.disconnect(self.aChanged) + self.a.setValue(1.0 / self.b.value()) + finally: + self.a.sigValueChanged.connect(self.aChanged) + + +## test add/remove +## this group includes a menu allowing the user to add new parameters into its child list +class ScalableGroup(pTypes.GroupParameter): + def __init__(self, **opts): + opts['type'] = 'group' + opts['addText'] = "Add" + opts['addList'] = ['str', 'float', 'int'] + pTypes.GroupParameter.__init__(self, **opts) + + def addNew(self, typ): + val = { + 'str': '', + 'float': 0.0, + 'int': 0 + }[typ] + self.addChild(dict(name="ScalableParam %d" % (len(self.childs)+1), type=typ, value=val, removable=True, renamable=True)) + + +## test column spanning (widget sub-item that spans all columns) +class TextParameterItem(pTypes.WidgetParameterItem): + def __init__(self, param, depth): + pTypes.WidgetParameterItem.__init__(self, param, depth) + self.subItem = QtGui.QTreeWidgetItem() + self.addChild(self.subItem) + + def treeWidgetChanged(self): + self.treeWidget().setFirstItemColumnSpanned(self.subItem, True) + self.treeWidget().setItemWidget(self.subItem, 0, self.textBox) + self.setExpanded(True) + + def makeWidget(self): + self.textBox = QtGui.QTextEdit() + self.textBox.setMaximumHeight(100) + self.textBox.value = lambda: str(self.textBox.toPlainText()) + self.textBox.setValue = self.textBox.setPlainText + self.textBox.sigChanged = self.textBox.textChanged + return self.textBox + +class TextParameter(Parameter): + type = 'text' + itemClass = TextParameterItem + +registerParameterType('text', TextParameter) + + + + +params = [ + {'name': 'Group 0', 'type': 'group', 'children': [ + {'name': 'Param 1', 'type': 'int', 'value': 10}, + {'name': 'Param 2', 'type': 'float', 'value': 10}, + ]}, + {'name': 'Group 1', 'type': 'group', 'children': [ + {'name': 'Param 1.1', 'type': 'float', 'value': 1.2e-6, 'dec': True, 'siPrefix': True, 'suffix': 'V'}, + {'name': 'Param 1.2', 'type': 'float', 'value': 1.2e6, 'dec': True, 'siPrefix': True, 'suffix': 'Hz'}, + {'name': 'Group 1.3', 'type': 'group', 'children': [ + {'name': 'Param 1.3.1', 'type': 'int', 'value': 11, 'limits': (-7, 15), 'default': -6}, + {'name': 'Param 1.3.2', 'type': 'float', 'value': 1.2e6, 'dec': True, 'siPrefix': True, 'suffix': 'Hz', 'readonly': True}, + ]}, + {'name': 'Param 1.4', 'type': 'str', 'value': "hi"}, + {'name': 'Param 1.5', 'type': 'list', 'values': [1,2,3], 'value': 2}, + {'name': 'Param 1.6', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2}, + ComplexParameter(name='ComplexParam'), + ScalableGroup(name="ScalableGroup", children=[ + {'name': 'ScalableParam 1', 'type': 'str', 'value': "hi"}, + {'name': 'ScalableParam 2', 'type': 'str', 'value': "hi"}, + + ]) + ]}, + {'name': 'Param 5', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"}, + {'name': 'Param 6', 'type': 'color', 'value': "FF0", 'tip': "This is a color button. It cam be renamed.", 'renamable': True}, + {'name': 'TextParam', 'type': 'text', 'value': 'Some text...'}, +] + +#p = pTypes.ParameterSet("params", params) +p = Parameter(name='params', type='group', children=params) +def change(param, changes): + print "tree changes:" + for param, change, data in changes: + print " [" + '.'.join(p.childPath(param))+ "] ", change, data + +p.sigTreeStateChanged.connect(change) + + +t = ParameterTree() +t.setParameters(p, showTop=False) +t.show() +t.resize(400,600) +t2 = ParameterTree() +t2.setParameters(p, showTop=False) +t2.show() +t2.resize(400,600) + +import sys +if sys.flags.interactive == 0: + app.exec_() diff --git a/parametertree/default.png b/parametertree/default.png new file mode 100644 index 0000000000000000000000000000000000000000..f12394214dadb66bd90e7c671d87a5810c1571a6 GIT binary patch literal 810 zcmV+_1J(SAP)zz1OUPe zo)1aVX}s1#$L?N9pk7vg&d^@u+F#8>SvWgp*|R{@KeW?!3fxz47$#1qCH6_Y)^y5H zFib~-aq$M`CM?fzU}@|U$SOTo#9WkucPXo&vB2K&eIgE|bw0-T(%|)#nK)Sc{AX5X zIWXIK##ob14q*wyDlklNU<++L7^Y&uFc|~-!AO{IKaDvBOR`X$$6{h|77ub^ps^|y z@hyVorLkzxE2BU^9tFCgFqp3K1eL%6WF<}}X}+szD2o9fg+8#Ph}aT;>%R=;NxN6^ zTbv-8ut4|b3g|{6U|~2MCR^ChTgEaKC)ux7-lsJ^PbRA}nwbcU4XA6=i!6g4>T`XP|J5%#wT3-^=I~qQWyqB2C1H_ z_Me+?4L0-^9XAQ?kmH0TldgX)VE^c=aNbnZcyEq2A2q2uJxpWGE*p|Gavqm`%@Q#W z38dLJis=%!hEL*?QgvCt%%ga6-E0rSv|=_efIkj~u&`hia8X517NcV-o8oKpY%2!R zm>szsnsM8u7~qjeox1WMKkXTaknYw+SY5C^YeS z2csdwiC&Q6hH#OEDXr!rU1uJ%@Lh>*$V47J6bmAsBv4|U6>byYGSUbfk$D~#xucHK z{Zv!lu04uMceeV4v#+L{6@Vo{eGhCx%J0&2Sm19dTTu(y$TBP2jv!^5H#OdTc&DoF zfSX$6?4fzaau?mB-@@gq<{k6mUm5H6LvOo$DP@b7ou<~qR$lC4S9y(2GiMgTKheDM oL#k?5npWtHR9tJvD)=vc0s@|Dx>pGq$N&HU07*qoM6N<$f=L-|8vp 0: + self.setValue(self.param.value()) + return w + + def value(self): + #vals = self.param.opts['limits'] + key = unicode(self.widget.currentText()) + #if isinstance(vals, dict): + #return vals[key] + #else: + #return key + print key, self.forward + return self.forward[key] + + def setValue(self, val): + #vals = self.param.opts['limits'] + #if isinstance(vals, dict): + #key = None + #for k,v in vals.iteritems(): + #if v == val: + #key = k + #if key is None: + #raise Exception("Value '%s' not allowed." % val) + #else: + #key = unicode(val) + if val not in self.reverse: + self.widget.setCurrentIndex(0) + else: + key = self.reverse[val] + ind = self.widget.findText(key) + self.widget.setCurrentIndex(ind) + + def limitsChanged(self, param, limits): + # set up forward / reverse mappings for name:value + self.forward = collections.OrderedDict() ## name: value + self.reverse = collections.OrderedDict() ## value: name + if isinstance(limits, dict): + for k, v in limits.iteritems(): + self.forward[k] = v + self.reverse[v] = k + else: + for v in limits: + n = unicode(v) + self.forward[n] = v + self.reverse[v] = n + + try: + self.widget.blockSignals(True) + val = unicode(self.widget.currentText()) + self.widget.clear() + for k in self.forward: + self.widget.addItem(k) + if k == val: + self.widget.setCurrentIndex(self.widget.count()-1) + + finally: + self.widget.blockSignals(False) + + + +class ListParameter(Parameter): + type = 'list' + itemClass = ListParameterItem + + def __init__(self, **opts): + self.forward = collections.OrderedDict() ## name: value + self.reverse = collections.OrderedDict() ## value: name + if 'values' in opts: + opts['limits'] = opts['values'] + Parameter.__init__(self, **opts) + + def setLimits(self, limits): + self.forward = collections.OrderedDict() ## name: value + self.reverse = collections.OrderedDict() ## value: name + if isinstance(limits, dict): + for k, v in limits.iteritems(): + self.forward[k] = v + self.reverse[v] = k + else: + for v in limits: + n = unicode(v) + self.forward[n] = v + self.reverse[v] = n + + Parameter.setLimits(self, limits) + #print self.name(), self.value(), limits + if self.value() not in self.reverse and len(self.reverse) > 0: + self.setValue(self.reverse.keys()[0]) + + +registerParameterType('list', ListParameter, override=True) + + diff --git a/plotConfigTemplate.py b/plotConfigTemplate.py deleted file mode 100644 index e0063b14..00000000 --- a/plotConfigTemplate.py +++ /dev/null @@ -1,295 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './lib/util/pyqtgraph/plotConfigTemplate.ui' -# -# Created: Wed May 18 20:44:20 2011 -# by: PyQt4 UI code generator 4.8.3 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName(_fromUtf8("Form")) - Form.resize(250, 340) - Form.setMaximumSize(QtCore.QSize(250, 350)) - self.gridLayout_3 = QtGui.QGridLayout(Form) - self.gridLayout_3.setMargin(0) - self.gridLayout_3.setSpacing(0) - self.gridLayout_3.setObjectName(_fromUtf8("gridLayout_3")) - self.tabWidget = QtGui.QTabWidget(Form) - self.tabWidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.tabWidget.setObjectName(_fromUtf8("tabWidget")) - self.tab = QtGui.QWidget() - self.tab.setObjectName(_fromUtf8("tab")) - self.verticalLayout = QtGui.QVBoxLayout(self.tab) - self.verticalLayout.setSpacing(0) - self.verticalLayout.setMargin(0) - self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) - self.groupBox = QtGui.QGroupBox(self.tab) - self.groupBox.setObjectName(_fromUtf8("groupBox")) - self.gridLayout = QtGui.QGridLayout(self.groupBox) - self.gridLayout.setMargin(0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName(_fromUtf8("gridLayout")) - self.xManualRadio = QtGui.QRadioButton(self.groupBox) - self.xManualRadio.setObjectName(_fromUtf8("xManualRadio")) - self.gridLayout.addWidget(self.xManualRadio, 0, 0, 1, 1) - self.xMinText = QtGui.QLineEdit(self.groupBox) - self.xMinText.setObjectName(_fromUtf8("xMinText")) - self.gridLayout.addWidget(self.xMinText, 0, 1, 1, 1) - self.xMaxText = QtGui.QLineEdit(self.groupBox) - self.xMaxText.setObjectName(_fromUtf8("xMaxText")) - self.gridLayout.addWidget(self.xMaxText, 0, 2, 1, 1) - self.xAutoRadio = QtGui.QRadioButton(self.groupBox) - self.xAutoRadio.setChecked(True) - self.xAutoRadio.setObjectName(_fromUtf8("xAutoRadio")) - self.gridLayout.addWidget(self.xAutoRadio, 1, 0, 1, 1) - self.xAutoPercentSpin = QtGui.QSpinBox(self.groupBox) - self.xAutoPercentSpin.setEnabled(True) - self.xAutoPercentSpin.setMinimum(1) - self.xAutoPercentSpin.setMaximum(100) - self.xAutoPercentSpin.setSingleStep(1) - self.xAutoPercentSpin.setProperty(_fromUtf8("value"), 100) - self.xAutoPercentSpin.setObjectName(_fromUtf8("xAutoPercentSpin")) - self.gridLayout.addWidget(self.xAutoPercentSpin, 1, 1, 1, 2) - self.xLinkCombo = QtGui.QComboBox(self.groupBox) - self.xLinkCombo.setObjectName(_fromUtf8("xLinkCombo")) - self.gridLayout.addWidget(self.xLinkCombo, 2, 1, 1, 2) - self.xMouseCheck = QtGui.QCheckBox(self.groupBox) - self.xMouseCheck.setChecked(True) - self.xMouseCheck.setObjectName(_fromUtf8("xMouseCheck")) - self.gridLayout.addWidget(self.xMouseCheck, 3, 1, 1, 1) - self.xLogCheck = QtGui.QCheckBox(self.groupBox) - self.xLogCheck.setObjectName(_fromUtf8("xLogCheck")) - self.gridLayout.addWidget(self.xLogCheck, 3, 0, 1, 1) - self.label = QtGui.QLabel(self.groupBox) - self.label.setObjectName(_fromUtf8("label")) - self.gridLayout.addWidget(self.label, 2, 0, 1, 1) - self.verticalLayout.addWidget(self.groupBox) - self.groupBox_2 = QtGui.QGroupBox(self.tab) - self.groupBox_2.setObjectName(_fromUtf8("groupBox_2")) - self.gridLayout_2 = QtGui.QGridLayout(self.groupBox_2) - self.gridLayout_2.setMargin(0) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) - self.yManualRadio = QtGui.QRadioButton(self.groupBox_2) - self.yManualRadio.setObjectName(_fromUtf8("yManualRadio")) - self.gridLayout_2.addWidget(self.yManualRadio, 0, 0, 1, 1) - self.yMinText = QtGui.QLineEdit(self.groupBox_2) - self.yMinText.setObjectName(_fromUtf8("yMinText")) - self.gridLayout_2.addWidget(self.yMinText, 0, 1, 1, 1) - self.yMaxText = QtGui.QLineEdit(self.groupBox_2) - self.yMaxText.setObjectName(_fromUtf8("yMaxText")) - self.gridLayout_2.addWidget(self.yMaxText, 0, 2, 1, 1) - self.yAutoRadio = QtGui.QRadioButton(self.groupBox_2) - self.yAutoRadio.setChecked(True) - self.yAutoRadio.setObjectName(_fromUtf8("yAutoRadio")) - self.gridLayout_2.addWidget(self.yAutoRadio, 1, 0, 1, 1) - self.yAutoPercentSpin = QtGui.QSpinBox(self.groupBox_2) - self.yAutoPercentSpin.setEnabled(True) - self.yAutoPercentSpin.setMinimum(1) - self.yAutoPercentSpin.setMaximum(100) - self.yAutoPercentSpin.setSingleStep(1) - self.yAutoPercentSpin.setProperty(_fromUtf8("value"), 100) - self.yAutoPercentSpin.setObjectName(_fromUtf8("yAutoPercentSpin")) - self.gridLayout_2.addWidget(self.yAutoPercentSpin, 1, 1, 1, 2) - self.yLinkCombo = QtGui.QComboBox(self.groupBox_2) - self.yLinkCombo.setObjectName(_fromUtf8("yLinkCombo")) - self.gridLayout_2.addWidget(self.yLinkCombo, 2, 1, 1, 2) - self.yMouseCheck = QtGui.QCheckBox(self.groupBox_2) - self.yMouseCheck.setChecked(True) - self.yMouseCheck.setObjectName(_fromUtf8("yMouseCheck")) - self.gridLayout_2.addWidget(self.yMouseCheck, 3, 1, 1, 1) - self.yLogCheck = QtGui.QCheckBox(self.groupBox_2) - self.yLogCheck.setObjectName(_fromUtf8("yLogCheck")) - self.gridLayout_2.addWidget(self.yLogCheck, 3, 0, 1, 1) - self.label_2 = QtGui.QLabel(self.groupBox_2) - self.label_2.setObjectName(_fromUtf8("label_2")) - self.gridLayout_2.addWidget(self.label_2, 2, 0, 1, 1) - self.verticalLayout.addWidget(self.groupBox_2) - self.tabWidget.addTab(self.tab, _fromUtf8("")) - self.tab_2 = QtGui.QWidget() - self.tab_2.setObjectName(_fromUtf8("tab_2")) - self.verticalLayout_2 = QtGui.QVBoxLayout(self.tab_2) - self.verticalLayout_2.setSpacing(0) - self.verticalLayout_2.setMargin(0) - self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2")) - self.powerSpectrumGroup = QtGui.QGroupBox(self.tab_2) - self.powerSpectrumGroup.setCheckable(True) - self.powerSpectrumGroup.setChecked(False) - self.powerSpectrumGroup.setObjectName(_fromUtf8("powerSpectrumGroup")) - self.verticalLayout_2.addWidget(self.powerSpectrumGroup) - self.decimateGroup = QtGui.QGroupBox(self.tab_2) - self.decimateGroup.setCheckable(True) - self.decimateGroup.setObjectName(_fromUtf8("decimateGroup")) - self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup) - self.gridLayout_4.setMargin(0) - self.gridLayout_4.setSpacing(0) - self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4")) - self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.manualDecimateRadio.setChecked(True) - self.manualDecimateRadio.setObjectName(_fromUtf8("manualDecimateRadio")) - self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1) - self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup) - self.downsampleSpin.setMinimum(1) - self.downsampleSpin.setMaximum(100000) - self.downsampleSpin.setProperty(_fromUtf8("value"), 1) - self.downsampleSpin.setObjectName(_fromUtf8("downsampleSpin")) - self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1) - self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup) - self.autoDecimateRadio.setChecked(False) - self.autoDecimateRadio.setObjectName(_fromUtf8("autoDecimateRadio")) - self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1) - self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck")) - self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1) - self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup) - self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin")) - self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1) - self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup) - self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck")) - self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2) - self.verticalLayout_2.addWidget(self.decimateGroup) - self.averageGroup = QtGui.QGroupBox(self.tab_2) - self.averageGroup.setCheckable(True) - self.averageGroup.setChecked(False) - self.averageGroup.setObjectName(_fromUtf8("averageGroup")) - self.gridLayout_5 = QtGui.QGridLayout(self.averageGroup) - self.gridLayout_5.setMargin(0) - self.gridLayout_5.setSpacing(0) - self.gridLayout_5.setObjectName(_fromUtf8("gridLayout_5")) - self.avgParamList = QtGui.QListWidget(self.averageGroup) - self.avgParamList.setObjectName(_fromUtf8("avgParamList")) - self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) - self.verticalLayout_2.addWidget(self.averageGroup) - self.tabWidget.addTab(self.tab_2, _fromUtf8("")) - self.tab_3 = QtGui.QWidget() - self.tab_3.setObjectName(_fromUtf8("tab_3")) - self.verticalLayout_3 = QtGui.QVBoxLayout(self.tab_3) - self.verticalLayout_3.setObjectName(_fromUtf8("verticalLayout_3")) - self.alphaGroup = QtGui.QGroupBox(self.tab_3) - self.alphaGroup.setCheckable(True) - self.alphaGroup.setObjectName(_fromUtf8("alphaGroup")) - self.horizontalLayout = QtGui.QHBoxLayout(self.alphaGroup) - self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.autoAlphaCheck = QtGui.QCheckBox(self.alphaGroup) - self.autoAlphaCheck.setChecked(False) - self.autoAlphaCheck.setObjectName(_fromUtf8("autoAlphaCheck")) - self.horizontalLayout.addWidget(self.autoAlphaCheck) - self.alphaSlider = QtGui.QSlider(self.alphaGroup) - self.alphaSlider.setMaximum(1000) - self.alphaSlider.setProperty(_fromUtf8("value"), 1000) - self.alphaSlider.setOrientation(QtCore.Qt.Horizontal) - self.alphaSlider.setObjectName(_fromUtf8("alphaSlider")) - self.horizontalLayout.addWidget(self.alphaSlider) - self.verticalLayout_3.addWidget(self.alphaGroup) - self.gridGroup = QtGui.QGroupBox(self.tab_3) - self.gridGroup.setCheckable(True) - self.gridGroup.setChecked(False) - self.gridGroup.setObjectName(_fromUtf8("gridGroup")) - self.verticalLayout_4 = QtGui.QVBoxLayout(self.gridGroup) - self.verticalLayout_4.setObjectName(_fromUtf8("verticalLayout_4")) - self.gridAlphaSlider = QtGui.QSlider(self.gridGroup) - self.gridAlphaSlider.setMaximum(255) - self.gridAlphaSlider.setProperty(_fromUtf8("value"), 70) - self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal) - self.gridAlphaSlider.setObjectName(_fromUtf8("gridAlphaSlider")) - self.verticalLayout_4.addWidget(self.gridAlphaSlider) - self.verticalLayout_3.addWidget(self.gridGroup) - self.pointsGroup = QtGui.QGroupBox(self.tab_3) - self.pointsGroup.setCheckable(True) - self.pointsGroup.setObjectName(_fromUtf8("pointsGroup")) - self.verticalLayout_5 = QtGui.QVBoxLayout(self.pointsGroup) - self.verticalLayout_5.setObjectName(_fromUtf8("verticalLayout_5")) - self.autoPointsCheck = QtGui.QCheckBox(self.pointsGroup) - self.autoPointsCheck.setChecked(True) - self.autoPointsCheck.setObjectName(_fromUtf8("autoPointsCheck")) - self.verticalLayout_5.addWidget(self.autoPointsCheck) - self.verticalLayout_3.addWidget(self.pointsGroup) - spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.verticalLayout_3.addItem(spacerItem) - self.tabWidget.addTab(self.tab_3, _fromUtf8("")) - self.tab_4 = QtGui.QWidget() - self.tab_4.setObjectName(_fromUtf8("tab_4")) - self.gridLayout_7 = QtGui.QGridLayout(self.tab_4) - self.gridLayout_7.setObjectName(_fromUtf8("gridLayout_7")) - spacerItem1 = QtGui.QSpacerItem(59, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_7.addItem(spacerItem1, 0, 0, 1, 1) - self.gridLayout_6 = QtGui.QGridLayout() - self.gridLayout_6.setObjectName(_fromUtf8("gridLayout_6")) - self.saveSvgBtn = QtGui.QPushButton(self.tab_4) - self.saveSvgBtn.setObjectName(_fromUtf8("saveSvgBtn")) - self.gridLayout_6.addWidget(self.saveSvgBtn, 0, 0, 1, 1) - self.saveImgBtn = QtGui.QPushButton(self.tab_4) - self.saveImgBtn.setObjectName(_fromUtf8("saveImgBtn")) - self.gridLayout_6.addWidget(self.saveImgBtn, 1, 0, 1, 1) - self.saveMaBtn = QtGui.QPushButton(self.tab_4) - self.saveMaBtn.setObjectName(_fromUtf8("saveMaBtn")) - self.gridLayout_6.addWidget(self.saveMaBtn, 2, 0, 1, 1) - self.saveCsvBtn = QtGui.QPushButton(self.tab_4) - self.saveCsvBtn.setObjectName(_fromUtf8("saveCsvBtn")) - self.gridLayout_6.addWidget(self.saveCsvBtn, 3, 0, 1, 1) - self.gridLayout_7.addLayout(self.gridLayout_6, 0, 1, 1, 1) - spacerItem2 = QtGui.QSpacerItem(59, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_7.addItem(spacerItem2, 0, 2, 1, 1) - spacerItem3 = QtGui.QSpacerItem(20, 211, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_7.addItem(spacerItem3, 1, 1, 1, 1) - self.tabWidget.addTab(self.tab_4, _fromUtf8("")) - self.gridLayout_3.addWidget(self.tabWidget, 0, 0, 1, 1) - - self.retranslateUi(Form) - self.tabWidget.setCurrentIndex(0) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox.setTitle(QtGui.QApplication.translate("Form", "X Axis", None, QtGui.QApplication.UnicodeUTF8)) - self.xManualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.xMinText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.xMaxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.xAutoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.xAutoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) - self.xMouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse", None, QtGui.QApplication.UnicodeUTF8)) - self.xLogCheck.setText(QtGui.QApplication.translate("Form", "Log", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("Form", "Link with:", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_2.setTitle(QtGui.QApplication.translate("Form", "Y Axis", None, QtGui.QApplication.UnicodeUTF8)) - self.yManualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.yMinText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.yMaxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.yAutoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.yAutoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) - self.yMouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse", None, QtGui.QApplication.UnicodeUTF8)) - self.yLogCheck.setText(QtGui.QApplication.translate("Form", "Log", None, QtGui.QApplication.UnicodeUTF8)) - self.label_2.setText(QtGui.QApplication.translate("Form", "Link with:", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QtGui.QApplication.translate("Form", "Scale", None, QtGui.QApplication.UnicodeUTF8)) - self.powerSpectrumGroup.setTitle(QtGui.QApplication.translate("Form", "Power Spectrum", None, QtGui.QApplication.UnicodeUTF8)) - self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8)) - self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8)) - self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8)) - self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8)) - self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8)) - self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8)) - self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QtGui.QApplication.translate("Form", "Data", None, QtGui.QApplication.UnicodeUTF8)) - self.alphaGroup.setTitle(QtGui.QApplication.translate("Form", "Alpha", None, QtGui.QApplication.UnicodeUTF8)) - self.autoAlphaCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.gridGroup.setTitle(QtGui.QApplication.translate("Form", "Grid", None, QtGui.QApplication.UnicodeUTF8)) - self.pointsGroup.setTitle(QtGui.QApplication.translate("Form", "Points", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPointsCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), QtGui.QApplication.translate("Form", "Display", None, QtGui.QApplication.UnicodeUTF8)) - self.saveSvgBtn.setText(QtGui.QApplication.translate("Form", "SVG", None, QtGui.QApplication.UnicodeUTF8)) - self.saveImgBtn.setText(QtGui.QApplication.translate("Form", "Image", None, QtGui.QApplication.UnicodeUTF8)) - self.saveMaBtn.setText(QtGui.QApplication.translate("Form", "MetaArray", None, QtGui.QApplication.UnicodeUTF8)) - self.saveCsvBtn.setText(QtGui.QApplication.translate("Form", "CSV", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), QtGui.QApplication.translate("Form", "Save", None, QtGui.QApplication.UnicodeUTF8)) - diff --git a/plotConfigTemplate.ui b/plotConfigTemplate.ui deleted file mode 100644 index 7baeb337..00000000 --- a/plotConfigTemplate.ui +++ /dev/null @@ -1,563 +0,0 @@ - - - Form - - - - 0 - 0 - 250 - 340 - - - - - 250 - 350 - - - - Form - - - - 0 - - - 0 - - - - - - 16777215 - 16777215 - - - - 0 - - - - Scale - - - - 0 - - - 0 - - - - - X Axis - - - - 0 - - - 0 - - - - - Manual - - - - - - - 0 - - - - - - - 0 - - - - - - - Auto - - - true - - - - - - - true - - - % - - - 1 - - - 100 - - - 1 - - - 100 - - - - - - - - - - Mouse - - - true - - - - - - - Log - - - - - - - Link with: - - - - - - - - - - Y Axis - - - - 0 - - - 0 - - - - - Manual - - - - - - - 0 - - - - - - - 0 - - - - - - - Auto - - - true - - - - - - - true - - - % - - - 1 - - - 100 - - - 1 - - - 100 - - - - - - - - - - Mouse - - - true - - - - - - - Log - - - - - - - Link with: - - - - - - - - - - - Data - - - - 0 - - - 0 - - - - - Power Spectrum - - - true - - - false - - - - - - - Downsample - - - true - - - - 0 - - - 0 - - - - - Manual - - - true - - - - - - - 1 - - - 100000 - - - 1 - - - - - - - Auto - - - false - - - - - - - If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed. - - - Max Traces: - - - - - - - If multiple curves are displayed in this plot, check "Max Traces" and set this value to limit the number of traces that are displayed. - - - - - - - If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden). - - - Forget hidden traces - - - - - - - - - - Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available). - - - Average - - - true - - - false - - - - 0 - - - 0 - - - - - - - - - - - - Display - - - - - - Alpha - - - true - - - - - - Auto - - - false - - - - - - - 1000 - - - 1000 - - - Qt::Horizontal - - - - - - - - - - Grid - - - true - - - false - - - - - - 255 - - - 70 - - - Qt::Horizontal - - - - - - - - - - Points - - - true - - - - - - Auto - - - true - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - Save - - - - - - Qt::Horizontal - - - - 59 - 20 - - - - - - - - - - SVG - - - - - - - Image - - - - - - - MetaArray - - - - - - - CSV - - - - - - - - - Qt::Horizontal - - - - 59 - 20 - - - - - - - - Qt::Vertical - - - - 20 - 211 - - - - - - - - - - - - - diff --git a/ptime.pyc b/ptime.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3761c6f2d6f56dfcf0adb2d955187786880931e3 GIT binary patch literal 1273 zcmcJP&u-H|5XRR|<2I)Jk&uAn99@aVE$R^=P(h*+sZf$bl}og-C-FA+uDiQ#8m{no zJO*#T1Hg=F6LAIAvd7=9ch}$icI?j2hjIP!#~>58M@aX#bh$4Sav>%Z1CfWK48(-Q zA03KGhdU?THijbah;KAF68TOmc17M57>LU~v5bVR1$M+`Pkf~@ajQet7>gIQ1V&;K z3LOdDWkKtXl88Ngrx4d(RoJiBa0u|(Vy2uj8n_eYjm|u`Dw!h~cc^rZHB!(X5?7d} z%%PCi2s1>QBbO92X=&^nW^1U|zA*YAJ~j2)szu@9)zR@0z^7)3a4M@>F0?6S{9ZY4 z)vWQDLqn@u7@UpMi{oP`RfgJOA4Z6qhQIMJHx?>GZ&G@0D#gbsojX`5UqC_rq2BOGR>Z=r99*Mi zU@xI@2m`6*0&SmHNDjaoa9$FJQhG96k=YHugMQLsTf1yz?RbXolFmw#EqzRCG=4W6 zPseA24+*uzHn 0: + #c = node.child(0) + #node.removeChild(c) + #self.invisibleRootItem().addChild(c) + self.expandToDepth(3) + self.resizeColumnToContents(0) + + def buildTree(self, data, parent, name='', hideRoot=False): + if hideRoot: + node = parent + else: + typeStr = type(data).__name__ + if typeStr == 'instance': + typeStr += ": " + data.__class__.__name__ + node = QtGui.QTreeWidgetItem([name, typeStr, ""]) + parent.addChild(node) + + if isinstance(data, types.TracebackType): ## convert traceback to a list of strings + data = map(str.strip, traceback.format_list(traceback.extract_tb(data))) + elif HAVE_METAARRAY and isinstance(data, metaarray.MetaArray): + data = { + 'data': data.view(np.ndarray), + 'meta': data.infoCopy() + } + + if isinstance(data, dict): + for k in data: + self.buildTree(data[k], node, str(k)) + elif isinstance(data, list) or isinstance(data, tuple): + for i in range(len(data)): + self.buildTree(data[i], node, str(i)) + else: + node.setText(2, str(data)) + + + #def mkNode(self, name, v): + #if type(v) is list and len(v) > 0 and isinstance(v[0], dict): + #inds = map(unicode, range(len(v))) + #v = OrderedDict(zip(inds, v)) + #if isinstance(v, dict): + ##print "\nadd tree", k, v + #node = QtGui.QTreeWidgetItem([name]) + #for k in v: + #newNode = self.mkNode(k, v[k]) + #node.addChild(newNode) + #else: + ##print "\nadd value", k, str(v) + #node = QtGui.QTreeWidgetItem([unicode(name), unicode(v)]) + #return node + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + d = { + 'list1': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], + 'dict1': { + 'x': 1, + 'y': 2, + 'z': 'three' + }, + 'array1 (20x20)': np.ones((10,10)) + } + + tree = DataTreeWidget(data=d) + tree.show() + tree.resize(600,600) + + + ## Start Qt event loop unless running in interactive mode. + import sys + if sys.flags.interactive != 1: + app.exec_() + \ No newline at end of file diff --git a/widgets/FileDialog.py b/widgets/FileDialog.py new file mode 100644 index 00000000..33b838a2 --- /dev/null +++ b/widgets/FileDialog.py @@ -0,0 +1,14 @@ +from pyqtgraph.Qt import QtGui, QtCore +import sys + +__all__ = ['FileDialog'] + +class FileDialog(QtGui.QFileDialog): + ## Compatibility fix for OSX: + ## For some reason the native dialog doesn't show up when you set AcceptMode to AcceptSave on OS X, so we don't use the native dialog + + def __init__(self, *args): + QtGui.QFileDialog.__init__(self, *args) + + if sys.platform == 'darwin': + self.setOption(QtGui.QFileDialog.DontUseNativeDialog) \ No newline at end of file diff --git a/widgets/GradientWidget.py b/widgets/GradientWidget.py new file mode 100644 index 00000000..47d6ab45 --- /dev/null +++ b/widgets/GradientWidget.py @@ -0,0 +1,620 @@ +# -*- coding: utf-8 -*- +if __name__ == '__main__': + import os, sys + path = os.path.join(os.path.dirname(__file__), '..', '..') + sys.path = [path] + sys.path + + +from pyqtgraph.Qt import QtGui, QtCore +from GraphicsView import GraphicsView +from pyqtgraph.graphicsItems.GradientEditorItem import GradientEditorItem +import weakref +import numpy as np +import collections + +__all__ = ['TickSlider', 'GradientWidget', 'BlackWhiteSlider'] + + +class GradientWidget(GraphicsView): + + sigGradientChanged = QtCore.Signal(object) + + def __init__(self, parent=None, orientation='bottom', *args, **kargs): + GraphicsView.__init__(self, parent, useOpenGL=False, background=None) + self.maxDim = 27 + kargs['tickPen'] = 'k' + self.item = GradientEditorItem(*args, **kargs) + self.item.sigGradientChanged.connect(self.sigGradientChanged) + self.setCentralItem(self.item) + self.setOrientation(orientation) + self.setCacheMode(self.CacheNone) + self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) + self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) + self.setBackgroundRole(QtGui.QPalette.NoRole) + #self.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.NoBrush)) + #self.setAutoFillBackground(False) + #self.setAttribute(QtCore.Qt.WA_PaintOnScreen, False) + #self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent, True) + + def setOrientation(self, ort): + self.item.setOrientation(ort) + self.orientation = ort + self.setMaxDim() + + def setMaxDim(self, mx=None): + if mx is None: + mx = self.maxDim + else: + self.maxDim = mx + + if self.orientation in ['bottom', 'top']: + self.setFixedHeight(mx) + self.setMaximumWidth(16777215) + else: + self.setFixedWidth(mx) + self.setMaximumHeight(16777215) + + def __getattr__(self, attr): + return getattr(self.item, attr) + + + +#Gradients = collections.OrderedDict([ + #('thermal', {'ticks': [(0.3333, (185, 0, 0, 255)), (0.6666, (255, 220, 0, 255)), (1, (255, 255, 255, 255)), (0, (0, 0, 0, 255))], 'mode': 'rgb'}), + #('flame', {'ticks': [(0.2, (7, 0, 220, 255)), (0.5, (236, 0, 134, 255)), (0.8, (246, 246, 0, 255)), (1.0, (255, 255, 255, 255)), (0.0, (0, 0, 0, 255))], 'mode': 'rgb'}), + #('yellowy', {'ticks': [(0.0, (0, 0, 0, 255)), (0.2328863796753704, (32, 0, 129, 255)), (0.8362738179251941, (255, 255, 0, 255)), (0.5257586450247, (115, 15, 255, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'} ), + #('bipolar', {'ticks': [(0.0, (0, 255, 255, 255)), (1.0, (255, 255, 0, 255)), (0.5, (0, 0, 0, 255)), (0.25, (0, 0, 255, 255)), (0.75, (255, 0, 0, 255))], 'mode': 'rgb'}), + #('spectrum', {'ticks': [(1.0, (255, 0, 255, 255)), (0.0, (255, 0, 0, 255))], 'mode': 'hsv'}), + #('cyclic', {'ticks': [(0.0, (255, 0, 4, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'hsv'}), + #('greyclip', {'ticks': [(0.0, (0, 0, 0, 255)), (0.99, (255, 255, 255, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'rgb'}), +#]) + + +#class TickSlider(GraphicsView): + #def __init__(self, parent=None, orientation='bottom', allowAdd=True, **kargs): + #self.orientation = orientation + #self.length = 100 + #self.tickSize = 15 + #self.ticks = {} + #self.maxDim = 20 + #GraphicsView.__init__(self, parent, useOpenGL=False) + #self.allowAdd = allowAdd + ##self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + ##self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + ##self.setTransformationAnchor(QtGui.QGraphicsView.NoAnchor) + ##self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter) + #self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing) + #self.orientations = { + #'left': (270, 1, -1), + #'right': (270, 1, 1), + #'top': (0, 1, -1), + #'bottom': (0, 1, 1) + #} + + ##self.scene = QtGui.QGraphicsScene() + ##self.setScene(self.scene) + + #self.setOrientation(orientation) + #self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) + #self.setBackgroundRole(QtGui.QPalette.NoRole) + #self.setMouseTracking(True) + + + #def keyPressEvent(self, ev): + #ev.ignore() + + #def setMaxDim(self, mx=None): + #if mx is None: + #mx = self.maxDim + #else: + #self.maxDim = mx + + #if self.orientation in ['bottom', 'top']: + #self.setFixedHeight(mx) + #self.setMaximumWidth(16777215) + #else: + #self.setFixedWidth(mx) + #self.setMaximumHeight(16777215) + + #def setOrientation(self, ort): + #self.orientation = ort + #self.resetTransform() + #self.rotate(self.orientations[ort][0]) + #self.scale(*self.orientations[ort][1:]) + #self.setMaxDim() + + #def addTick(self, x, color=None, movable=True): + #if color is None: + #color = QtGui.QColor(255,255,255) + #tick = Tick(self, [x*self.length, 0], color, movable, self.tickSize) + #self.ticks[tick] = x + #self.scene.addItem(tick) + #return tick + + #def removeTick(self, tick): + #del self.ticks[tick] + #self.scene.removeItem(tick) + + #def tickMoved(self, tick, pos): + ##print "tick changed" + ### Correct position of tick if it has left bounds. + #newX = min(max(0, pos.x()), self.length) + #pos.setX(newX) + #tick.setPos(pos) + #self.ticks[tick] = float(newX) / self.length + + #def tickClicked(self, tick, ev): + #if ev.button() == QtCore.Qt.RightButton: + #self.removeTick(tick) + + #def widgetLength(self): + #if self.orientation in ['bottom', 'top']: + #return self.width() + #else: + #return self.height() + + #def resizeEvent(self, ev): + #wlen = max(40, self.widgetLength()) + #self.setLength(wlen-self.tickSize) + #bounds = self.scene().itemsBoundingRect() + #bounds.setLeft(min(-self.tickSize*0.5, bounds.left())) + #bounds.setRight(max(self.length + self.tickSize, bounds.right())) + ##bounds.setTop(min(bounds.top(), self.tickSize)) + ##bounds.setBottom(max(0, bounds.bottom())) + #self.setSceneRect(bounds) + #self.fitInView(bounds, QtCore.Qt.KeepAspectRatio) + + #def setLength(self, newLen): + #for t, x in self.ticks.items(): + #t.setPos(x * newLen, t.pos().y()) + #self.length = float(newLen) + + #def mousePressEvent(self, ev): + #QtGui.QGraphicsView.mousePressEvent(self, ev) + #self.ignoreRelease = False + #for i in self.items(ev.pos()): + #if isinstance(i, Tick): + #self.ignoreRelease = True + #break + ##if len(self.items(ev.pos())) > 0: ## Let items handle their own clicks + ##self.ignoreRelease = True + + #def mouseReleaseEvent(self, ev): + #QtGui.QGraphicsView.mouseReleaseEvent(self, ev) + #if self.ignoreRelease: + #return + + #pos = self.mapToScene(ev.pos()) + + #if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: + #if pos.x() < 0 or pos.x() > self.length: + #return + #if pos.y() < 0 or pos.y() > self.tickSize: + #return + #pos.setX(min(max(pos.x(), 0), self.length)) + #self.addTick(pos.x()/self.length) + #elif ev.button() == QtCore.Qt.RightButton: + #self.showMenu(ev) + + + #def showMenu(self, ev): + #pass + + #def setTickColor(self, tick, color): + #tick = self.getTick(tick) + #tick.color = color + #tick.setBrush(QtGui.QBrush(QtGui.QColor(tick.color))) + + #def setTickValue(self, tick, val): + #tick = self.getTick(tick) + #val = min(max(0.0, val), 1.0) + #x = val * self.length + #pos = tick.pos() + #pos.setX(x) + #tick.setPos(pos) + #self.ticks[tick] = val + + #def tickValue(self, tick): + #tick = self.getTick(tick) + #return self.ticks[tick] + + #def getTick(self, tick): + #if type(tick) is int: + #tick = self.listTicks()[tick][0] + #return tick + + #def mouseMoveEvent(self, ev): + #QtGui.QGraphicsView.mouseMoveEvent(self, ev) + ##print ev.pos(), ev.buttons() + + #def listTicks(self): + #ticks = self.ticks.items() + #ticks.sort(lambda a,b: cmp(a[1], b[1])) + #return ticks + + +#class GradientWidget(TickSlider): + + #sigGradientChanged = QtCore.Signal(object) + + #def __init__(self, *args, **kargs): + #self.currentTick = None + #self.currentTickColor = None + #self.rectSize = 15 + #self.gradRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize)) + #self.backgroundRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize)) + #self.backgroundRect.setBrush(QtGui.QBrush(QtCore.Qt.DiagCrossPattern)) + #self.colorMode = 'rgb' + #TickSlider.__init__(self, *args, **kargs) + #self.colorDialog = QtGui.QColorDialog() + #self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) + #self.colorDialog.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) + + #self.colorDialog.currentColorChanged.connect(self.currentColorChanged) + #self.colorDialog.rejected.connect(self.currentColorRejected) + + ##self.gradient = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(100,0)) + #self.scene.addItem(self.backgroundRect) + #self.scene.addItem(self.gradRect) + + #self.setMaxDim(self.rectSize + self.tickSize) + + ##self.btn = QtGui.QPushButton('RGB') + ##self.btnProxy = self.scene.addWidget(self.btn) + ##self.btnProxy.setFlag(self.btnProxy.ItemIgnoresTransformations) + ##self.btnProxy.scale(0.7, 0.7) + ##self.btnProxy.translate(-self.btnProxy.sceneBoundingRect().width()+self.tickSize/2., 0) + ##if self.orientation == 'bottom': + ##self.btnProxy.translate(0, -self.rectSize) + #self.rgbAction = QtGui.QAction('RGB', self) + #self.rgbAction.setCheckable(True) + #self.rgbAction.triggered.connect(lambda: self.setColorMode('rgb')) + #self.hsvAction = QtGui.QAction('HSV', self) + #self.hsvAction.setCheckable(True) + #self.hsvAction.triggered.connect(lambda: self.setColorMode('hsv')) + + #self.menu = QtGui.QMenu() + + ### build context menu of gradients + #global Gradients + #for g in Gradients: + #px = QtGui.QPixmap(100, 15) + #p = QtGui.QPainter(px) + #self.restoreState(Gradients[g]) + #grad = self.getGradient() + #brush = QtGui.QBrush(grad) + #p.fillRect(QtCore.QRect(0, 0, 100, 15), brush) + #p.end() + #label = QtGui.QLabel() + #label.setPixmap(px) + #label.setContentsMargins(1, 1, 1, 1) + #act = QtGui.QWidgetAction(self) + #act.setDefaultWidget(label) + #act.triggered.connect(self.contextMenuClicked) + #act.name = g + #self.menu.addAction(act) + + #self.menu.addSeparator() + #self.menu.addAction(self.rgbAction) + #self.menu.addAction(self.hsvAction) + + + #for t in self.ticks.keys(): + #self.removeTick(t) + #self.addTick(0, QtGui.QColor(0,0,0), True) + #self.addTick(1, QtGui.QColor(255,0,0), True) + #self.setColorMode('rgb') + #self.updateGradient() + + #def showMenu(self, ev): + #self.menu.popup(ev.globalPos()) + + #def contextMenuClicked(self, b): + #global Gradients + #act = self.sender() + #self.restoreState(Gradients[act.name]) + + #def setColorMode(self, cm): + #if cm not in ['rgb', 'hsv']: + #raise Exception("Unknown color mode %s. Options are 'rgb' and 'hsv'." % str(cm)) + + #try: + #self.rgbAction.blockSignals(True) + #self.hsvAction.blockSignals(True) + #self.rgbAction.setChecked(cm == 'rgb') + #self.hsvAction.setChecked(cm == 'hsv') + #finally: + #self.rgbAction.blockSignals(False) + #self.hsvAction.blockSignals(False) + #self.colorMode = cm + #self.updateGradient() + + #def updateGradient(self): + #self.gradient = self.getGradient() + #self.gradRect.setBrush(QtGui.QBrush(self.gradient)) + #self.sigGradientChanged.emit(self) + + #def setLength(self, newLen): + #TickSlider.setLength(self, newLen) + #self.backgroundRect.setRect(0, -self.rectSize, newLen, self.rectSize) + #self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize) + #self.updateGradient() + + #def currentColorChanged(self, color): + #if color.isValid() and self.currentTick is not None: + #self.setTickColor(self.currentTick, color) + #self.updateGradient() + + #def currentColorRejected(self): + #self.setTickColor(self.currentTick, self.currentTickColor) + #self.updateGradient() + + #def tickClicked(self, tick, ev): + #if ev.button() == QtCore.Qt.LeftButton: + #if not tick.colorChangeAllowed: + #return + #self.currentTick = tick + #self.currentTickColor = tick.color + #self.colorDialog.setCurrentColor(tick.color) + #self.colorDialog.open() + ##color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) + ##if color.isValid(): + ##self.setTickColor(tick, color) + ##self.updateGradient() + #elif ev.button() == QtCore.Qt.RightButton: + #if not tick.removeAllowed: + #return + #if len(self.ticks) > 2: + #self.removeTick(tick) + #self.updateGradient() + + #def tickMoved(self, tick, pos): + #TickSlider.tickMoved(self, tick, pos) + #self.updateGradient() + + + #def getGradient(self): + #g = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(self.length,0)) + #if self.colorMode == 'rgb': + #ticks = self.listTicks() + #g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks]) + #elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop + #ticks = self.listTicks() + #stops = [] + #stops.append((ticks[0][1], ticks[0][0].color)) + #for i in range(1,len(ticks)): + #x1 = ticks[i-1][1] + #x2 = ticks[i][1] + #dx = (x2-x1) / 10. + #for j in range(1,10): + #x = x1 + dx*j + #stops.append((x, self.getColor(x))) + #stops.append((x2, self.getColor(x2))) + #g.setStops(stops) + #return g + + #def getColor(self, x, toQColor=True): + #ticks = self.listTicks() + #if x <= ticks[0][1]: + #c = ticks[0][0].color + #if toQColor: + #return QtGui.QColor(c) # always copy colors before handing them out + #else: + #return (c.red(), c.green(), c.blue(), c.alpha()) + #if x >= ticks[-1][1]: + #c = ticks[-1][0].color + #if toQColor: + #return QtGui.QColor(c) # always copy colors before handing them out + #else: + #return (c.red(), c.green(), c.blue(), c.alpha()) + + #x2 = ticks[0][1] + #for i in range(1,len(ticks)): + #x1 = x2 + #x2 = ticks[i][1] + #if x1 <= x and x2 >= x: + #break + + #dx = (x2-x1) + #if dx == 0: + #f = 0. + #else: + #f = (x-x1) / dx + #c1 = ticks[i-1][0].color + #c2 = ticks[i][0].color + #if self.colorMode == 'rgb': + #r = c1.red() * (1.-f) + c2.red() * f + #g = c1.green() * (1.-f) + c2.green() * f + #b = c1.blue() * (1.-f) + c2.blue() * f + #a = c1.alpha() * (1.-f) + c2.alpha() * f + #if toQColor: + #return QtGui.QColor(r, g, b,a) + #else: + #return (r,g,b,a) + #elif self.colorMode == 'hsv': + #h1,s1,v1,_ = c1.getHsv() + #h2,s2,v2,_ = c2.getHsv() + #h = h1 * (1.-f) + h2 * f + #s = s1 * (1.-f) + s2 * f + #v = v1 * (1.-f) + v2 * f + #c = QtGui.QColor() + #c.setHsv(h,s,v) + #if toQColor: + #return c + #else: + #return (c.red(), c.green(), c.blue(), c.alpha()) + + #def getLookupTable(self, nPts, alpha=True): + #"""Return an RGB/A lookup table.""" + #if alpha: + #table = np.empty((nPts,4), dtype=np.ubyte) + #else: + #table = np.empty((nPts,3), dtype=np.ubyte) + + #for i in range(nPts): + #x = float(i)/(nPts-1) + #color = self.getColor(x, toQColor=False) + #table[i] = color[:table.shape[1]] + + #return table + + + + #def mouseReleaseEvent(self, ev): + #TickSlider.mouseReleaseEvent(self, ev) + #self.updateGradient() + + #def addTick(self, x, color=None, movable=True): + #if color is None: + #color = self.getColor(x) + #t = TickSlider.addTick(self, x, color=color, movable=movable) + #t.colorChangeAllowed = True + #t.removeAllowed = True + #return t + + #def saveState(self): + #ticks = [] + #for t in self.ticks: + #c = t.color + #ticks.append((self.ticks[t], (c.red(), c.green(), c.blue(), c.alpha()))) + #state = {'mode': self.colorMode, 'ticks': ticks} + #return state + + #def restoreState(self, state): + #self.setColorMode(state['mode']) + #for t in self.ticks.keys(): + #self.removeTick(t) + #for t in state['ticks']: + #c = QtGui.QColor(*t[1]) + #self.addTick(t[0], c) + #self.updateGradient() + + + +#class BlackWhiteSlider(GradientWidget): + #def __init__(self, parent): + #GradientWidget.__init__(self, parent) + #self.getTick(0).colorChangeAllowed = False + #self.getTick(1).colorChangeAllowed = False + #self.allowAdd = False + #self.setTickColor(self.getTick(1), QtGui.QColor(255,255,255)) + #self.setOrientation('right') + + #def getLevels(self): + #return (self.tickValue(0), self.tickValue(1)) + + #def setLevels(self, black, white): + #self.setTickValue(0, black) + #self.setTickValue(1, white) + + + + +#class GammaWidget(TickSlider): + #pass + + +#class Tick(QtGui.QGraphicsPolygonItem): + #def __init__(self, view, pos, color, movable=True, scale=10): + ##QObjectWorkaround.__init__(self) + #self.movable = movable + #self.view = weakref.ref(view) + #self.scale = scale + #self.color = color + ##self.endTick = endTick + #self.pg = QtGui.QPolygonF([QtCore.QPointF(0,0), QtCore.QPointF(-scale/3**0.5,scale), QtCore.QPointF(scale/3**0.5,scale)]) + #QtGui.QGraphicsPolygonItem.__init__(self, self.pg) + #self.setPos(pos[0], pos[1]) + #self.setFlags(QtGui.QGraphicsItem.ItemIsMovable | QtGui.QGraphicsItem.ItemIsSelectable) + #self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) + #if self.movable: + #self.setZValue(1) + #else: + #self.setZValue(0) + + ##def x(self): + ##return self.pos().x()/100. + + #def mouseMoveEvent(self, ev): + ##print self, "move", ev.scenePos() + #if not self.movable: + #return + #if not ev.buttons() & QtCore.Qt.LeftButton: + #return + + + #newPos = ev.scenePos() + self.mouseOffset + #newPos.setY(self.pos().y()) + ##newPos.setX(min(max(newPos.x(), 0), 100)) + #self.setPos(newPos) + #self.view().tickMoved(self, newPos) + #self.movedSincePress = True + ##self.emit(QtCore.SIGNAL('tickChanged'), self) + #ev.accept() + + #def mousePressEvent(self, ev): + #self.movedSincePress = False + #if ev.button() == QtCore.Qt.LeftButton: + #ev.accept() + #self.mouseOffset = self.pos() - ev.scenePos() + #self.pressPos = ev.scenePos() + #elif ev.button() == QtCore.Qt.RightButton: + #ev.accept() + ##if self.endTick: + ##return + ##self.view.tickChanged(self, delete=True) + + #def mouseReleaseEvent(self, ev): + ##print self, "release", ev.scenePos() + #if not self.movedSincePress: + #self.view().tickClicked(self, ev) + + ##if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: + ##color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) + ##if color.isValid(): + ##self.color = color + ##self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) + ###self.emit(QtCore.SIGNAL('tickChanged'), self) + ##self.view.tickChanged(self) + + + + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + w = QtGui.QMainWindow() + w.show() + w.resize(400,400) + cw = QtGui.QWidget() + w.setCentralWidget(cw) + + l = QtGui.QGridLayout() + l.setSpacing(0) + cw.setLayout(l) + + w1 = GradientWidget(orientation='top') + w2 = GradientWidget(orientation='right', allowAdd=False) + #w2.setTickColor(1, QtGui.QColor(255,255,255)) + w3 = GradientWidget(orientation='bottom') + w4 = GradientWidget(orientation='left') + label = QtGui.QLabel(""" + - Click a triangle to change its color + - Drag triangles to move + - Click in an empty area to add a new color + (adding is disabled for the right-side widget) + - Right click a triangle to remove + """) + + l.addWidget(w1, 0, 1) + l.addWidget(w2, 1, 2) + l.addWidget(w3, 2, 1) + l.addWidget(w4, 1, 0) + l.addWidget(label, 1, 1) + + ## Start Qt event loop unless running in interactive mode. + import sys + if sys.flags.interactive != 1: + app.exec_() + + \ No newline at end of file diff --git a/widgets/GraphicsLayoutWidget.py b/widgets/GraphicsLayoutWidget.py new file mode 100644 index 00000000..937c880a --- /dev/null +++ b/widgets/GraphicsLayoutWidget.py @@ -0,0 +1,12 @@ +from pyqtgraph.Qt import QtGui +from pyqtgraph.graphicsItems.GraphicsLayout import GraphicsLayout +from GraphicsView import GraphicsView + +__all__ = ['GraphicsLayoutWidget'] +class GraphicsLayoutWidget(GraphicsView): + def __init__(self, parent=None, **kargs): + GraphicsView.__init__(self, parent) + self.ci = GraphicsLayout(**kargs) + for n in ['nextRow', 'nextCol', 'addPlot', 'addViewBox', 'addItem', 'getItem']: + setattr(self, n, getattr(self.ci, n)) + self.setCentralItem(self.ci) diff --git a/GraphicsView.py b/widgets/GraphicsView.py similarity index 81% rename from GraphicsView.py rename to widgets/GraphicsView.py index 9846d0c4..899db420 100644 --- a/GraphicsView.py +++ b/widgets/GraphicsView.py @@ -5,23 +5,30 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from PyQt4 import QtCore, QtGui, QtOpenGL, QtSvg +from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL, QtSvg #from numpy import vstack #import time -from Point import * +from pyqtgraph.Point import Point #from vector import * import sys, os import debug - +from FileDialog import FileDialog +from pyqtgraph.GraphicsScene import GraphicsScene +import numpy as np +import pyqtgraph.functions as fn + +__all__ = ['GraphicsView'] + class GraphicsView(QtGui.QGraphicsView): sigRangeChanged = QtCore.Signal(object, object) sigMouseReleased = QtCore.Signal(object) sigSceneMouseMoved = QtCore.Signal(object) #sigRegionChanged = QtCore.Signal(object) + sigScaleChanged = QtCore.Signal(object) lastFileDir = None - def __init__(self, parent=None, useOpenGL=False): + def __init__(self, parent=None, useOpenGL=None, background='k'): """Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the viewed coordinate range. Also automatically creates a QGraphicsScene and a central QGraphicsWidget that is automatically scaled to the full view geometry. @@ -35,14 +42,23 @@ class GraphicsView(QtGui.QGraphicsView): self.closed = False QtGui.QGraphicsView.__init__(self, parent) - if 'linux' in sys.platform: ## linux has bugs in opengl implementation - useOpenGL = False + + ## in general openGL is poorly supported in Qt. + ## we only enable it where the performance benefit is critical. + if useOpenGL is None: + if 'linux' in sys.platform: ## linux has numerous bugs in opengl implementation + useOpenGL = False + elif 'darwin' in sys.platform: ## openGL greatly speeds up display on mac + useOpenGL = True + else: + useOpenGL = False self.useOpenGL(useOpenGL) self.setCacheMode(self.CacheBackground) - brush = QtGui.QBrush(QtGui.QColor(0,0,0)) - self.setBackgroundBrush(brush) + if background is not None: + brush = fn.mkBrush(background) + self.setBackgroundBrush(brush) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setFrameShape(QtGui.QFrame.NoFrame) @@ -57,7 +73,7 @@ class GraphicsView(QtGui.QGraphicsView): self.lockedViewports = [] self.lastMousePos = None - #self.setMouseTracking(False) + self.setMouseTracking(True) self.aspectLocked = False #self.yInverted = True self.range = QtCore.QRectF(0, 0, 1, 1) @@ -65,7 +81,7 @@ class GraphicsView(QtGui.QGraphicsView): self.currentItem = None self.clearMouse() self.updateMatrix() - self.sceneObj = QtGui.QGraphicsScene() + self.sceneObj = GraphicsScene() self.setScene(self.sceneObj) ## by default we set up a central widget with a grid layout. @@ -103,9 +119,15 @@ class GraphicsView(QtGui.QGraphicsView): self.setViewport(v) def keyPressEvent(self, ev): - ev.ignore() + #QtGui.QGraphicsView.keyPressEvent(self, ev) + self.scene().keyPressEvent(ev) ## bypass view, hand event directly to scene + ## (view likes to eat arrow key events) + def setCentralItem(self, item): + return self.setCentralWidget(item) + + def setCentralWidget(self, item): """Sets a QGraphicsWidget to automatically fill the entire view.""" if self.centralWidget is not None: self.scene().removeItem(self.centralWidget) @@ -142,49 +164,22 @@ class GraphicsView(QtGui.QGraphicsView): else: self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) - ##print "udpateMatrix:" - #translate = Point(self.range.center()) - #if self.range.width() == 0 or self.range.height() == 0: - #return - #scale = Point(self.size().width()/self.range.width(), self.size().height()/self.range.height()) - - #m = QtGui.QTransform() - - ### First center the viewport at 0 - #self.resetMatrix() - #center = self.viewportTransform().inverted()[0].map(Point(self.width()/2., self.height()/2.)) - #if self.yInverted: - #m.translate(center.x(), center.y()) - ##print " inverted; translate", center.x(), center.y() - #else: - #m.translate(center.x(), -center.y()) - ##print " not inverted; translate", center.x(), -center.y() - - ### Now scale and translate properly - #if self.aspectLocked: - #scale = Point(scale.min()) - #if not self.yInverted: - #scale = scale * Point(1, -1) - #m.scale(scale[0], scale[1]) - ##print " scale:", scale - #st = translate - #m.translate(-st[0], -st[1]) - ##print " translate:", st - #self.setTransform(m) - #self.currentScale = scale - ##self.emit(QtCore.SIGNAL('viewChanged'), self.range) self.sigRangeChanged.emit(self, self.range) if propagate: for v in self.lockedViewports: v.setXRange(self.range, padding=0) - def visibleRange(self): + def viewRect(self): """Return the boundaries of the view in scene coordinates""" ## easier to just return self.range ? r = QtCore.QRectF(self.rect()) return self.viewportTransform().inverted()[0].mapRect(r) + def visibleRange(self): + ## for backward compatibility + return self.viewRect() + def translate(self, dx, dy): self.range.adjust(dx, dy, dx, dy) self.updateMatrix() @@ -210,6 +205,7 @@ class GraphicsView(QtGui.QGraphicsView): self.updateMatrix() + self.sigScaleChanged.emit(self) def setRange(self, newRect=None, padding=0.05, lockAspect=None, propagate=True, disableAutoPixel=True): if disableAutoPixel: @@ -217,23 +213,36 @@ class GraphicsView(QtGui.QGraphicsView): if newRect is None: newRect = self.visibleRange() padding = 0 + padding = Point(padding) newRect = QtCore.QRectF(newRect) pw = newRect.width() * padding[0] ph = newRect.height() * padding[1] - self.range = newRect.adjusted(-pw, -ph, pw, ph) + newRect = newRect.adjusted(-pw, -ph, pw, ph) + scaleChanged = False + if self.range.width() != newRect.width() or self.range.height() != newRect.height(): + scaleChanged = True + self.range = newRect #print "New Range:", self.range self.centralWidget.setGeometry(self.range) self.updateMatrix(propagate) + if scaleChanged: + self.sigScaleChanged.emit(self) def scaleToImage(self, image): """Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase.""" pxSize = image.pixelSize() + image.setPxMode(True) + try: + self.sigScaleChanged.disconnect(image.setScaledMode) + except TypeError: + pass tl = image.sceneBoundingRect().topLeft() w = self.size().width() * pxSize[0] h = self.size().height() * pxSize[1] range = QtCore.QRectF(tl.x(), tl.y(), w, h) self.setRange(range, padding=0) + self.sigScaleChanged.connect(image.setScaledMode) @@ -294,6 +303,9 @@ class GraphicsView(QtGui.QGraphicsView): #ev1.setButtonDownScreenPos(fev.screenPos()) #return ev1 + def leaveEvent(self, ev): + self.scene().leaveEvent(ev) ## inform scene when mouse leaves + def mousePressEvent(self, ev): QtGui.QGraphicsView.mousePressEvent(self, ev) @@ -364,7 +376,7 @@ class GraphicsView(QtGui.QGraphicsView): return if ev.buttons() == QtCore.Qt.RightButton: - delta = Point(clip(delta[0], -50, 50), clip(-delta[1], -50, 50)) + delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50)) scale = 1.01 ** delta #if self.yInverted: #scale[0] = 1. / scale[0] @@ -401,7 +413,7 @@ class GraphicsView(QtGui.QGraphicsView): def writeSvg(self, fileName=None): if fileName is None: - self.fileDialog = QtGui.QFileDialog() + self.fileDialog = FileDialog() self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) if GraphicsView.lastFileDir is not None: @@ -420,13 +432,13 @@ class GraphicsView(QtGui.QGraphicsView): def writeImage(self, fileName=None): if fileName is None: - self.fileDialog = QtGui.QFileDialog() + self.fileDialog = FileDialog() self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) ## this is the line that makes the fileDialog not show on mac if GraphicsView.lastFileDir is not None: self.fileDialog.setDirectory(GraphicsView.lastFileDir) self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.writePng) + self.fileDialog.fileSelected.connect(self.writeImage) return fileName = str(fileName) GraphicsView.lastFileDir = os.path.split(fileName)[0] @@ -440,7 +452,14 @@ class GraphicsView(QtGui.QGraphicsView): def writePs(self, fileName=None): if fileName is None: - fileName = str(QtGui.QFileDialog.getSaveFileName()) + self.fileDialog = FileDialog() + self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.show() + self.fileDialog.fileSelected.connect(self.writePs) + return + #if fileName is None: + # fileName = str(QtGui.QFileDialog.getSaveFileName()) printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) printer.setOutputFileName(fileName) painter = QtGui.QPainter(printer) @@ -468,54 +487,3 @@ class GraphicsView(QtGui.QGraphicsView): #return fl[-1] -#class GraphicsSceneMouseEvent(QtGui.QGraphicsSceneMouseEvent): - #"""Stand-in class for QGraphicsSceneMouseEvent""" - #def __init__(self): - #QtGui.QGraphicsSceneMouseEvent.__init__(self) - - #def setPos(self, p): - #self.vpos = p - #def setButtons(self, p): - #self.vbuttons = p - #def setButton(self, p): - #self.vbutton = p - #def setModifiers(self, p): - #self.vmodifiers = p - #def setScenePos(self, p): - #self.vscenePos = p - #def setLastPos(self, p): - #self.vlastPos = p - #def setLastScenePos(self, p): - #self.vlastScenePos = p - #def setLastScreenPos(self, p): - #self.vlastScreenPos = p - #def setButtonDownPos(self, p): - #self.vbuttonDownPos = p - #def setButtonDownScenePos(self, p): - #self.vbuttonDownScenePos = p - #def setButtonDownScreenPos(self, p): - #self.vbuttonDownScreenPos = p - - #def pos(self): - #return self.vpos - #def buttons(self): - #return self.vbuttons - #def button(self): - #return self.vbutton - #def modifiers(self): - #return self.vmodifiers - #def scenePos(self): - #return self.vscenePos - #def lastPos(self): - #return self.vlastPos - #def lastScenePos(self): - #return self.vlastScenePos - #def lastScreenPos(self): - #return self.vlastScreenPos - #def buttonDownPos(self): - #return self.vbuttonDownPos - #def buttonDownScenePos(self): - #return self.vbuttonDownScenePos - #def buttonDownScreenPos(self): - #return self.vbuttonDownScreenPos - diff --git a/widgets/HistogramLUTWidget.py b/widgets/HistogramLUTWidget.py new file mode 100644 index 00000000..f05d3f1c --- /dev/null +++ b/widgets/HistogramLUTWidget.py @@ -0,0 +1,33 @@ +""" +Widget displaying an image histogram along with gradient editor. Can be used to adjust the appearance of images. +This is a wrapper around HistogramLUTItem +""" + +from pyqtgraph.Qt import QtGui, QtCore +from GraphicsView import GraphicsView +from pyqtgraph.graphicsItems.HistogramLUTItem import HistogramLUTItem + +__all__ = ['HistogramLUTWidget'] + + +class HistogramLUTWidget(GraphicsView): + + def __init__(self, parent=None, *args, **kargs): + background = kargs.get('background', 'k') + GraphicsView.__init__(self, parent, useOpenGL=False, background=background) + self.item = HistogramLUTItem(*args, **kargs) + self.setCentralItem(self.item) + self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) + self.setMinimumWidth(92) + + + def sizeHint(self): + return QtCore.QSize(115, 200) + + + + def __getattr__(self, attr): + return getattr(self.item, attr) + + + diff --git a/widgets/JoystickButton.py b/widgets/JoystickButton.py new file mode 100644 index 00000000..5320563f --- /dev/null +++ b/widgets/JoystickButton.py @@ -0,0 +1,90 @@ +from pyqtgraph.Qt import QtGui, QtCore + + +__all__ = ['JoystickButton'] + +class JoystickButton(QtGui.QPushButton): + sigStateChanged = QtCore.Signal(object, object) + + def __init__(self, parent=None): + QtGui.QPushButton.__init__(self, parent) + self.radius = 200 + self.setCheckable(True) + self.state = None + self.setState(0,0) + + + def mousePressEvent(self, ev): + self.setChecked(True) + self.pressPos = ev.pos() + ev.accept() + + def mouseMoveEvent(self, ev): + dif = ev.pos()-self.pressPos + self.setState(dif.x(), -dif.y()) + + def mouseReleaseEvent(self, ev): + self.setChecked(False) + self.setState(0,0) + + def wheelEvent(self, ev): + ev.accept() + + + def doubleClickEvent(self, ev): + ev.accept() + + def setState(self, *xy): + xy = list(xy) + d = (xy[0]**2 + xy[1]**2)**0.5 + nxy = [0,0] + for i in [0,1]: + if xy[i] == 0: + nxy[i] = 0 + else: + nxy[i] = xy[i]/d + + if d > self.radius: + d = self.radius + d = (d/self.radius)**2 + xy = [nxy[0]*d, nxy[1]*d] + + w2 = self.width()/2. + h2 = self.height()/2 + self.spotPos = QtCore.QPoint(w2*(1+xy[0]), h2*(1-xy[1])) + self.update() + if self.state == xy: + return + self.state = xy + self.sigStateChanged.emit(self, self.state) + + def paintEvent(self, ev): + QtGui.QPushButton.paintEvent(self, ev) + p = QtGui.QPainter(self) + p.setBrush(QtGui.QBrush(QtGui.QColor(0,0,0))) + p.drawEllipse(self.spotPos.x()-3,self.spotPos.y()-3,6,6) + + def resizeEvent(self, ev): + self.setState(*self.state) + QtGui.QPushButton.resizeEvent(self, ev) + + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + w = QtGui.QMainWindow() + b = JoystickButton() + w.setCentralWidget(b) + w.show() + w.resize(100, 100) + + def fn(b, s): + print "state changed:", s + + b.sigStateChanged.connect(fn) + + ## Start Qt event loop unless running in interactive mode. + import sys + if sys.flags.interactive != 1: + app.exec_() + \ No newline at end of file diff --git a/MultiPlotWidget.py b/widgets/MultiPlotWidget.py similarity index 88% rename from MultiPlotWidget.py rename to widgets/MultiPlotWidget.py index 8071127a..00e2c2d8 100644 --- a/MultiPlotWidget.py +++ b/widgets/MultiPlotWidget.py @@ -5,16 +5,17 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from GraphicsView import * -from MultiPlotItem import * +from GraphicsView import GraphicsView +import pyqtgraph.graphicsItems.MultiPlotItem as MultiPlotItem import exceptions +__all__ = ['MultiPlotWidget'] class MultiPlotWidget(GraphicsView): """Widget implementing a graphicsView with a single PlotItem inside.""" def __init__(self, parent=None): GraphicsView.__init__(self, parent) self.enableMouse(False) - self.mPlotItem = MultiPlotItem() + self.mPlotItem = MultiPlotItem.MultiPlotItem() self.setCentralItem(self.mPlotItem) ## Explicitly wrap methods from mPlotItem #for m in ['setData']: diff --git a/PlotWidget.py b/widgets/PlotWidget.py similarity index 95% rename from PlotWidget.py rename to widgets/PlotWidget.py index 1254b963..310838c5 100644 --- a/PlotWidget.py +++ b/widgets/PlotWidget.py @@ -5,10 +5,12 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ +from pyqtgraph.Qt import QtCore, QtGui from GraphicsView import * -from PlotItem import * +from pyqtgraph.graphicsItems.PlotItem import * import exceptions +__all__ = ['PlotWidget'] class PlotWidget(GraphicsView): #sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView diff --git a/widgets/ProgressDialog.py b/widgets/ProgressDialog.py new file mode 100644 index 00000000..d5f8a2ca --- /dev/null +++ b/widgets/ProgressDialog.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore + +__all__ = ['ProgressDialog'] +class ProgressDialog(QtGui.QProgressDialog): + """Extends QProgressDialog for use in 'with' statements. + Arguments: + labelText (required) + cancelText Text to display on cancel button, or None to disable it. + minimum + maximum + parent + wait Length of time (im ms) to wait before displaying dialog + busyCursor If True, show busy cursor until dialog finishes + + + Example: + with ProgressDialog("Processing..", minVal, maxVal) as dlg: + # do stuff + dlg.setValue(i) ## could also use dlg += 1 + if dlg.wasCanceled(): + raise Exception("Processing canceled by user") + """ + def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False): + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if not isGuiThread: + self.disabled = True + return + + self.disabled = False + + noCancel = False + if cancelText is None: + cancelText = '' + noCancel = True + + self.busyCursor = busyCursor + + QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent) + self.setMinimumDuration(wait) + self.setWindowModality(QtCore.Qt.WindowModal) + self.setValue(self.minimum()) + if noCancel: + self.setCancelButton(None) + + + def __enter__(self): + if self.disabled: + return self + if self.busyCursor: + QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + return self + + def __exit__(self, exType, exValue, exTrace): + if self.disabled: + return + if self.busyCursor: + QtGui.QApplication.restoreOverrideCursor() + self.setValue(self.maximum()) + + def __iadd__(self, val): + """Use inplace-addition operator for easy incrementing.""" + if self.disabled: + return self + self.setValue(self.value()+val) + return self + + + ## wrap all other functions to make sure they aren't being called from non-gui threads + + def setValue(self, val): + if self.disabled: + return + QtGui.QProgressDialog.setValue(self, val) + + def setLabelText(self, val): + if self.disabled: + return + QtGui.QProgressDialog.setLabelText(self, val) + + def setMaximum(self, val): + if self.disabled: + return + QtGui.QProgressDialog.setMaximum(self, val) + + def setMinimum(self, val): + if self.disabled: + return + QtGui.QProgressDialog.setMinimum(self, val) + + def wasCanceled(self): + if self.disabled: + return False + return QtGui.QProgressDialog.wasCanceled(self) + + def maximum(self): + if self.disabled: + return 0 + return QtGui.QProgressDialog.maximum(self) + + def minimum(self): + if self.disabled: + return 0 + return QtGui.QProgressDialog.minimum(self) + \ No newline at end of file diff --git a/widgets/RawImageWidget.py b/widgets/RawImageWidget.py new file mode 100644 index 00000000..84c061a1 --- /dev/null +++ b/widgets/RawImageWidget.py @@ -0,0 +1,79 @@ +from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL + +import pyqtgraph.functions as fn +import numpy as np + +class RawImageWidget(QtGui.QWidget): + """ + Widget optimized for very fast video display. + Generally using an ImageItem inside GraphicsView is fast enough, + but if you need even more performance, this widget is about as fast as it gets (but only in unscaled mode). + """ + def __init__(self, parent=None, scaled=False): + """ + Setting scaled=True will cause the entire image to be displayed within the boundaries of the widget. This also greatly reduces the speed at which it will draw frames. + """ + QtGui.QWidget.__init__(self, parent=None) + self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)) + self.scaled = scaled + self.opts = None + self.image = None + + def setImage(self, img, *args, **kargs): + """ + img must be ndarray of shape (x,y), (x,y,3), or (x,y,4). + Extra arguments are sent to functions.makeARGB + """ + self.opts = (img, args, kargs) + self.image = None + self.update() + + def paintEvent(self, ev): + if self.opts is None: + return + if self.image is None: + argb, alpha = fn.makeARGB(self.opts[0], *self.opts[1], **self.opts[2]) + self.image = fn.makeQImage(argb, alpha) + self.opts = () + #if self.pixmap is None: + #self.pixmap = QtGui.QPixmap.fromImage(self.image) + p = QtGui.QPainter(self) + if self.scaled: + rect = self.rect() + ar = rect.width() / float(rect.height()) + imar = self.image.width() / float(self.image.height()) + if ar > imar: + rect.setWidth(int(rect.width() * imar/ar)) + else: + rect.setHeight(int(rect.height() * ar/imar)) + + p.drawImage(rect, self.image) + else: + p.drawImage(QtCore.QPointF(), self.image) + #p.drawPixmap(self.rect(), self.pixmap) + p.end() + + +class RawImageGLWidget(QtOpenGL.QGLWidget): + """ + Similar to RawImageWidget, but uses a GL widget to do all drawing. + Generally this will be about as fast as using GraphicsView + ImageItem, + but performance may vary on some platforms. + """ + def __init__(self, parent=None, scaled=False): + QtOpenGL.QGLWidget.__init__(self, parent=None) + self.scaled = scaled + self.image = None + + def setImage(self, img): + self.image = fn.makeQImage(img) + self.update() + + def paintEvent(self, ev): + if self.image is None: + return + p = QtGui.QPainter(self) + p.drawImage(self.rect(), self.image) + p.end() + + diff --git a/widgets/SpinBox.py b/widgets/SpinBox.py new file mode 100644 index 00000000..b2b166a3 --- /dev/null +++ b/widgets/SpinBox.py @@ -0,0 +1,481 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.SignalProxy import SignalProxy + +import pyqtgraph.functions as fn +from math import log +from decimal import Decimal as D ## Use decimal to avoid accumulating floating-point errors +from decimal import * +import weakref + +__all__ = ['SpinBox'] +class SpinBox(QtGui.QAbstractSpinBox): + """QSpinBox widget on steroids. Allows selection of numerical value, with extra features: + - SI prefix notation + - Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.) + - Option for unbounded values + - Delayed signals (allows multiple rapid changes with only one change signal) + """ + + ## There's a PyQt bug that leaks a reference to the + ## QLineEdit returned from QAbstractSpinBox.lineEdit() + ## This makes it possible to crash the entire program + ## by making accesses to the LineEdit after the spinBox has been deleted. + ## I have no idea how to get around this.. + + + valueChanged = QtCore.Signal(object) # (value) for compatibility with QSpinBox + sigValueChanged = QtCore.Signal(object) # (self) + sigValueChanging = QtCore.Signal(object, object) # (self, value) sent immediately; no delay. + + def __init__(self, parent=None, value=0.0, **kwargs): + QtGui.QAbstractSpinBox.__init__(self, parent) + self.lastValEmitted = None + self.lastText = '' + self.textValid = True ## If false, we draw a red border + self.setMinimumWidth(0) + self.setMaximumHeight(20) + self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + self.opts = { + 'bounds': [None, None], + + ## Log scaling options #### Log mode is no longer supported. + #'step': 0.1, + #'minStep': 0.001, + #'log': True, + #'dec': False, + + ## decimal scaling option - example + #'step': 0.1, + #'minStep': .001, + #'log': False, + #'dec': True, + + ## normal arithmetic step + 'step': D('0.01'), ## if 'dec' is false, the spinBox steps by 'step' every time + ## if 'dec' is True, the step size is relative to the value + ## 'step' needs to be an integral divisor of ten, ie 'step'*n=10 for some integer value of n (but only if dec is True) + 'log': False, + 'dec': False, ## if true, does decimal stepping. ie from 1-10 it steps by 'step', from 10 to 100 it steps by 10*'step', etc. + ## if true, minStep must be set in order to cross zero. + + + 'int': False, ## Set True to force value to be integer + + 'suffix': '', + 'siPrefix': False, ## Set to True to display numbers with SI prefix (ie, 100pA instead of 1e-10A) + + 'delayUntilEditFinished': True, ## do not send signals until text editing has finished + + ## for compatibility with QDoubleSpinBox and QSpinBox + 'decimals': 2 + } + + self.decOpts = ['step', 'minStep'] + + self.val = D(unicode(value)) ## Value is precise decimal. Ordinary math not allowed. + self.updateText() + self.skipValidate = False + self.setCorrectionMode(self.CorrectToPreviousValue) + self.setKeyboardTracking(False) + self.setOpts(**kwargs) + + + self.editingFinished.connect(self.editingFinishedEvent) + self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange) + + ##lots of config options, just gonna stuff 'em all in here rather than do the get/set crap. + def setOpts(self, **opts): + for k in opts: + if k == 'bounds': + #print opts[k] + self.setMinimum(opts[k][0], update=False) + self.setMaximum(opts[k][1], update=False) + #for i in [0,1]: + #if opts[k][i] is None: + #self.opts[k][i] = None + #else: + #self.opts[k][i] = D(unicode(opts[k][i])) + elif k in ['step', 'minStep']: + self.opts[k] = D(unicode(opts[k])) + elif k == 'value': + pass ## don't set value until bounds have been set + else: + self.opts[k] = opts[k] + if 'value' in opts: + self.setValue(opts['value']) + + ## If bounds have changed, update value to match + if 'bounds' in opts and 'value' not in opts: + self.setValue() + + ## sanity checks: + if self.opts['int']: + step = self.opts['step'] + mStep = self.opts['minStep'] + if (int(step) != step) or (self.opts['dec'] and (int(mStep) != mStep)): + raise Exception("Integer SpinBox may only have integer step and minStep.") + + self.updateText() + + + + def setMaximum(self, m, update=True): + if m is not None: + m = D(unicode(m)) + self.opts['bounds'][1] = m + if update: + self.setValue() + + def setMinimum(self, m, update=True): + if m is not None: + m = D(unicode(m)) + self.opts['bounds'][0] = m + if update: + self.setValue() + + def setPrefix(self, p): + self.setOpts(prefix=p) + + def setRange(self, r0, r1): + self.setOpts(bounds = [r0,r1]) + + def setProperty(self, prop, val): + """setProperty is just for compatibility with QSpinBox""" + if prop == 'value': + #if type(val) is QtCore.QVariant: + #val = val.toDouble()[0] + self.setValue(val) + else: + print "Warning: SpinBox.setProperty('%s', ..) not supported." % prop + + def setSuffix(self, suf): + self.setOpts(suffix=suf) + + def setSingleStep(self, step): + self.setOpts(step=step) + + def setDecimals(self, decimals): + self.setOpts(decimals=decimals) + + def value(self): + if self.opts['int']: + return int(self.val) + else: + return float(self.val) + + def setValue(self, value=None, update=True, delaySignal=False): + """ + Set the value of this spin. + If the value is out of bounds, it will be moved to the nearest boundary + If the spin is integer type, the value will be coerced to int + Returns the actual value set. + + If value is None, then the current value is used (this is for resetting + the value after bounds, etc. have changed) + """ + + if value is None: + value = self.value() + + bounds = self.opts['bounds'] + if bounds[0] is not None and value < bounds[0]: + value = bounds[0] + if bounds[1] is not None and value > bounds[1]: + value = bounds[1] + + if self.opts['int']: + value = int(value) + + value = D(unicode(value)) + if value == self.val: + return + prev = self.val + + self.val = value + if update: + self.updateText(prev=prev) + + self.sigValueChanging.emit(self, float(self.val)) ## change will be emitted in 300ms if there are no subsequent changes. + if not delaySignal: + self.emitChanged() + + return value + + + def emitChanged(self): + self.lastValEmitted = self.val + self.valueChanged.emit(float(self.val)) + self.sigValueChanged.emit(self) + + def delayedChange(self): + try: + if self.val != self.lastValEmitted: + self.emitChanged() + except RuntimeError: + pass ## This can happen if we try to handle a delayed signal after someone else has already deleted the underlying C++ object. + + def widgetGroupInterface(self): + return (self.valueChanged, SpinBox.value, SpinBox.setValue) + + def sizeHint(self): + return QtCore.QSize(120, 0) + + + def stepEnabled(self): + return self.StepUpEnabled | self.StepDownEnabled + + #def fixup(self, *args): + #print "fixup:", args + + def stepBy(self, n): + n = D(int(n)) ## n must be integral number of steps. + s = [D(-1), D(1)][n >= 0] ## determine sign of step + val = self.val + + for i in range(abs(n)): + + if self.opts['log']: + raise Exception("Log mode no longer supported.") + # step = abs(val) * self.opts['step'] + # if 'minStep' in self.opts: + # step = max(step, self.opts['minStep']) + # val += step * s + if self.opts['dec']: + if val == 0: + step = self.opts['minStep'] + exp = None + else: + vs = [D(-1), D(1)][val >= 0] + #exp = D(int(abs(val*(D('1.01')**(s*vs))).log10())) + fudge = D('1.01')**(s*vs) ## fudge factor. at some places, the step size depends on the step sign. + exp = abs(val * fudge).log10().quantize(1, ROUND_FLOOR) + step = self.opts['step'] * D(10)**exp + if 'minStep' in self.opts: + step = max(step, self.opts['minStep']) + val += s * step + #print "Exp:", exp, "step", step, "val", val + else: + val += s*self.opts['step'] + + if 'minStep' in self.opts and abs(val) < self.opts['minStep']: + val = D(0) + self.setValue(val, delaySignal=True) ## note all steps (arrow buttons, wheel, up/down keys..) emit delayed signals only. + + + def valueInRange(self, value): + bounds = self.opts['bounds'] + if bounds[0] is not None and value < bounds[0]: + return False + if bounds[1] is not None and value > bounds[1]: + return False + if self.opts.get('int', False): + if int(value) != value: + return False + return True + + + def updateText(self, prev=None): + #print "Update text." + self.skipValidate = True + if self.opts['siPrefix']: + if self.val == 0 and prev is not None: + (s, p) = fn.siScale(prev) + txt = "0.0 %s%s" % (p, self.opts['suffix']) + else: + txt = fn.siFormat(float(self.val), suffix=self.opts['suffix']) + else: + txt = '%g%s' % (self.val , self.opts['suffix']) + self.lineEdit().setText(txt) + self.lastText = txt + self.skipValidate = False + + def validate(self, strn, pos): + if self.skipValidate: + #print "skip validate" + #self.textValid = False + ret = QtGui.QValidator.Acceptable + else: + try: + ## first make sure we didn't mess with the suffix + suff = self.opts.get('suffix', '') + if len(suff) > 0 and unicode(strn)[-len(suff):] != suff: + #print '"%s" != "%s"' % (unicode(strn)[-len(suff):], suff) + ret = QtGui.QValidator.Invalid + + ## next see if we actually have an interpretable value + else: + val = self.interpret() + if val is False: + #print "can't interpret" + #self.setStyleSheet('SpinBox {border: 2px solid #C55;}') + #self.textValid = False + ret = QtGui.QValidator.Intermediate + else: + if self.valueInRange(val): + if not self.opts['delayUntilEditFinished']: + self.setValue(val, update=False) + #print " OK:", self.val + #self.setStyleSheet('') + #self.textValid = True + + ret = QtGui.QValidator.Acceptable + else: + ret = QtGui.QValidator.Intermediate + + except: + #print " BAD" + #import sys + #sys.excepthook(*sys.exc_info()) + #self.textValid = False + #self.setStyleSheet('SpinBox {border: 2px solid #C55;}') + ret = QtGui.QValidator.Intermediate + + ## draw / clear border + if ret == QtGui.QValidator.Intermediate: + self.textValid = False + elif ret == QtGui.QValidator.Acceptable: + self.textValid = True + ## note: if text is invalid, we don't change the textValid flag + ## since the text will be forced to its previous state anyway + self.update() + + ## support 2 different pyqt APIs. Bleh. + if hasattr(QtCore, 'QString'): + return (ret, pos) + else: + return (ret, strn, pos) + + def paintEvent(self, ev): + QtGui.QAbstractSpinBox.paintEvent(self, ev) + + ## draw red border if text is invalid + if not self.textValid: + p = QtGui.QPainter(self) + p.setRenderHint(p.Antialiasing) + p.setPen(fn.mkPen((200,50,50), width=2)) + p.drawRoundedRect(self.rect().adjusted(2, 2, -2, -2), 4, 4) + p.end() + + + def interpret(self): + """Return value of text. Return False if text is invalid, raise exception if text is intermediate""" + strn = self.lineEdit().text() + suf = self.opts['suffix'] + if len(suf) > 0: + if strn[-len(suf):] != suf: + return False + #raise Exception("Units are invalid.") + strn = strn[:-len(suf)] + try: + val = fn.siEval(strn) + except: + #sys.excepthook(*sys.exc_info()) + #print "invalid" + return False + #print val + return val + + #def interpretText(self, strn=None): + #print "Interpret:", strn + #if strn is None: + #strn = self.lineEdit().text() + #self.setValue(siEval(strn), update=False) + ##QtGui.QAbstractSpinBox.interpretText(self) + + + def editingFinishedEvent(self): + """Edit has finished; set value.""" + #print "Edit finished." + if unicode(self.lineEdit().text()) == self.lastText: + #print "no text change." + return + try: + val = self.interpret() + except: + return + + if val is False: + #print "value invalid:", str(self.lineEdit().text()) + return + if val == self.val: + #print "no value change:", val, self.val + return + self.setValue(val, delaySignal=False) ## allow text update so that values are reformatted pretty-like + + #def textChanged(self): + #print "Text changed." + + +### Drop-in replacement for SpinBox; just for crash-testing +#class SpinBox(QtGui.QDoubleSpinBox): + #valueChanged = QtCore.Signal(object) # (value) for compatibility with QSpinBox + #sigValueChanged = QtCore.Signal(object) # (self) + #sigValueChanging = QtCore.Signal(object) # (value) + #def __init__(self, parent=None, *args, **kargs): + #QtGui.QSpinBox.__init__(self, parent) + + #def __getattr__(self, attr): + #return lambda *args, **kargs: None + + #def widgetGroupInterface(self): + #return (self.valueChanged, SpinBox.value, SpinBox.setValue) + + +if __name__ == '__main__': + import sys + app = QtGui.QApplication([]) + + def valueChanged(sb): + #sb = QtCore.QObject.sender() + print str(sb) + " valueChanged: %s" % str(sb.value()) + + def valueChanging(sb, value): + #sb = QtCore.QObject.sender() + print str(sb) + " valueChanging: %s" % str(sb.value()) + + def mkWin(): + win = QtGui.QMainWindow() + g = QtGui.QFormLayout() + w = QtGui.QWidget() + w.setLayout(g) + win.setCentralWidget(w) + s1 = SpinBox(value=5, step=0.1, bounds=[-1.5, None], suffix='units') + t1 = QtGui.QLineEdit() + g.addRow(s1, t1) + s2 = SpinBox(value=10e-6, dec=True, step=0.1, minStep=1e-6, suffix='A', siPrefix=True) + t2 = QtGui.QLineEdit() + g.addRow(s2, t2) + s3 = SpinBox(value=1000, dec=True, step=0.5, minStep=1e-6, bounds=[1, 1e9], suffix='Hz', siPrefix=True) + t3 = QtGui.QLineEdit() + g.addRow(s3, t3) + s4 = SpinBox(int=True, dec=True, step=1, minStep=1, bounds=[-10, 1000]) + t4 = QtGui.QLineEdit() + g.addRow(s4, t4) + + win.show() + + import sys + for sb in [s1, s2, s3,s4]: + + #QtCore.QObject.connect(sb, QtCore.SIGNAL('valueChanged(double)'), lambda v: sys.stdout.write(str(sb) + " valueChanged\n")) + #QtCore.QObject.connect(sb, QtCore.SIGNAL('editingFinished()'), lambda: sys.stdout.write(str(sb) + " editingFinished\n")) + sb.sigValueChanged.connect(valueChanged) + sb.sigValueChanging.connect(valueChanging) + sb.editingFinished.connect(lambda: sys.stdout.write(str(sb) + " editingFinished\n")) + return win, w, [s1, s2, s3, s4] + a = mkWin() + + + def test(n=100): + for i in range(n): + win, w, sb = mkWin() + for s in sb: + w.setParent(None) + s.setParent(None) + s.valueChanged.disconnect() + s.editingFinished.disconnect() + + ## Start Qt event loop unless running in interactive mode. + if sys.flags.interactive != 1: + app.exec_() diff --git a/widgets/TableWidget.py b/widgets/TableWidget.py new file mode 100644 index 00000000..b1d38a92 --- /dev/null +++ b/widgets/TableWidget.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore + +import numpy as np +try: + import metaarray + HAVE_METAARRAY = True +except: + HAVE_METAARRAY = False + +__all__ = ['TableWidget'] +class TableWidget(QtGui.QTableWidget): + """Extends QTableWidget with some useful functions for automatic data handling. + Can automatically format and display: + numpy arrays + numpy record arrays + metaarrays + list-of-lists [[1,2,3], [4,5,6]] + dict-of-lists {'x': [1,2,3], 'y': [4,5,6]} + list-of-dicts [ + {'x': 1, 'y': 4}, + {'x': 2, 'y': 5}, + {'x': 3, 'y': 6} + ] + """ + + def __init__(self, *args): + QtGui.QTableWidget.__init__(self, *args) + self.setVerticalScrollMode(self.ScrollPerPixel) + self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection) + self.clear() + self.contextMenu = QtGui.QMenu() + self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel) + self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll) + self.contextMenu.addAction('Save Selection').triggered.connect(self.saveSel) + self.contextMenu.addAction('Save All').triggered.connect(self.saveAll) + + def clear(self): + QtGui.QTableWidget.clear(self) + self.verticalHeadersSet = False + self.horizontalHeadersSet = False + self.items = [] + self.setRowCount(0) + self.setColumnCount(0) + + def setData(self, data): + self.clear() + self.appendData(data) + + def appendData(self, data): + """Types allowed: + 1 or 2D numpy array or metaArray + 1D numpy record array + list-of-lists, list-of-dicts or dict-of-lists + """ + fn0, header0 = self.iteratorFn(data) + if fn0 is None: + self.clear() + return + it0 = fn0(data) + try: + first = it0.next() + except StopIteration: + return + #if type(first) == type(np.float64(1)): + # return + fn1, header1 = self.iteratorFn(first) + if fn1 is None: + self.clear() + return + + #print fn0, header0 + #print fn1, header1 + firstVals = [x for x in fn1(first)] + self.setColumnCount(len(firstVals)) + + #print header0, header1 + if not self.verticalHeadersSet and header0 is not None: + #print "set header 0:", header0 + self.setRowCount(len(header0)) + self.setVerticalHeaderLabels(header0) + self.verticalHeadersSet = True + if not self.horizontalHeadersSet and header1 is not None: + #print "set header 1:", header1 + self.setHorizontalHeaderLabels(header1) + self.horizontalHeadersSet = True + + self.setRow(0, firstVals) + i = 1 + for row in it0: + self.setRow(i, [x for x in fn1(row)]) + i += 1 + + def iteratorFn(self, data): + """Return 1) a function that will provide an iterator for data and 2) a list of header strings""" + if isinstance(data, list): + return lambda d: d.__iter__(), None + elif isinstance(data, dict): + return lambda d: d.itervalues(), map(str, data.keys()) + elif HAVE_METAARRAY and isinstance(data, metaarray.MetaArray): + if data.axisHasColumns(0): + header = [str(data.columnName(0, i)) for i in xrange(data.shape[0])] + elif data.axisHasValues(0): + header = map(str, data.xvals(0)) + else: + header = None + return self.iterFirstAxis, header + elif isinstance(data, np.ndarray): + return self.iterFirstAxis, None + elif isinstance(data, np.void): + return self.iterate, map(str, data.dtype.names) + elif data is None: + return (None,None) + else: + raise Exception("Don't know how to iterate over data type: %s" % str(type(data))) + + def iterFirstAxis(self, data): + for i in xrange(data.shape[0]): + yield data[i] + + def iterate(self, data): ## for numpy.void, which can be iterated but mysteriously has no __iter__ (??) + for x in data: + yield x + + def appendRow(self, data): + self.appendData([data]) + + def addRow(self, vals): + #print "add row:", vals + row = self.rowCount() + self.setRowCount(row+1) + self.setRow(row, vals) + + def setRow(self, row, vals): + if row > self.rowCount()-1: + self.setRowCount(row+1) + for col in xrange(self.columnCount()): + val = vals[col] + if isinstance(val, float) or isinstance(val, np.floating): + s = "%0.3g" % val + else: + s = str(val) + item = QtGui.QTableWidgetItem(s) + item.value = val + #print "add item to row %d:"%row, item, item.value + self.items.append(item) + self.setItem(row, col, item) + + def serialize(self, useSelection=False): + """Convert entire table (or just selected area) into tab-separated text values""" + if useSelection: + selection = self.selectedRanges()[0] + rows = range(selection.topRow(), selection.bottomRow()+1) + columns = range(selection.leftColumn(), selection.rightColumn()+1) + else: + rows = range(self.rowCount()) + columns = range(self.columnCount()) + + + data = [] + if self.horizontalHeadersSet: + row = [] + if self.verticalHeadersSet: + row.append(u'') + + for c in columns: + row.append(unicode(self.horizontalHeaderItem(c).text())) + data.append(row) + + for r in rows: + row = [] + if self.verticalHeadersSet: + row.append(unicode(self.verticalHeaderItem(r).text())) + for c in columns: + item = self.item(r, c) + if item is not None: + row.append(unicode(item.value)) + else: + row.append(u'') + data.append(row) + + s = u'' + for row in data: + s += (u'\t'.join(row) + u'\n') + return s + + def copySel(self): + """Copy selected data to clipboard.""" + QtGui.QApplication.clipboard().setText(self.serialize(useSelection=True)) + + def copyAll(self): + """Copy all data to clipboard.""" + QtGui.QApplication.clipboard().setText(self.serialize(useSelection=False)) + + def saveSel(self): + """Save selected data to file.""" + self.save(self.serialize(useSelection=True)) + + def saveAll(self): + """Save all data to file.""" + self.save(self.serialize(useSelection=False)) + + def save(self, data): + fileName = QtGui.QFileDialog.getSaveFileName(self, "Save As..", "", "Tab-separated values (*.tsv)") + if fileName == '': + return + open(fileName, 'w').write(data) + + + def contextMenuEvent(self, ev): + self.contextMenu.popup(ev.globalPos()) + + def keyPressEvent(self, ev): + if ev.text() == 'c' and ev.modifiers() == QtCore.Qt.ControlModifier: + ev.accept() + self.copy() + else: + ev.ignore() + + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + t = TableWidget() + win.setCentralWidget(t) + win.resize(800,600) + win.show() + + ll = [[1,2,3,4,5]] * 20 + ld = [{'x': 1, 'y': 2, 'z': 3}] * 20 + dl = {'x': range(20), 'y': range(20), 'z': range(20)} + + a = np.ones((20, 5)) + ra = np.ones((20,), dtype=[('x', int), ('y', int), ('z', int)]) + + t.setData(ll) + + if HAVE_METAARRAY: + ma = metaarray.MetaArray(np.ones((20, 3)), info=[ + {'values': np.linspace(1, 5, 20)}, + {'cols': [ + {'name': 'x'}, + {'name': 'y'}, + {'name': 'z'}, + ]} + ]) + t.setData(ma) + \ No newline at end of file diff --git a/widgets/TreeWidget.py b/widgets/TreeWidget.py new file mode 100644 index 00000000..43bba487 --- /dev/null +++ b/widgets/TreeWidget.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +if __name__ == '__main__': + import sys, os + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) +from pyqtgraph.Qt import QtGui, QtCore +from weakref import * + +__all__ = ['TreeWidget'] +class TreeWidget(QtGui.QTreeWidget): + """Extends QTreeWidget to allow internal drag/drop with widgets in the tree. + Also maintains the expanded state of subtrees as they are moved. + This class demonstrates the absurd lengths one must go to to make drag/drop work.""" + + sigItemMoved = QtCore.Signal(object, object, object) # (item, parent, index) + + def __init__(self, parent=None): + QtGui.QTreeWidget.__init__(self, parent) + #self.itemWidgets = WeakKeyDictionary() + self.setAcceptDrops(True) + self.setDragEnabled(True) + self.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed|QtGui.QAbstractItemView.SelectedClicked) + self.placeholders = [] + self.childNestingLimit = None + + def setItemWidget(self, item, col, wid): + w = QtGui.QWidget() ## foster parent / surrogate child widget + l = QtGui.QVBoxLayout() + l.setContentsMargins(0,0,0,0) + w.setLayout(l) + w.setSizePolicy(wid.sizePolicy()) + w.setMinimumHeight(wid.minimumHeight()) + w.setMinimumWidth(wid.minimumWidth()) + l.addWidget(wid) + w.realChild = wid + self.placeholders.append(w) + QtGui.QTreeWidget.setItemWidget(self, item, col, w) + + def itemWidget(self, item, col): + w = QtGui.QTreeWidget.itemWidget(self, item, col) + if w is not None: + w = w.realChild + return w + + def dropMimeData(self, parent, index, data, action): + item = self.currentItem() + p = parent + #print "drop", item, "->", parent, index + while True: + if p is None: + break + if p is item: + return False + #raise Exception("Can not move item into itself.") + p = p.parent() + + if not self.itemMoving(item, parent, index): + return False + + currentParent = item.parent() + if currentParent is None: + currentParent = self.invisibleRootItem() + if parent is None: + parent = self.invisibleRootItem() + + if currentParent is parent and index > parent.indexOfChild(item): + index -= 1 + + self.prepareMove(item) + + currentParent.removeChild(item) + #print " insert child to index", index + parent.insertChild(index, item) ## index will not be correct + self.setCurrentItem(item) + + self.recoverMove(item) + #self.emit(QtCore.SIGNAL('itemMoved'), item, parent, index) + self.sigItemMoved.emit(item, parent, index) + return True + + def itemMoving(self, item, parent, index): + """Called when item has been dropped elsewhere in the tree. + Return True to accept the move, False to reject.""" + return True + + def prepareMove(self, item): + item.__widgets = [] + item.__expanded = item.isExpanded() + for i in range(self.columnCount()): + w = self.itemWidget(item, i) + item.__widgets.append(w) + if w is None: + continue + w.setParent(None) + for i in range(item.childCount()): + self.prepareMove(item.child(i)) + + def recoverMove(self, item): + for i in range(self.columnCount()): + w = item.__widgets[i] + if w is None: + continue + self.setItemWidget(item, i, w) + for i in range(item.childCount()): + self.recoverMove(item.child(i)) + + item.setExpanded(False) ## Items do not re-expand correctly unless they are collapsed first. + QtGui.QApplication.instance().processEvents() + item.setExpanded(item.__expanded) + + def collapseTree(self, item): + item.setExpanded(False) + for i in range(item.childCount()): + self.collapseTree(item.child(i)) + + def removeTopLevelItem(self, item): + for i in range(self.topLevelItemCount()): + if self.topLevelItem(i) is item: + self.takeTopLevelItem(i) + return + raise Exception("Item '%s' not in top-level items." % str(item)) + + def listAllItems(self, item=None): + items = [] + if item != None: + items.append(item) + else: + item = self.invisibleRootItem() + + for cindex in range(item.childCount()): + foundItems = self.listAllItems(item=item.child(cindex)) + for f in foundItems: + items.append(f) + return items + + def dropEvent(self, ev): + QtGui.QTreeWidget.dropEvent(self, ev) + self.updateDropFlags() + + + def updateDropFlags(self): + ### intended to put a limit on how deep nests of children can go. + ### self.childNestingLimit is upheld when moving items without children, but if the item being moved has children/grandchildren, the children/grandchildren + ### can end up over the childNestingLimit. + if self.childNestingLimit == None: + pass # enable drops in all items (but only if there are drops that aren't enabled? for performance...) + else: + items = self.listAllItems() + for item in items: + parentCount = 0 + p = item.parent() + while p is not None: + parentCount += 1 + p = p.parent() + if parentCount >= self.childNestingLimit: + item.setFlags(item.flags() & (~QtCore.Qt.ItemIsDropEnabled)) + else: + item.setFlags(item.flags() | QtCore.Qt.ItemIsDropEnabled) + +if __name__ == '__main__': + app = QtGui.QApplication([]) + + w = TreeWidget() + w.setColumnCount(2) + w.show() + + i1 = QtGui.QTreeWidgetItem(["Item 1"]) + i11 = QtGui.QTreeWidgetItem(["Item 1.1"]) + i12 = QtGui.QTreeWidgetItem(["Item 1.2"]) + i2 = QtGui.QTreeWidgetItem(["Item 2"]) + i21 = QtGui.QTreeWidgetItem(["Item 2.1"]) + i211 = QtGui.QTreeWidgetItem(["Item 2.1.1"]) + i212 = QtGui.QTreeWidgetItem(["Item 2.1.2"]) + i22 = QtGui.QTreeWidgetItem(["Item 2.2"]) + i3 = QtGui.QTreeWidgetItem(["Item 3"]) + i4 = QtGui.QTreeWidgetItem(["Item 4"]) + i5 = QtGui.QTreeWidgetItem(["Item 5"]) + + w.addTopLevelItem(i1) + w.addTopLevelItem(i2) + w.addTopLevelItem(i3) + w.addTopLevelItem(i4) + w.addTopLevelItem(i5) + i1.addChild(i11) + i1.addChild(i12) + i2.addChild(i21) + i21.addChild(i211) + i21.addChild(i212) + i2.addChild(i22) + + b1 = QtGui.QPushButton("B1") + w.setItemWidget(i1, 1, b1) + + app.exec_() + diff --git a/widgets/VerticalLabel.py b/widgets/VerticalLabel.py new file mode 100644 index 00000000..fa45ae5d --- /dev/null +++ b/widgets/VerticalLabel.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtGui, QtCore + +__all__ = ['VerticalLabel'] +#class VerticalLabel(QtGui.QLabel): + #def paintEvent(self, ev): + #p = QtGui.QPainter(self) + #p.rotate(-90) + #self.hint = p.drawText(QtCore.QRect(-self.height(), 0, self.height(), self.width()), QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter, self.text()) + #p.end() + #self.setMinimumWidth(self.hint.height()) + #self.setMinimumHeight(self.hint.width()) + + #def sizeHint(self): + #if hasattr(self, 'hint'): + #return QtCore.QSize(self.hint.height(), self.hint.width()) + #else: + #return QtCore.QSize(16, 50) + +class VerticalLabel(QtGui.QLabel): + def __init__(self, text, orientation='vertical', forceWidth=True): + QtGui.QLabel.__init__(self, text) + self.forceWidth = forceWidth + self.orientation = None + self.setOrientation(orientation) + + def setOrientation(self, o): + if self.orientation == o: + return + self.orientation = o + self.update() + self.updateGeometry() + + def paintEvent(self, ev): + p = QtGui.QPainter(self) + #p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200))) + #p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100))) + #p.drawRect(self.rect().adjusted(0, 0, -1, -1)) + + #p.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255))) + + if self.orientation == 'vertical': + p.rotate(-90) + rgn = QtCore.QRect(-self.height(), 0, self.height(), self.width()) + else: + rgn = self.contentsRect() + align = self.alignment() + #align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter + + self.hint = p.drawText(rgn, align, self.text()) + p.end() + + if self.orientation == 'vertical': + self.setMaximumWidth(self.hint.height()) + self.setMinimumWidth(0) + self.setMaximumHeight(16777215) + if self.forceWidth: + self.setMinimumHeight(self.hint.width()) + else: + self.setMinimumHeight(0) + else: + self.setMaximumHeight(self.hint.height()) + self.setMinimumHeight(0) + self.setMaximumWidth(16777215) + if self.forceWidth: + self.setMinimumWidth(self.hint.width()) + else: + self.setMinimumWidth(0) + + def sizeHint(self): + if self.orientation == 'vertical': + if hasattr(self, 'hint'): + return QtCore.QSize(self.hint.height(), self.hint.width()) + else: + return QtCore.QSize(19, 50) + else: + if hasattr(self, 'hint'): + return QtCore.QSize(self.hint.width(), self.hint.height()) + else: + return QtCore.QSize(50, 19) + + +if __name__ == '__main__': + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + w = QtGui.QWidget() + l = QtGui.QGridLayout() + w.setLayout(l) + + l1 = VerticalLabel("text 1", orientation='horizontal') + l2 = VerticalLabel("text 2") + l3 = VerticalLabel("text 3") + l4 = VerticalLabel("text 4", orientation='horizontal') + l.addWidget(l1, 0, 0) + l.addWidget(l2, 1, 1) + l.addWidget(l3, 2, 2) + l.addWidget(l4, 3, 3) + win.setCentralWidget(w) + win.show() \ No newline at end of file diff --git a/widgets/__init__.py b/widgets/__init__.py new file mode 100644 index 00000000..a81fe391 --- /dev/null +++ b/widgets/__init__.py @@ -0,0 +1,21 @@ +## just import everything from sub-modules + +#import os + +#d = os.path.split(__file__)[0] +#files = [] +#for f in os.listdir(d): + #if os.path.isdir(os.path.join(d, f)): + #files.append(f) + #elif f[-3:] == '.py' and f != '__init__.py': + #files.append(f[:-3]) + +#for modName in files: + #mod = __import__(modName, globals(), locals(), fromlist=['*']) + #if hasattr(mod, '__all__'): + #names = mod.__all__ + #else: + #names = [n for n in dir(mod) if n[0] != '_'] + #for k in names: + #print modName, k + #globals()[k] = getattr(mod, k) From aaece4badc494dcec754486348a07ba0d8ad47ac Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 1 Mar 2012 22:17:55 -0500 Subject: [PATCH 002/238] bugfixes --- canvas/Canvas.py | 8 +- graphicsItems/ArrowItem.py | 15 +- graphicsItems/AxisItem.py | 6 +- graphicsItems/ROI.py | 598 ++++++++++--------------------- graphicsItems/ViewBox/ViewBox.py | 5 +- imageview/ImageView.py | 4 +- widgets/GraphicsView.py | 2 +- 7 files changed, 215 insertions(+), 423 deletions(-) diff --git a/canvas/Canvas.py b/canvas/Canvas.py index 9bd9e863..8514b60e 100644 --- a/canvas/Canvas.py +++ b/canvas/Canvas.py @@ -298,6 +298,7 @@ class Canvas(QtGui.QWidget): Common options are name, pos, scale, and z """ citem = CanvasItem(item, **opts) + item._canvasItem = citem self.addItem(citem) return citem @@ -493,12 +494,13 @@ class Canvas(QtGui.QWidget): def removeItem(self, item): if isinstance(item, CanvasItem): item.setCanvas(None) - #self.view.scene().removeItem(item.item) self.itemList.removeTopLevelItem(item.listItem) - #del self.items[item.name] self.items.remove(item) else: - self.view.removeItem(item) + if hasattr(item, '_canvasItem'): + self.removeItem(item._canvasItem) + else: + self.view.removeItem(item) ## disconnect signals, remove from list, etc.. diff --git a/graphicsItems/ArrowItem.py b/graphicsItems/ArrowItem.py index d9cf9663..be409c42 100644 --- a/graphicsItems/ArrowItem.py +++ b/graphicsItems/ArrowItem.py @@ -11,14 +11,14 @@ class ArrowItem(QtGui.QGraphicsPolygonItem): def __init__(self, **opts): - QtGui.QGraphicsPolygonItem.__init__(self) + QtGui.QGraphicsPolygonItem.__init__(self, opts.get('parent', None)) defOpts = { 'style': 'tri', 'pxMode': True, 'size': 20, - 'angle': -150, + 'angle': -150, ## If the angle is 0, the arrow points left 'pos': (0,0), - 'width': 8, + 'width': None, ## width is automatically size / 2. 'tipAngle': 25, 'baseAngle': 90, 'pen': (200,200,200), @@ -38,10 +38,15 @@ class ArrowItem(QtGui.QGraphicsPolygonItem): self.opts = opts if opts['style'] == 'tri': + if opts['width'] is None: + width = opts['size'] / 2. + else: + width = opts['width'] + points = [ QtCore.QPointF(0,0), - QtCore.QPointF(opts['size'],-opts['width']/2.), - QtCore.QPointF(opts['size'],opts['width']/2.), + QtCore.QPointF(opts['size'],-width/2.), + QtCore.QPointF(opts['size'],width/2.), ] poly = QtGui.QPolygonF(points) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 98faa152..11834f01 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -284,7 +284,7 @@ class AxisItem(GraphicsWidget): lengthInPixels = Point(points[1] - points[0]).length() ## decide optimal tick spacing in pixels - pixelSpacing = np.log(lengthInPixels+10) * 3 + pixelSpacing = np.log(lengthInPixels+10) * 2 optimalTickCount = lengthInPixels / pixelSpacing ## Determine optimal tick spacing @@ -328,9 +328,11 @@ class AxisItem(GraphicsWidget): ## draw three different intervals, long ticks first texts = [] for i in [2,1,0]: - if i1+i > len(intervals): + if i1+i >= len(intervals) or i1+i < 0: + print "AxisItem.paint error: i1=%d, i=%d, len(intervals)=%d" % (i1, i, len(intervals)) continue ## spacing for this interval + sp = pw*intervals[i1+i] ## determine starting tick diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index f8bb0d72..833baa94 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -37,7 +37,8 @@ def rectStr(r): class ROI(GraphicsObject): """Generic region-of-interest widget. - Can be used for implementing many types of selection box with rotate/translate/scale handles.""" + Can be used for implementing many types of selection box with rotate/translate/scale handles. + """ sigRegionChangeFinished = QtCore.Signal(object) sigRegionChangeStarted = QtCore.Signal(object) @@ -53,6 +54,8 @@ class ROI(GraphicsObject): self.translatable = movable self.rotateAllowed = True + self.freeHandleMoved = False ## keep track of whether free handles have moved since last change signal was emitted. + if pen is None: pen = (255, 255, 255) self.setPen(pen) @@ -78,29 +81,33 @@ class ROI(GraphicsObject): #self.setFlag(self.ItemIsSelectable, True) def getState(self): - return self.state.copy() + return self.stateCopy() + + def stateCopy(self): + sc = {} + sc['pos'] = Point(self.state['pos']) + sc['size'] = Point(self.state['size']) + sc['angle'] = self.state['angle'] + return sc def saveState(self): - """Return the state of the widget in a format suitable for storing to disk.""" + """Return the state of the widget in a format suitable for storing to disk. (Points are converted to tuple)""" state = {} state['pos'] = tuple(self.state['pos']) state['size'] = tuple(self.state['size']) state['angle'] = self.state['angle'] return state - def setState(self, state): + def setState(self, state, update=True): self.setPos(state['pos'], update=False) self.setSize(state['size'], update=False) - self.setAngle(state['angle']) + self.setAngle(state['angle'], update=update) def setZValue(self, z): QtGui.QGraphicsItem.setZValue(self, z) for h in self.handles: h['item'].setZValue(z+1) - def sceneBounds(self): - return self.sceneTransform().mapRect(self.boundingRect()) - def parentBounds(self): return self.mapToParent(self.boundingRect()).boundingRect() @@ -118,34 +125,123 @@ class ROI(GraphicsObject): def angle(self): return self.getState()['angle'] - def setPos(self, pos, update=True): - #print "setPos() called." + def setPos(self, pos, update=True, finish=True): + """Set the position of the ROI (in the parent's coordinate system). + By default, this will cause both sigStateChanged and sigStateChangeFinished to be emitted. + + If finish is False, then sigStateChangeFinished will not be emitted. You can then use + stateChangeFinished() to cause the signal to be emitted after a series of state changes. + + If update is False, the state change will be remembered but not processed and no signals + will be emitted. You can then use stateChanged() to complete the state change. This allows + multiple change functions to be called sequentially while minimizing processing overhead + and repeated signals. Setting update=False also forces finish=False. + """ + pos = Point(pos) self.state['pos'] = pos QtGui.QGraphicsItem.setPos(self, pos) if update: - self.updateHandles() - self.handleChange() + self.stateChanged(finish=finish) - def setSize(self, size, update=True): + def setSize(self, size, update=True, finish=True): + """Set the size of the ROI. May be specified as a QPoint, Point, or list of two values. + See setPos() for an explanation of the update and finish arguments. + """ size = Point(size) self.prepareGeometryChange() self.state['size'] = size if update: - self.updateHandles() - self.handleChange() + self.stateChanged(finish=finish) - def setAngle(self, angle, update=True): + def setAngle(self, angle, update=True, finish=True): + """Set the angle of rotation (in degrees) for this ROI. + See setPos() for an explanation of the update and finish arguments. + """ self.state['angle'] = angle tr = QtGui.QTransform() #tr.rotate(-angle * 180 / np.pi) tr.rotate(angle) self.setTransform(tr) if update: - self.updateHandles() - self.handleChange() + self.stateChanged(finish=finish) + def scale(self, s, center=[0,0], update=True, finish=True): + """ + Resize the ROI by scaling relative to *center*. + See setPos() for an explanation of the *update* and *finish* arguments. + """ + c = self.mapToScene(Point(center) * self.state['size']) + self.prepareGeometryChange() + newSize = self.state['size'] * s + c1 = self.mapToScene(Point(center) * newSize) + newPos = self.state['pos'] + c - c1 + self.setSize(newSize, update=False) + self.setPos(self.state['pos'], update=update, finish=finish) + + + def translate(self, *args, **kargs): + """ + Move the ROI to a new position. + Accepts either (x, y, snap) or ([x,y], snap) as arguments + If the ROI is bounded and the move would exceed boundaries, then the ROI + is moved to the nearest acceptable position instead. + + snap can be: + None (default): use self.translateSnap and self.snapSize to determine whether/how to snap + False: do not snap + Point(w,h) snap to rectangular grid with spacing (w,h) + True: snap using self.snapSize (and ignoring self.translateSnap) + + Also accepts *update* and *finish* arguments (see setPos() for a description of these). + """ + + if len(args) == 1: + pt = args[0] + else: + pt = args + + newState = self.stateCopy() + newState['pos'] = newState['pos'] + pt + + ## snap position + #snap = kargs.get('snap', None) + #if (snap is not False) and not (snap is None and self.translateSnap is False): + + snap = kargs.get('snap', None) + if snap is None: + snap = self.translateSnap + if snap is not False: + newState['pos'] = self.getSnapPosition(newState['pos'], snap=snap) + + #d = ev.scenePos() - self.mapToScene(self.pressPos) + if self.maxBounds is not None: + r = self.stateRect(newState) + #r0 = self.sceneTransform().mapRect(self.boundingRect()) + d = Point(0,0) + if self.maxBounds.left() > r.left(): + d[0] = self.maxBounds.left() - r.left() + elif self.maxBounds.right() < r.right(): + d[0] = self.maxBounds.right() - r.right() + if self.maxBounds.top() > r.top(): + d[1] = self.maxBounds.top() - r.top() + elif self.maxBounds.bottom() < r.bottom(): + d[1] = self.maxBounds.bottom() - r.bottom() + newState['pos'] += d + + #self.state['pos'] = newState['pos'] + update = kargs.get('update', True) + finish = kargs.get('finish', True) + self.setPos(newState['pos'], update=update, finish=finish) + #if 'update' not in kargs or kargs['update'] is True: + #self.stateChanged() + + def rotate(self, angle, center=(0,0), angleSnap=False, update=True, finish=True): + pass + #self.setAngle(self.angle()+angle, update=update, finish=finish) + + def addTranslateHandle(self, pos, axes=None, item=None, name=None): pos = Point(pos) return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}) @@ -236,47 +332,6 @@ class ROI(GraphicsObject): for h in self.handles: h['item'].hide() - #def mousePressEvent(self, ev): - ### Bug: sometimes we get events we shouldn't. - #p = ev.pos() - #if not self.isMoving and not self.shape().contains(p): - #ev.ignore() - #return - #if ev.button() == QtCore.Qt.LeftButton: - #self.setSelected(True) - #if self.translatable: - #self.isMoving = True - #self.preMoveState = self.getState() - #self.cursorOffset = self.scenePos() - ev.scenePos() - ##self.emit(QtCore.SIGNAL('regionChangeStarted'), self) - #self.sigRegionChangeStarted.emit(self) - #ev.accept() - #else: - #ev.ignore() - #elif ev.button() == QtCore.Qt.RightButton: - #if self.isMoving: - #ev.accept() - #self.cancelMove() - #else: - #ev.ignore() - #else: - #ev.ignore() - - #def mouseMoveEvent(self, ev): - ##print "mouse move", ev.pos() - #if self.translatable and self.isMoving and ev.buttons() == QtCore.Qt.LeftButton: - #snap = True if (ev.modifiers() & QtCore.Qt.ControlModifier) else None - ##if self.translateSnap or (ev.modifiers() & QtCore.Qt.ControlModifier): - ##snap = Point(self.snapSize, self.snapSize) - #newPos = ev.scenePos() + self.cursorOffset - #newPos = self.mapSceneToParent(newPos) - #self.translate(newPos - self.pos(), snap=snap) - - #def mouseReleaseEvent(self, ev): - #if self.translatable: - #self.isMoving = False - ##self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - #self.sigRegionChangeFinished.emit(self) def hoverEvent(self, ev): if self.translatable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): @@ -288,7 +343,7 @@ class ROI(GraphicsObject): def mouseDragEvent(self, ev): if ev.isStart(): - p = ev.pos() + #p = ev.pos() #if not self.isMoving and not self.shape().contains(p): #ev.ignore() #return @@ -298,7 +353,6 @@ class ROI(GraphicsObject): self.isMoving = True self.preMoveState = self.getState() self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) - #self.emit(QtCore.SIGNAL('regionChangeStarted'), self) self.sigRegionChangeStarted.emit(self) ev.accept() else: @@ -306,21 +360,16 @@ class ROI(GraphicsObject): elif ev.isFinish(): if self.translatable: + if self.isMoving: + self.stateChangeFinished() self.isMoving = False - #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) - self.sigRegionChangeFinished.emit(self) return if self.translatable and self.isMoving and ev.buttons() == QtCore.Qt.LeftButton: snap = True if (ev.modifiers() & QtCore.Qt.ControlModifier) else None - #if self.translateSnap or (ev.modifiers() & QtCore.Qt.ControlModifier): - #snap = Point(self.snapSize, self.snapSize) newPos = self.mapToParent(ev.pos()) + self.cursorOffset - #newPos = self.mapSceneToParent(newPos) - self.translate(newPos - self.pos(), snap=snap) + self.translate(newPos - self.pos(), snap=snap, finish=False) - - def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton: if self.isMoving: @@ -329,24 +378,24 @@ class ROI(GraphicsObject): else: ev.ignore() - def cancelMove(self): self.isMoving = False self.setState(self.preMoveState) - def pointDragEvent(self, pt, ev): - if ev.isStart(): - self.isMoving = True - self.preMoveState = self.getState() + #def pointDragEvent(self, pt, ev): + ### just for handling drag start/stop. + ### drag moves are handled through movePoint() + + #if ev.isStart(): + #self.isMoving = True + #self.preMoveState = self.getState() - self.sigRegionChangeStarted.emit(self) - elif ev.isFinish(): - self.isMoving = False - self.sigRegionChangeFinished.emit(self) - return - - #self.movePoint(pt, ev.scenePos(), ev.modifiers()) + #self.sigRegionChangeStarted.emit(self) + #elif ev.isFinish(): + #self.isMoving = False + #self.sigRegionChangeFinished.emit(self) + #return #def pointPressEvent(self, pt, ev): @@ -368,37 +417,20 @@ class ROI(GraphicsObject): #def pointMoveEvent(self, pt, ev): #self.movePoint(pt, ev.scenePos(), ev.modifiers()) - def stateCopy(self): - sc = {} - sc['pos'] = Point(self.state['pos']) - sc['size'] = Point(self.state['size']) - sc['angle'] = self.state['angle'] - return sc - - def updateHandles(self): - #print "update", self.handles - for h in self.handles: - #print " try", h - if h['item'] in self.childItems(): - p = h['pos'] - #print h['pos'] * self.state['size'] - h['item'].setPos(h['pos'] * self.state['size']) - #else: - #print " Not child!", self.childItems() - def checkPointMove(self, pt, pos, modifiers): + """When handles move, they must ask the ROI if the move is acceptable. + By default, this always returns True. Subclasses may wish override. + """ return True - - def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier()): - #print "movePoint() called." + + def movePoint(self, pt, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True): + ## called by Handles when they are moved. ## pos is the new position of the handle in scene coords, as requested by the handle. newState = self.stateCopy() h = self.handles[pt] - #p0 = self.mapToScene(h['item'].pos()) - ## p0 is current (before move) position of handle in scene coords p0 = self.mapToScene(h['pos'] * self.state['size']) p1 = Point(pos) @@ -410,15 +442,10 @@ class ROI(GraphicsObject): if h.has_key('center'): c = h['center'] cs = c * self.state['size'] - #lpOrig = h['pos'] - - #lp0 = self.mapFromScene(p0) - cs - #lp1 = self.mapFromScene(p1) - cs lp0 = self.mapFromParent(p0) - cs lp1 = self.mapFromParent(p1) - cs if h['type'] == 't': - #p0 = Point(self.mapToScene(h['item'].pos())) - #p1 = Point(pos + self.mapToScene(self.pressHandlePos) - self.mapToScene(self.pressPos)) snap = True if (modifiers & QtCore.Qt.ControlModifier) else None #if self.translateSnap or (): #snap = Point(self.snapSize, self.snapSize) @@ -426,14 +453,10 @@ class ROI(GraphicsObject): elif h['type'] == 'f': h['item'].setPos(self.mapFromScene(pos)) - #self.emit(QtCore.SIGNAL('regionChanged'), self) - self.sigRegionChanged.emit(self) + self.freeHandleMoved = True + #self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged() elif h['type'] == 's': - #c = h['center'] - #cs = c * self.state['size'] - #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs - ## If a handle and its center have the same x or y value, we can't scale across that axis. if h['center'][0] == h['pos'][0]: lp1[0] = 0 @@ -485,13 +508,12 @@ class ROI(GraphicsObject): return self.setPos(newState['pos'], update=False) - self.prepareGeometryChange() - self.state = newState - - ## move handles to their new locations - self.updateHandles() + self.setSize(newState['size'], update=False) elif h['type'] in ['r', 'rf']: + if h['type'] == 'rf': + self.freeHandleMoved = True + if not self.rotateAllowed: return ## If the handle is directly over its center point, we can't compute an angle. @@ -507,10 +529,9 @@ class ROI(GraphicsObject): ## create rotation transform tr = QtGui.QTransform() - #tr.rotate(-ang * 180. / np.pi) tr.rotate(ang) - ## mvoe ROI so that center point remains stationary after rotate + ## move ROI so that center point remains stationary after rotate cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) newState['angle'] = ang newState['pos'] = newState['pos'] + cc @@ -520,60 +541,22 @@ class ROI(GraphicsObject): r = self.stateRect(newState) if not self.maxBounds.contains(r): return - self.setTransform(tr) + #self.setTransform(tr) self.setPos(newState['pos'], update=False) - self.state = newState + self.setAngle(ang, update=False) + #self.state = newState ## If this is a free-rotate handle, its distance from the center may change. if h['type'] == 'rf': h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle - - #elif h['type'] == 'rf': - ### If the handle is directly over its center point, we can't compute an angle. - #if lp1.length() == 0 or lp0.length() == 0: - #return - - ### determine new rotation angle, constrained if necessary - #pos = Point(pos) - #ang = newState['angle'] + lp0.angle(lp1) - #if ang is None: - ##h['item'].setPos(self.mapFromScene(Point(pos[0], 0.0))) ## changes ROI coordinates of handle - #h['item'].setPos(self.mapFromScene(pos)) - #return - #if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - #ang = round(ang / (np.pi/12.)) * (np.pi/12.) - - - #tr = QtGui.QTransform() - #tr.rotate(-ang * 180. / np.pi) - - #cc = self.mapToParent(cs) - (tr.map(cs) + self.state['pos']) - #newState['angle'] = ang - #newState['pos'] = newState['pos'] + cc - #if self.maxBounds is not None: - #r = self.stateRect(newState) - #if not self.maxBounds.contains(r): - #return - #self.setTransform(tr) - #self.setPos(newState['pos'], update=False) - #self.state = newState - - #h['item'].setPos(self.mapFromScene(pos)) ## changes ROI coordinates of handle - ##self.emit(QtCore.SIGNAL('regionChanged'), self) - elif h['type'] == 'sr': - #newState = self.stateCopy() if h['center'][0] == h['pos'][0]: scaleAxis = 1 else: scaleAxis = 0 - #c = h['center'] - #cs = c * self.state['size'] - #p0 = Point(h['item'].pos()) - cs - #p1 = (self.mapFromScene(ev.scenePos()) + self.pressHandlePos - self.pressPos) - cs if lp1.length() == 0 or lp0.length() == 0: return @@ -586,14 +569,14 @@ class ROI(GraphicsObject): hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) newState['size'][scaleAxis] = lp1.length() / hs - if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): + #if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): + if self.scaleSnap: ## use CTRL only for angular snap here. newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize if newState['size'][scaleAxis] == 0: newState['size'][scaleAxis] = 1 c1 = c * newState['size'] tr = QtGui.QTransform() - #tr.rotate(-ang * 180. / np.pi) tr.rotate(ang) cc = self.mapToParent(cs) - (tr.map(c1) + self.state['pos']) @@ -603,94 +586,48 @@ class ROI(GraphicsObject): r = self.stateRect(newState) if not self.maxBounds.contains(r): return - self.setTransform(tr) - self.setPos(newState['pos'], update=False) - self.prepareGeometryChange() - self.state = newState + #self.setTransform(tr) + #self.setPos(newState['pos'], update=False) + #self.prepareGeometryChange() + #self.state = newState + self.setState(newState, update=False) - self.updateHandles() - - self.handleChange() + self.stateChanged(finish=finish) - def handleChange(self): - """The state of the ROI has changed; redraw if needed.""" - #print "handleChange() called." + def stateChanged(self, finish=True): + """Process changes to the state of the ROI. + If there are any changes, then the positions of handles are updated accordingly + and sigRegionChanged is emitted. If finish is True, then + sigRegionChangeFinished will also be emitted.""" + changed = False - #print "self.lastState:", self.lastState if self.lastState is None: changed = True else: for k in self.state.keys(): - #print k, self.state[k], self.lastState[k] if self.state[k] != self.lastState[k]: - #print "state %s has changed; emit signal" % k changed = True self.lastState = self.stateCopy() - #print "changed =", changed + if changed: - #print "handle changed." + ## Move all handles to match the current configuration of the ROI + for h in self.handles: + if h['item'] in self.childItems(): + p = h['pos'] + h['item'].setPos(h['pos'] * self.state['size']) + self.update() - #self.emit(QtCore.SIGNAL('regionChanged'), self) + self.sigRegionChanged.emit(self) + elif self.freeHandleMoved: self.sigRegionChanged.emit(self) - - def scale(self, s, center=[0,0]): - c = self.mapToScene(Point(center) * self.state['size']) - self.prepareGeometryChange() - self.state['size'] = self.state['size'] * s - c1 = self.mapToScene(Point(center) * self.state['size']) - self.state['pos'] = self.state['pos'] + c - c1 - self.setPos(self.state['pos']) - self.updateHandles() - - - def translate(self, *args, **kargs): - """accepts either (x, y, snap) or ([x,y], snap) as arguments - - snap can be: - None (default): use self.translateSnap and self.snapSize to determine whether/how to snap - False: do no snap - Point(w,h) snap to rectangular grid with spacing (w,h) - True: snap using self.snapSize (and ignoring self.translateSnap) - """ - - if len(args) == 1: - pt = args[0] - else: - pt = args + self.freeHandleMoved = False - newState = self.stateCopy() - newState['pos'] = newState['pos'] + pt - - ## snap position - #snap = kargs.get('snap', None) - #if (snap is not False) and not (snap is None and self.translateSnap is False): - - snap = kargs.get('snap', None) - if snap is None: - snap = self.translateSnap - if snap is not False: - newState['pos'] = self.getSnapPosition(newState['pos'], snap=snap) - - #d = ev.scenePos() - self.mapToScene(self.pressPos) - if self.maxBounds is not None: - r = self.stateRect(newState) - #r0 = self.sceneTransform().mapRect(self.boundingRect()) - d = Point(0,0) - if self.maxBounds.left() > r.left(): - d[0] = self.maxBounds.left() - r.left() - elif self.maxBounds.right() < r.right(): - d[0] = self.maxBounds.right() - r.right() - if self.maxBounds.top() > r.top(): - d[1] = self.maxBounds.top() - r.top() - elif self.maxBounds.bottom() < r.bottom(): - d[1] = self.maxBounds.bottom() - r.bottom() - newState['pos'] += d - - self.state['pos'] = newState['pos'] - self.setPos(self.state['pos']) - #if 'update' not in kargs or kargs['update'] is True: - self.handleChange() + if finish: + self.stateChangeFinished() + + def stateChangeFinished(self): + self.sigRegionChangeFinished.emit(self) def stateRect(self, state): r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) @@ -950,166 +887,6 @@ class ROI(GraphicsObject): self.setState(st) -#class Handle(QtGui.QGraphicsItem): - - #types = { ## defines number of sides, start angle for each handle type - #'t': (4, np.pi/4), - #'f': (4, np.pi/4), - #'s': (4, 0), - #'r': (12, 0), - #'sr': (12, 0), - #'rf': (12, 0), - #} - - #def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None): - ##print " create item with parent", parent - #self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) - #QtGui.QGraphicsItem.__init__(self, parent) - #self.setFlags(self.flags() | self.ItemIgnoresTransformations | self.ItemSendsScenePositionChanges) - #self.setZValue(11) - #self.roi = [] - #self.radius = radius - #self.typ = typ - #self.pen = fn.mkPen(pen) - #self.currentPen = self.pen - #self.pen.setWidth(0) - #self.pen.setCosmetic(True) - #self.isMoving = False - #self.sides, self.startAng = self.types[typ] - #self.buildPath() - #self.updateShape() - - #def connectROI(self, roi, i): - #self.roi.append((roi, i)) - - ##def boundingRect(self): - ##return self.bounds - - - #def hoverEvent(self, ev): - #if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): - #self.currentPen = fn.mkPen(255, 0,0) - #else: - #self.currentPen = self.pen - #self.update() - - - - #def mouseClickEvent(self, ev): - ### right-click cancels drag - #if ev.button() == QtCore.Qt.RightButton and self.isMoving: - #self.isMoving = False ## prevents any further motion - #for r in self.roi: - #r[0].cancelMove() - #ev.accept() - - - #def mouseDragEvent(self, ev): - #if ev.button() != QtCore.Qt.LeftButton: - #return - #ev.accept() - - ### Inform ROIs that a drag is happening - ### note: the ROI is informed that the handle has moved using ROI.movePoint - ### this is for other (more nefarious) purposes. - #for r in self.roi: - #r[0].pointDragEvent(r[1], ev) - - #if ev.isFinish(): - #self.isMoving = False - #elif ev.isStart(): - #self.isMoving = True - #self.cursorOffset = self.scenePos() - ev.buttonDownScenePos() - - #if self.isMoving: ## note: isMoving may become False in mid-drag due to right-click. - #pos = ev.scenePos() + self.cursorOffset - #self.movePoint(pos, ev.modifiers()) - - - - #def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier()): - #for r in self.roi: - #if not r[0].checkPointMove(r[1], pos, modifiers): - #return - ##print "point moved; inform %d ROIs" % len(self.roi) - ## A handle can be used by multiple ROIs; tell each to update its handle position - #for r in self.roi: - #r[0].movePoint(r[1], pos, modifiers) - - #def buildPath(self): - #size = self.radius - #self.path = QtGui.QPainterPath() - #ang = self.startAng - #dt = 2*np.pi / self.sides - #for i in range(0, self.sides+1): - #x = size * cos(ang) - #y = size * sin(ang) - #ang += dt - #if i == 0: - #self.path.moveTo(x, y) - #else: - #self.path.lineTo(x, y) - - #def paint(self, p, opt, widget): - #### determine rotation of transform - ##m = self.sceneTransform() - ###mi = m.inverted()[0] - ##v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0)) - ##va = np.arctan2(v.y(), v.x()) - - #### Determine length of unit vector in painter's coords - ###size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0)) - ###size = (size.x()*size.x() + size.y() * size.y()) ** 0.5 - ##size = self.radius - - ##bounds = QtCore.QRectF(-size, -size, size*2, size*2) - ##if bounds != self.bounds: - ##self.bounds = bounds - ##self.prepareGeometryChange() - #p.setRenderHints(p.Antialiasing, True) - #p.setPen(self.currentPen) - - ##p.rotate(va * 180. / 3.1415926) - ##p.drawPath(self.path) - #p.drawPath(self.shape()) - - ##ang = self.startAng + va - ##dt = 2*np.pi / self.sides - ##for i in range(0, self.sides): - ##x1 = size * cos(ang) - ##y1 = size * sin(ang) - ##x2 = size * cos(ang+dt) - ##y2 = size * sin(ang+dt) - ##ang += dt - ##p.drawLine(Point(x1, y1), Point(x2, y2)) - - #def shape(self): - #return self._shape - - #def boundingRect(self): - #return self.shape().boundingRect() - - #def updateShape(self): - ### determine rotation of transform - #m = self.sceneTransform() - ##mi = m.inverted()[0] - #v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0)) - #va = np.arctan2(v.y(), v.x()) - - #tr = QtGui.QTransform() - #tr.rotate(va * 180. / 3.1415926) - ##tr.scale(self.radius, self.radius) - #self._shape = tr.map(self.path) - #self.prepareGeometryChange() - - - - #def itemChange(self, change, value): - #ret = QtGui.QGraphicsItem.itemChange(self, change, value) - #if change == self.ItemScenePositionHasChanged: - #self.updateShape() - #return ret - class Handle(UIGraphicsItem): types = { ## defines number of sides, start angle for each handle type @@ -1160,8 +937,9 @@ class Handle(UIGraphicsItem): ## right-click cancels drag if ev.button() == QtCore.Qt.RightButton and self.isMoving: self.isMoving = False ## prevents any further motion - for r in self.roi: - r[0].cancelMove() + self.movePoint(self.startPos, finish=True) + #for r in self.roi: + #r[0].cancelMove() ev.accept() @@ -1173,29 +951,31 @@ class Handle(UIGraphicsItem): ## Inform ROIs that a drag is happening ## note: the ROI is informed that the handle has moved using ROI.movePoint ## this is for other (more nefarious) purposes. - for r in self.roi: - r[0].pointDragEvent(r[1], ev) + #for r in self.roi: + #r[0].pointDragEvent(r[1], ev) if ev.isFinish(): + if self.isMoving: + for r in self.roi: + r[0].stateChangeFinished() self.isMoving = False elif ev.isStart(): self.isMoving = True + self.startPos = self.scenePos() self.cursorOffset = self.scenePos() - ev.buttonDownScenePos() if self.isMoving: ## note: isMoving may become False in mid-drag due to right-click. pos = ev.scenePos() + self.cursorOffset - self.movePoint(pos, ev.modifiers()) + self.movePoint(pos, ev.modifiers(), finish=False) - - - def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier()): + def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True): for r in self.roi: if not r[0].checkPointMove(r[1], pos, modifiers): return #print "point moved; inform %d ROIs" % len(self.roi) # A handle can be used by multiple ROIs; tell each to update its handle position for r in self.roi: - r[0].movePoint(r[1], pos, modifiers) + r[0].movePoint(r[1], pos, modifiers, finish=finish) def buildPath(self): size = self.radius @@ -1629,8 +1409,8 @@ class SpiralROI(ROI): for h in self.handles: h['pos'] = h['item'].pos()/self.state['size'][0] - def handleChange(self): - ROI.handleChange(self) + def stateChanged(self): + ROI.stateChanged(self) if len(self.handles) > 1: self.path = QtGui.QPainterPath() h0 = Point(self.handles[0]['item'].pos()).length() diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 10d3e347..f62269a4 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -206,7 +206,10 @@ class ViewBox(GraphicsWidget): #print "addItem:", item, item.boundingRect() def removeItem(self, item): - self.addedItems.remove(item) + try: + self.addedItems.remove(item) + except: + pass self.scene().removeItem(item) self.updateAutoRange() diff --git a/imageview/ImageView.py b/imageview/ImageView.py index 2e04c82b..3ff75d9c 100644 --- a/imageview/ImageView.py +++ b/imageview/ImageView.py @@ -23,9 +23,9 @@ from pyqtgraph.graphicsItems.ViewBox import * from pyqtgraph.Qt import QtCore, QtGui import sys #from numpy import ndarray -import ptime +import pyqtgraph.ptime as ptime import numpy as np -import debug +import pyqtgraph.debug as debug from pyqtgraph.SignalProxy import SignalProxy diff --git a/widgets/GraphicsView.py b/widgets/GraphicsView.py index 899db420..831ac12f 100644 --- a/widgets/GraphicsView.py +++ b/widgets/GraphicsView.py @@ -11,7 +11,7 @@ from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL, QtSvg from pyqtgraph.Point import Point #from vector import * import sys, os -import debug +#import debug from FileDialog import FileDialog from pyqtgraph.GraphicsScene import GraphicsScene import numpy as np From 4c525ffa06c6889c4a91ac1583bae063d5b4ca61 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 1 Mar 2012 22:21:18 -0500 Subject: [PATCH 003/238] removed pyc files, fixed import bug --- GraphicsScene.pyc | Bin 27489 -> 0 bytes Point.pyc | Bin 6601 -> 0 bytes Qt.py | 1 + Qt.pyc | Bin 367 -> 0 bytes SignalProxy.pyc | Bin 3604 -> 0 bytes Transform.pyc | Bin 9368 -> 0 bytes WidgetGroup.pyc | Bin 9737 -> 0 bytes __init__.pyc | Bin 3585 -> 0 bytes debug.pyc | Bin 29902 -> 0 bytes functions.pyc | Bin 19701 -> 0 bytes graphicsItems/GraphicsItemMethods.py | 4 ++-- graphicsItems/PlotItem/PlotItem.py | 1 + graphicsWindows.pyc | Bin 4242 -> 0 bytes ptime.pyc | Bin 1273 -> 0 bytes 14 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 GraphicsScene.pyc delete mode 100644 Point.pyc delete mode 100644 Qt.pyc delete mode 100644 SignalProxy.pyc delete mode 100644 Transform.pyc delete mode 100644 WidgetGroup.pyc delete mode 100644 __init__.pyc delete mode 100644 debug.pyc delete mode 100644 functions.pyc delete mode 100644 graphicsWindows.pyc delete mode 100644 ptime.pyc diff --git a/GraphicsScene.pyc b/GraphicsScene.pyc deleted file mode 100644 index 35431aa08b042fbceb3feb069a506181b5f0638b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27489 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 diff --git a/Qt.py b/Qt.py index e1d4b28a..eb63b7c1 100644 --- a/Qt.py +++ b/Qt.py @@ -1,5 +1,6 @@ ## Do all Qt imports from here to allow easier PyQt / PySide compatibility +#from PySide import QtGui, QtCore, QtOpenGL, QtSvg from PyQt4 import QtGui, QtCore, QtOpenGL, QtSvg if not hasattr(QtCore, 'Signal'): QtCore.Signal = QtCore.pyqtSignal diff --git a/Qt.pyc b/Qt.pyc deleted file mode 100644 index 0a46429d8504f92f1ef55df29f6c149de83342e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 367 zcmYjM!A`?440Y0OLz58yz^SL^vLiy1K-vWj+KNjrQ`SapSkiWl3-ruya^w$?xPntA zfn`5G%g>Gze4a1AKHj$loGhTcrf|0eB{>5FU>W2LEQg$f4ImG|hLDHA)C?4dlEWJZ zxAy#L?s2z(hK2*kqL3EpZ?cKujLHNzgU0#9ZrWbq*+lO<)okugitM?X6q&9XX||D< zOs$78!Bt8Xq|5{N;LpYdK`os`>~TrF8;|%qp86vqihtl9sT6fuXoq$*opH*ISfch8_DrTgj0kF!~_#5Y~nE}V1!2Fncm%A&nweC zUK{yF{s0GV{41^;a^!@B_yO=$&8!oUkT}GQ)3yDmuCDs((VgZWtG%C&e;b8Ve{KAJ zgl1lXMEK`WOq4jZ?~ude2E`3ZnzY}lq zVS|caL|R*S6+91`WS&P(?*w$tgp)as|(O}b~ zrl<_H1S=R1kl30PirUiYleahMu;fi>_T*ruLhH__I-I&;7RMMFw+M0_-5%>gxkhv| zZjeS;Pa~BYWn4Q|NzcWnQ=7Q~xY9|yVBT`SG%DJ3$64X3;~>dnwdsN_FVvArE!g*R zoj%BpMZ&nq5_h6fRRk7N+@y@H#+I{_=TnvPfo_tOMnP4jVpl^+);1Cu*^w$-ZMS3~ zmzztPl1D(3LDs&dP+1O<-Re~pX z<@@=5$ll}DK-*?0e+02aGel~GP9^S~Qw*9sfV()rFvh@NqElh<#dkc@k*!^hJs$RC zPme>k-rtw~c2$-y(5XoE*SG`sA9ktuLH4Z6hNf_H96E?YR?++p@+j3-2eJMRFlVV7 z6q7Q6H_Sc3i+x$S+Pw`J4zd#duHw=KF!{-ITaR>VY>3M8fUO#$^QJWyrvg(!?p???tRguf7hmo+(RR?x0}#$hqGDAtq*h&$ z6UmE|LmoQJfPC!GYqumsdHGPw8llFM6#rJu|R0E2EU+rtt zY*{=2m)6y56Cxj>5Kk50L*Wk=mFjt^N`P19o2WSe7^No7=3Apd7dtB4q$e=0@f-)Y zz{qt+K=rOHroJf3>T;G)Jr@+Y!rVbK>`rQ2cUp~&f5Gty7;vL7Fbfez(rRu7k28lw|<{BfVf=r^da?5^>yl)db52kJE2J7u6Qs|E4V` z!cmbb>Lym-i*yM+3h7#JXKu#S{AjFOWoejA$KiK^!CA zSfs~DcyBRrK`dwY?#Fo^>rftBsCz^~p37r@_j5SR}n$5kpvFg18BF7yCajCYaK{`>9$Lh)*k@J5k&Ml}#?GLIf h{E#6qH$m`dXmlGl9{2HhSabULTXlMv>9*Dze*t4SB(4Ae diff --git a/Transform.pyc b/Transform.pyc deleted file mode 100644 index 2bb1ce46bb5ab0d7334093a3c975e466b7cab671..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9368 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 F{U0*kgaiNp diff --git a/WidgetGroup.pyc b/WidgetGroup.pyc deleted file mode 100644 index 1743d3041eb7cf343cd14cd0a04c7dc72b48e741..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9737 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 i$cNQnob)}{%9weOePsSZ8Mqa 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 Ow|Ag-aQ~66;Qs(Npy)yX diff --git a/functions.pyc b/functions.pyc deleted file mode 100644 index b71083c09a1b1c4693941ac288a630f9e2a49166..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19701 zcmcJ1Yiu1!c3$-*MT#6rlqf#L(L8#j(eRQ(UWt@uywPK4G}L36%xGqsGsB5AT21qI z^RmhN((S$yxhp1{m4eNWEaJqkyqq9`9osWif+Prn zAdZoZff(6*-&fsz@1->MVxg7T#p-&TI#qSfsdG-9X5a509RA1u#eKJI(tktv{U)Bc z?HCgnbJLh;zX@J3L7#a&m^MK_eicoy55HziFo0jPCK$x8S52@VzvfIZgkSR}7&fop zbWHG!xmhs50dup@g#E_fXM%$!IAp2=rZ#A94w~A2b929OSUO^=L+0kNS|E>}2eSM@ za}y10n?vU2p)5br)#k9ddDsMpIZSZGJSv!*M@%qkf}<)K$*RW8qXBdCs0qf+BTR72 z1QW`Pn_yCz2@@PsX3_*x+0V1)QJ=Ya%ml|(h4SZ=dDaBao8Sc#V3IGIz%>Epj68l` zGLJB~<0d$vmd|m5{5dJy(CcNDqwgtY(0#E5N za#*eU^{~;7-MrSlA0-=Zz2P>Jjj-izG-~1SQlq)kiYgn4yLe{djO)JL-U{6%zt;3C z^+wenz7oYrD_UzOVc@pw0lK}j{K4su7Z%)VR1WKLSaMgwFn>w56L-DQa%+uN=tlMR zM$IQ;rQry8n&|sT5)h18Q(95lQd(A8Q@W#cOX>agDBACQ$zN~Qzx|DxyY0@ACmWz>+L8RP;Ln*-Kczb_jmsHzxiMP;(WOeg{&CFvxFzU z0K5`o9tgwDf_Ydlt(Q#F$C8JR*)^sPJYF$LzuDZE7RGPsbs&9hT_XXntk$QD<2dMd zJsC`^u7BaH0N|L9ll^JQ1IKI`(|SaX+3iz`7H^tlNHB+$KBM%2$$J44A^>{tCsyNl zz+>RAhke2l=nB>ru>UTpYq#IjZ>VsP05Z(0QhJjnLLW^95DPniekH{~P; z(^ivHEIwqCL)``7RrET#i)Fx1zgaJU91n64e;>)Pi+^62v|Du-gyy!wa-&uYK|FyQ zm;Gw!uKVQ#MB>*2cV*daw!-yj+a;aVE25t5l3*ygx(#BihUd?BiU>cBRtTzirgY}a znS~jYoWwvJGj@v$;p=nm^lyJ_dPbet{-pcCMihf2gQ)B$VGPXtMCjGqwKb5n1>~a5 zS{NsQ1+?O@PP+HwtajD~X!nx4aORBr+u!=I9wo6xh)B~Q9+0?_{|f4=FlWq(-^em= zMt=a}gp|ZOk@~_@LB!vcKm4WN`*-iGz4wObR-n9Llov;kB+Nx|RF4zCUJgM|5Vv7D zs`*vrucUcW`+BwECkcta-W2&qaow+{IWQv#2LN}PA&A--e=XMUsGg{zT#almfN8W; zRc%y~XV7$I`ObUZ)oaTiUtLMac3tmxF0U*tFBi#4V%KePTP>=81OyLom}<3gA1hUE z1feGcm~hY4zFEYZQ4(K5?sPkDovt>px2LPo+Ue#_veBq7mVWGX06{e0^vhd*C5%yi zH>tGz=Emt-3{xNs2+ym?Rq28abORkob`VY zZoej&DZ9_PaeEyy;(9jf-4UyVcWMV6D%s*ob2 z`5TS8l+KEj<~_^g1e23YUPdBlVXM_>X)NzKR=mLEdDas6dM&Z2R@*VS zt{p#3e)7ma*YLzd&J2-Z@bn#XPB;g_K4Z=}7-!5WQ~Yr~iO~|C_}7rw!xZF-qZ3Py zBNa>oq$j~9lb~B1u)B^3?itbeRn8a8fT{`f`l>?~AYr2Kpm_+n*Kei>4$PGo?K4wg zInw+9IjHp&w&IypR3Xkh6#f)dP8J^J_?zGr9hM;XV<;uDgXH_o(Xo{xMOOSXNJK$P zjrzT?mAJlp&#$&a_kJq^UDrw1px&IElZK0vzzy%VBecVIr)OFGl8LI~l=5V{nz8WsJM+NEX zo+w_W*i`&e@)QmWPB4zR>^^hEmL&cxNc^ZX;T-Nu$-QvYDYK4@&b7RbC;q=!1Cleu zuhhc{kTP(zFCcJ|-52}pIY0qVx`W1s(q+*g$PNHd0fJuXY6fA8Hh)fZZySJ~$Iz%B zWZw@`2f>oU_nEuj>;?}8LBB_Z6zVS6X26~-`-ICk^k60ZR^3F;(QXf`Z}l#3DbZ2 zbl|7MVoF`uVAZ+@9M*tig?vJ2`qJL{4DwjaO7rOqQD$VNPn->e8WtAAWvlf=15Y8~ zFn=Z=QZpe5NChAv;ix7Fys8DwX)6_rznKxnC!nBge^WZLE7Iv?!{iQ|~X3u)7QPYXiio#^aZLGU~tI~#n1bYO^d0cWY zmnkY~d88FFa$%PRbZO5&chCDD`zoy6>GCvfqx>Hf6rgk7ZOyrrId^T&Ezh~NId^By z-I{an=goRD7LqsTUY~Q{nsaYmzRhYF6=5Z8!El;ieC;*crr6yG=UdW-UB~G^sj+OC zu7N78a7V`D9Br}rh0+-d6n(o|!7hX|;yi7F^Pi$h3tpUYFXr|_DJ%W_rzut_m~%JU zVN4_PhX8QGyZ+XRE*v((Z9lcpQkyou;MV*d*p<+Pt?D~{a@vm0&X>D-XQkft>u=>y zdkQb7&23>yQu*-dduA?j;4K)tK2H4Rj@7gG(#8)*JMk+x1#dJGXyWknY&7muf8bym zD<|_4r{WW`Xsu3>`+rW#){3ZkN3^jb9;{M1WFJefxN`@p)SWwJDgk_O;+hpa8X5=h zW_H}hFYp|Oeo0<+OzQ+s<`ulG$fCZXv5V~P(WLe`phr8lNU+i@{HLl}><8G3Rll|t z_@Dm{@-ZGGk>SlC=S?$_($AIYTiax+0P5-878X7mePM}~oh_E1}*uNxge9(NI8k(&bq&>{KpJ%%jJOE1;_zjwE zo5N=F8KQ9)FyX?o^xZui==(o)=$p2*?Y_@}&IeuE{=hqJ`_gbggCWy$jpJ zXV4KbM{yB08fYm^qml%ytmyw?X^+d{*l$gJaN?ka@N6a~q~rNzf<<64+RT+FMh+m#o)%1Dy#Ix&gii%&~>6Q$cUb?pPgNX?0CElOJ7KkCW8rD@#LqAS- zs-YZV<%V4kvF}EFPX~7NuhHg4k>|9^D`6sMf_Dlfog-APH8}MX4WYHN5`?_Ros|6W zDF4;sMuTM(8yXs;Epf@cYb>Eii9@K3DLxcMcwZr=a(H->GQ@t1N2=!Ly^UAz9VR!J zkm0>$BtjeO|1@Qg$20qP@x<{59igTSUm(&LO$U1vz~0ITC$iP_KW9Mq&dYW(dl|v4_mlVas3|q z<_jwyrB1WBWaTs0(z}F2LL965h4|jJj>l)idyNfVXYwP_NW2dLdKi)*roLscG85Ku zNll1Lp(Ws6L6$;LV+okSA#F2|LN)_I1ix!PgbRCW(NZtCpV~CAbgs|6HFtR?v-PZY zr;}bp(Fy|*BdnU#IlKn~Py7>e6Jpy_plYr%8X6v~_lpFTWGc;Z7y z0_Z=QG6d>Gs;6*$y0#p~PIhs}3!dKj%JX>Y%;L7R`Xai51Kk04LBlk?TQS#>*na#7@&<$?&A6tN51H9 z7bo8c71IYe7omGda;pJfJ>3Ar5{q)rZ|&F>V4lwZ;+g^_Fp+pB zRS(|U0aELJy9yUV!u#botcGP=1*ATnY^DB$AeTgN5_z)mbWT~3NRebkqP-aklgp9= zGF2(*b<7bxS%N|+R@0il{Snc#FS>Wd3*n*W3t2X*GfVD<-RRxHZ|`G5WeN64(D8dC z%iObq_!b+o4cY!_Io`d?fHg&4)UDF~12~q(aBxwJejb;x6A;Zuaafhfhez?yloUVy zLrANfADMwDhKbc@TGM0^uoGU>d!vk1*-R=So83?|v9N8n{ThK>xjk>-7iUvhdOdwU zR3D8*U*?DWS%or=wIkDEOsJ)h4PaRu7wQkSd;2(ty>k1gR95J*U{=eYcmEN(LyZ+u z%)oVzD_!2pqCZk-_4dvT13-*uWH|mg9$vfUk3g);pdiYc{@5g%+(Fz3nk#Mu=RIn! z+#c5jG`yTZR2u&m3+C?U9E>$(MHhP@UTT2h98(w;xIDyqj~I+#>zL_`Gc^Wv8%}Mw zkslhzYUUXG$C$%RfCAbI#TOwF3E>EJE{i6F+$2l`HXOx#9Z1UUD{6F<0uV;QmD^`2 z5bgL7bdrN(W^>$ZPOu*&;1nhTQTcVKCJ?KK%*DI^%~|C}9LP3egkc!Fht2jXHz=7* z5yGUe++H%fN4VOX$IOExT=RTxIpW7SD5WcQ?+}|f4-T8nsqE$MazR@Ot7F7WqI!2! zV`CFJsm%QNSrD)U(FfK)7Bh5;VvE{!176^IdM%zy+d9N(%ik%bK`XJ_?#SkFJ82<) zhO#M!tYO#}VbvgnqWK_#ADK7Se$|42NNPjoB824~Z`noI2%;w>;_pS_eM!F9UAO`) z+{G(y;3xhbaZX&-Y@2RGxcYoVg8_lo&IS4oApdY}2sa?E_yf8CA;b|pveBpr1r$)k zV~%ir0woT>vBG;0zo-m-;3hF&a+ebZ-!vhH76}g=NA=K$ z%NlV;h>@wZt9-#AO}_=#1O#$3Y67`WjR?<&Fs z@j_J`kvPNW(i@otLL&^2PWxY{w;F`K`QdJ8OPagwv{X8`K3&XeJ6fY6~G z3rDomLg)&iQ%27>TJ)9?d|c4o8n&jBlw#VtVH_Yo1HP*UGL zcg0<&U=d>jHqf>*E{J@IfO&Wm>V7MQ-Dpc);}TVUEc{vbv-U^;FIgF82LqWRv@d~x zn{&5yDfOipgyIzu@-ye2%byO-&O&;IF2@(wHrrGwfdHjU*n(XSuK|TcHvN<$iraHL zb1CS#g*n6)<_lpVrc~TRYMBBHqVMWYtoYN7(<-7!sO{Rt_7Qfwy?7?YQd*;ABBv=5 z$4y*xPXgG5@(oF3x!uls5x^I_pbJq>8Y0Uo@)A|4ugAULXjh@VHyT^utgS4hvWJFz zgjLnU6%s;`TL9@sv3aS8p)eBVDnSX=Avo6!4N^tQOt-F1UIJP`OdQE>;IrjHsRVzZ|V45yJTBgL_s088Ij0e z(muD&+_pOtQ@ynd%b3)(x=S|5Lv9(oB!fSt0k#&ab;L+dQ^M7LT9Ek!yBbl>5QH@j zEzWcq=UP*xC;Y6q`ncz3_R9IfaVSiBS%ts(Q6>6ovu>IZ?mX7Qh6(z09+X}YR5J_u6rJ5uCBhoKcNc0*N0;Acs0kW7u z>}_#CI!$o-#`z{K8_FJ~Z;MaUQj$NiizOFnG`)#Y&Ga$&`uH3~dbBXw$J`Nkv_=L- zQ0n%bE?jYj`;Q_}Zvxj6FXNhQ02gK_)#`9xnW&HB;mR-JiN6aS6>2ZsUA&-xbwfMD zVQ3u+^N8|gy(-*g3g2^V4VqSkiZzS|sJk?eDyIHI)(0`Iu(@ml89CPGf&qXpDd@BR z9*qG)SM+P^7xW6~`w`9Zs-S;I&}9)3`ga7~_CW|cpxXw+>G1UWqjr#X2Fw>W_?{rZ z{*XroRX}tiMfQ8Vt)m~tqVv#UH+Ww%8w(kjc=%L>mntWnyAPeTWZncNQ$dv1fWg-b zJxlbF!}UNcPG*UAru!9D!DVBY>MI1u#Gb>u%oCNvFxC*qh)a|s za?obD$G_Fp`akk!rG*r;f%&3Kam2Iq%=K6Lcm6G##RCPRAaf-}L06R615Wy*0+`bp zaa<>C41WQOW8=7qVx4^jx-_6+2w_3xyyA@y#@xtO$8PaZACZFD1*(GaY}XgyU|CM$ z`x{nTa}XBvw3EOV%$s0{E4M$D5i68X1M?3k$y~KBhxKxdUq$5Tv5_r*B9grD8Y3it zxp?>Q7FO%OoYAknO#Ag7l=mX}RYN2-8&EN$`L5jFd|F86h)v~d+KcY>zE$ojF6N9` zz56Tu`09jP{~!9u%J^VuTy&I-8y_x4kvh4}ZtZeE(axFgrG6|Z*MA~S!4&xU^NK2; z!VTTsukCGNTX))F{=CWiJU6We(ikJtS=Tcn;Nfr^OPg{RQzErIc^X6p${Pc);Ebia z1<{=`nCZuyj%Aj~J0NjBmh{!yz^tgtzexBooP8ZA6;3~R|fg(Qf zB8S77VP9ASpD>!GHDG7^!Wysvycf2HIrMqajKkXzJWJO`;+oBu%;pJtH}EwYzC%mD zK(n8py~OX$@QoS1E~`L(79ei`z+x)D7Ud1+W7lZ)?$-uV@_KXNGsp|e0M8y46m37Q zzphE=vKaFIYXjZOjIY_S#PGhbFmrl!w)GWlYiF5(O}}x2v$>#P7l$5~ZZiH|ZvK;q z%F@cTfC3O^hNZ?02c!SGIXLBxHwU-tWNow%Pro2P9zdT1DdJl}hz#_?E=X(~A6%`! zVYV--hlC&(kMf`ZdGK=9HTuWEQ5E_bA{m$END{PFS~CqrcEjqBLx_QTvRUfEX>_vyXm=pm zo7Pr-C6L-Qk6k3#`!pM!(uIuFZFjBFsD_Fw(YIV2=>A3sn-U`-!F>iaYI@J%=lARN z(7K(v{91b)F*JZ=ZsOC)d_fQjk8dfZ4%4ZCl{sxg5*4DETO@ggK=_Z2BLGoHn5#5Y zXRN=+)@P|TB{lKfO^`1OKDFf4jO)u-Yd6ysEYj1IU5B{S)pjyH)9VYc(B)pJ!pBy{ zLSQ{$L56kfP|!TFKCt|;NAQYqNf9W-uNNsiQUtpef&#)6F|2bBYEQu8F(6!- zjMCDTfzPh8=Km4v1M{^-3kw)`EUeT&bC#rgP7}ziZ{QBY9kg12i%P|g2cc`u(-+D@MDSU zuDASvKIzpcfNj~G(`(9QiRVOfSsX(q*)_+=h$5Kz9JT3sYo zK-l#x;XS8OIC;1R^B8dkn}_``VrDb$X}5^+Rs0w*6~qjN%pA3GiQ%b~COA_d$fKBY z&3=Mu15s%|$Ir12qm^U;-&(w#8|{|hVwz3blFVC?$qZTN@o`jh-EH@U$GVT%`W$NJ ze2>qvqMM|iNGNWx0xCU4!oVf1AIvBSV+p$`M*}H1SGE;sS>2 zp@W_mFldot+>I`sx!^_@jRp9h1kYdR|EMz!;O|5@$MIi;^DecgWD zOvi)5LH534^NiTAtAc(O`%2dA)Y>o9RFmGRvt?IlhbpY!jNQ}b$YojLQhXT+RD1Ar zxYn*nG`!bptRn=VWh2d6sFR<`y4&dA5p`Oh8+%HE+sN*Fc-7k0{MMFM^TU;^cUG@n z{^+W{oc3b~!z8M-@rN0tBiN7Ooip0mYT~*Vsg;qT}a85g1NRF8F;AiAQp^@dQy4)<_}V zpUwK*C%*}9ROelrvTzee@kUY5A8`nNGpQOYe1uRF3z?DB+qzW|5>h?Xwz0kr$D{XG z2|$d@U>srV{dMO429v+V2aGc%p)vEyhd%~G19PB^NP zOL*dckK_sdL74RT;9^!6_X?GV$B?1CFhcM;z|Y}n$-u{=;0Mt44(wT!=zHAN`i$uZ zfou(%)-L~K1|$ysZ<)s`Z^%EK0Y3o$?1emyjvWAoI-4A%l{S^;V06JALYyKarEM4a z{OE^*7v&Bj22cu@8!e=Lu#Fh?g)c~q`G9}0qSyKXw=^DoY$5{tV=h`gErvfrpIz0sTA~SKWL?609A0W4w?!wa1y}FP{7}E z5kXsewEhs}ZFKn!odz*iS;unFXwzPf%eEA^E!MtsJxsa@FIHR8(*d zTXlmvMGJPh?Oj zmu=iLFA8)lS?Nq>@P3sEj|uPBn0$i?=}ey;i7*A#!q;#e@%}Bg!}HXF%KkoJ!B1i% zR}KE!2V~*?0m#!)M{&2QN{Uv}Qj~O6e4iC~Y^|;6lA1zzI9p^MAN+z!@kE^i3et@BnVkkQPQ>IevWT b*`arbMu$qHgG2p8{Vz=oeR1U5LsS1B{mDK0 diff --git a/graphicsItems/GraphicsItemMethods.py b/graphicsItems/GraphicsItemMethods.py index 63e95476..49da713b 100644 --- a/graphicsItems/GraphicsItemMethods.py +++ b/graphicsItems/GraphicsItemMethods.py @@ -3,7 +3,7 @@ from pyqtgraph.GraphicsScene import GraphicsScene from pyqtgraph.Point import Point import weakref -class GraphicsItemMethods: +class GraphicsItemMethods(object): """ Class providing useful methods to GraphicsObject and GraphicsWidget. """ @@ -253,4 +253,4 @@ class GraphicsItemMethods: - \ No newline at end of file + diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 2e15f016..c02dbc24 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -24,6 +24,7 @@ from pyqtgraph.widgets.FileDialog import FileDialog import weakref #from types import * import numpy as np +import os #from .. PlotCurveItem import PlotCurveItem #from .. ScatterPlotItem import ScatterPlotItem from .. PlotDataItem import PlotDataItem diff --git a/graphicsWindows.pyc b/graphicsWindows.pyc deleted file mode 100644 index 415b490152399763e69e5d88fec199297f1cb2fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4242 zcmc&%&2Ah;5U!bBukH13?8FchiHQkf6^J(pIRZ-JpeRbR*kg+XOK3FSX|HEJJL62Z zvoSIk94-iuxNze+xbXtKOD;SBd{sTWv-v^XmbJU4|E9aT>ifECDu2z@f4Tgk)u!TC z!Sg1H{sST++JST^aw&1B=hBW#y$bDA$l=}!B~{ukY7@3rrKCo^N!ppTb0#R6qTV#^ zOryO(;S5DJdX9x>DViiTNqbmmihE9pzS^0iXqr@oy7Mwn9}X-~G(&1pdS{2dXDFH@ zwIsds!`@}WPFsydKF6OB^-dlh?#FHYC{ClSuh$Nby(^x#o~2J!8mqLeymk_5t+dz2 zw0*CgtI#Mf^itLL`tlMUX5ZULGV>^oI?8xi?%nH!9rb0b`fK&|?C>~`JNw4Fe)ZZ_ z&wFrmpuF|4cNlikED7s(Vr}wx_sFQoJ4z#!dk^nzHy>ZS<|T1krCP0dTS^TZ=9%&K zutP7)l^3UbSuZqkmaf&~zwv9hIFzYDY?^hbTS@Hjgv z)VHIvBcptdAq;qfbrk&}gdyrW)OFcIfW*NR6ZOO8&2J7RCD!jwy zt;0i{Qchr|&=IHFgs{4)qS4AyTiLv|vC%*Wl|t{L+1$^1s+qu`&3=|2G<#th>mUht zo9!%8&9MFCMl*?bn@1*2nuo_vjCi}Lp27Nw(#^Fs{x1h!2ZNZhcffisU_;Gnk#o*9 z^LP%dXR$N1Pz6J`QS@673V0SiL{;X>a}cFM{+8Gw_AoVm42!n;9bTKC_fhmy2w3+N zyTNJJ!Y4#Qg@AE(aWNv1WmDptXZB!YGXpT&+42*(ah?a zW7yx@;qJcy+QV=GXrOtb1aws48+ND4$mYwZw1FU-LBcz%qv#&QOUMQyN1$6B;Fe)O z!KlPwAn5J_(*SVI!h8tpKzd)r^nU=_7ksljeI~XzW>b#NSGZwq+kKy5YMsRX_Rl~x z_%Qwwi)9vkjh5aUm*ZIRRg@tP7o9o5;w;eEX6?DKs$%o&sKDlIJx93jQoc$!e%zEX5rHn-&26d7fvvkU$J1B<2ZYw~t}(20@`; z5PXEDW?+)D z5EJnTk$@2e998HPSpl-aCpYO)8G^nq#=G^`L(ohaf>_~}5VOH)LK%lFZMkp68q8R^ zMTM_|OvDy}>IHtAPwbZjW+RLcnD0Nyi+ZJ_$4TQG*kAK~G8#@i#|9M9^$ox*RUIoP zJ!ttc${=1a%_P-*C1P;OAduu6vw$Q?h;TVX@Nk<2A8!if3l#kV;>~!pnCc?t0M~$( ziw_21%%y`G!g`=8XvMqqnYP@`AXDl58M@aX#bh$4Sav>%Z1CfWK48(-Q zA03KGhdU?THijbah;KAF68TOmc17M57>LU~v5bVR1$M+`Pkf~@ajQet7>gIQ1V&;K z3LOdDWkKtXl88Ngrx4d(RoJiBa0u|(Vy2uj8n_eYjm|u`Dw!h~cc^rZHB!(X5?7d} z%%PCi2s1>QBbO92X=&^nW^1U|zA*YAJ~j2)szu@9)zR@0z^7)3a4M@>F0?6S{9ZY4 z)vWQDLqn@u7@UpMi{oP`RfgJOA4Z6qhQIMJHx?>GZ&G@0D#gbsojX`5UqC_rq2BOGR>Z=r99*Mi zU@xI@2m`6*0&SmHNDjaoa9$FJQhG96k=YHugMQLsTf1yz?RbXolFmw#EqzRCG=4W6 zPseA24+*uzHn Date: Thu, 1 Mar 2012 22:23:51 -0500 Subject: [PATCH 004/238] added script for rebuilding ui files for pyside --- rebuildUi.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 rebuildUi.py diff --git a/rebuildUi.py b/rebuildUi.py new file mode 100644 index 00000000..78295f4a --- /dev/null +++ b/rebuildUi.py @@ -0,0 +1,16 @@ +import os, sys +## run "python rebuildUi.py pyside" to rebuild all ui files for pyside + +uic = 'pyuic4' +if len(sys.argv) > 1 and sys.argv[1] == 'pyside': + uic = 'pyside-uic' + +for path, sd, files in os.walk('.'): + for f in files: + base, ext = os.path.splitext(f) + if ext != '.ui': + continue + ui = os.path.join(path, f) + py = os.path.join(path, base + '.py') + os.system('%s %s > %s' % (uic, ui, py)) + print py From dc597ac58406ed3a377a46ed76dada077bfe4a53 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 1 Mar 2012 22:53:52 -0500 Subject: [PATCH 005/238] fixes for pyside compatibility --- Qt.py | 8 ++++---- examples/Arrow.py | 2 +- examples/CLIexample.py | 2 +- examples/GradientEditor.py | 2 +- examples/GraphicsLayout.py | 2 +- examples/HistogramLUT.py | 2 +- examples/MultiPlotWidget.py | 2 +- examples/PlotSpeedTest.py | 2 +- examples/PlotWidget.py | 2 +- examples/Plotting.py | 2 +- examples/ScatterPlot.py | 2 +- examples/VideoSpeedTest.py | 2 +- examples/ViewBox.py | 2 +- examples/__main__.py | 8 ++++++-- graphicsItems/GraphicsItemMethods.py | 17 +++++++++++++---- 15 files changed, 35 insertions(+), 22 deletions(-) diff --git a/Qt.py b/Qt.py index eb63b7c1..068662b2 100644 --- a/Qt.py +++ b/Qt.py @@ -1,6 +1,6 @@ ## Do all Qt imports from here to allow easier PyQt / PySide compatibility -#from PySide import QtGui, QtCore, QtOpenGL, QtSvg -from PyQt4 import QtGui, QtCore, QtOpenGL, QtSvg -if not hasattr(QtCore, 'Signal'): - QtCore.Signal = QtCore.pyqtSignal +from PySide import QtGui, QtCore, QtOpenGL, QtSvg +#from PyQt4 import QtGui, QtCore, QtOpenGL, QtSvg +#if not hasattr(QtCore, 'Signal'): +# QtCore.Signal = QtCore.pyqtSignal diff --git a/examples/Arrow.py b/examples/Arrow.py index f9384008..4f7b970a 100755 --- a/examples/Arrow.py +++ b/examples/Arrow.py @@ -4,7 +4,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) import numpy as np -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg diff --git a/examples/CLIexample.py b/examples/CLIexample.py index f2def91a..1957328d 100644 --- a/examples/CLIexample.py +++ b/examples/CLIexample.py @@ -3,7 +3,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg diff --git a/examples/GradientEditor.py b/examples/GradientEditor.py index f22479db..935d5611 100644 --- a/examples/GradientEditor.py +++ b/examples/GradientEditor.py @@ -4,7 +4,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) import numpy as np -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg diff --git a/examples/GraphicsLayout.py b/examples/GraphicsLayout.py index 940d450f..0602dd87 100755 --- a/examples/GraphicsLayout.py +++ b/examples/GraphicsLayout.py @@ -2,7 +2,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import user diff --git a/examples/HistogramLUT.py b/examples/HistogramLUT.py index 114da050..c83b3133 100644 --- a/examples/HistogramLUT.py +++ b/examples/HistogramLUT.py @@ -5,7 +5,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) import numpy as np import scipy.ndimage as ndi -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg diff --git a/examples/MultiPlotWidget.py b/examples/MultiPlotWidget.py index 9e5878a2..2175241d 100644 --- a/examples/MultiPlotWidget.py +++ b/examples/MultiPlotWidget.py @@ -7,7 +7,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) from scipy import random from numpy import linspace -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg from pyqtgraph import MultiPlotWidget try: diff --git a/examples/PlotSpeedTest.py b/examples/PlotSpeedTest.py index 3010270b..866f30d2 100644 --- a/examples/PlotSpeedTest.py +++ b/examples/PlotSpeedTest.py @@ -5,7 +5,7 @@ import sys, os, time sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg diff --git a/examples/PlotWidget.py b/examples/PlotWidget.py index cecbb58e..15d0a036 100644 --- a/examples/PlotWidget.py +++ b/examples/PlotWidget.py @@ -5,7 +5,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg diff --git a/examples/Plotting.py b/examples/Plotting.py index cfeb4786..b4018b7d 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -5,7 +5,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index da9d4750..365b7ed6 100755 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -3,7 +3,7 @@ import sys, os ## Add path to library (just for examples; you do not need this) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index 4a1d5fb4..49d4c715 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -5,7 +5,7 @@ import sys, os, time sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg from pyqtgraph import RawImageWidget diff --git a/examples/ViewBox.py b/examples/ViewBox.py index 6e30a7e6..13c8ce1c 100755 --- a/examples/ViewBox.py +++ b/examples/ViewBox.py @@ -8,7 +8,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) #from scipy import random import numpy as np -from PyQt4 import QtGui, QtCore +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg app = QtGui.QApplication([]) diff --git a/examples/__main__.py b/examples/__main__.py index f8fe4c76..b245beae 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -1,4 +1,8 @@ -from PyQt4 import QtCore, QtGui +import sys, os +## make sure this pyqtgraph is importable before any others +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +from pyqtgraph.Qt import QtCore, QtGui + from exampleLoaderTemplate import Ui_Form import os, sys from collections import OrderedDict @@ -98,4 +102,4 @@ def run(): app.exec_() if __name__ == '__main__': - run() \ No newline at end of file + run() diff --git a/graphicsItems/GraphicsItemMethods.py b/graphicsItems/GraphicsItemMethods.py index 49da713b..76a54ccf 100644 --- a/graphicsItems/GraphicsItemMethods.py +++ b/graphicsItems/GraphicsItemMethods.py @@ -51,7 +51,6 @@ class GraphicsItemMethods(object): if hasattr(p, 'implements') and p.implements('ViewBox'): self._viewBox = weakref.ref(p) break - return self._viewBox() ## If we made it this far, _viewBox is definitely not None def forgetViewBox(self): @@ -78,7 +77,10 @@ class GraphicsItemMethods(object): if view is None: return None if hasattr(view, 'implements') and view.implements('ViewBox'): - return self.itemTransform(view.innerSceneItem())[0] + tr = self.itemTransform(view.innerSceneItem()) + if isinstance(tr, tuple): + tr = tr[0] ## difference between pyside and pyqt + return tr else: return self.sceneTransform() #return self.deviceTransform(view.viewportTransform()) @@ -102,7 +104,11 @@ class GraphicsItemMethods(object): view = self.getViewBox() if view is None: return None - bounds = self.mapRectFromView(view.viewRect()).normalized() + bounds = self.mapRectFromView(view.viewRect()) + if bounds is None: + return None + + bounds = bounds.normalized() ## nah. #for p in self.getBoundingParents(): @@ -246,7 +252,10 @@ class GraphicsItemMethods(object): if relativeItem is None: relativeItem = self.parentItem() - tr = self.itemTransform(relativeItem)[0] + + tr = self.itemTransform(relativeItem) + if isinstance(tr, tuple): ## difference between pyside and pyqt + tr = tr[0] vec = tr.map(Point(1,0)) - tr.map(Point(0,0)) return Point(vec).angle(Point(1,0)) From 45fb4f6d406cf23b895f8f7a39dd33382543b6bb Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 1 Mar 2012 22:58:02 -0500 Subject: [PATCH 006/238] import corrections --- examples/DataSlicing.py | 2 +- examples/Draw.py | 2 +- examples/GraphicsScene.py | 2 +- examples/ImageItem.py | 4 ++-- examples/ImageView.py | 2 +- examples/ROItypes.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/DataSlicing.py b/examples/DataSlicing.py index 32b9c584..a9e910be 100644 --- a/examples/DataSlicing.py +++ b/examples/DataSlicing.py @@ -6,7 +6,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) import numpy as np import scipy -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg app = QtGui.QApplication([]) diff --git a/examples/Draw.py b/examples/Draw.py index 83736cc4..4c687354 100644 --- a/examples/Draw.py +++ b/examples/Draw.py @@ -4,7 +4,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui import numpy as np import pyqtgraph as pg diff --git a/examples/GraphicsScene.py b/examples/GraphicsScene.py index 9720f65b..9fc97876 100644 --- a/examples/GraphicsScene.py +++ b/examples/GraphicsScene.py @@ -3,7 +3,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg from pyqtgraph.GraphicsScene import GraphicsScene diff --git a/examples/ImageItem.py b/examples/ImageItem.py index 6697e93c..f754a0bc 100644 --- a/examples/ImageItem.py +++ b/examples/ImageItem.py @@ -2,11 +2,11 @@ ## Add path to library (just for examples; you do not need this) import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -import ptime -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui import numpy as np import pyqtgraph as pg +import pyqtgraph.ptime as ptime app = QtGui.QApplication([]) diff --git a/examples/ImageView.py b/examples/ImageView.py index fd1fd8fe..07d296ec 100644 --- a/examples/ImageView.py +++ b/examples/ImageView.py @@ -6,7 +6,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) import numpy as np import scipy -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui import pyqtgraph as pg app = QtGui.QApplication([]) diff --git a/examples/ROItypes.py b/examples/ROItypes.py index 7649baaa..b7d6b5eb 100644 --- a/examples/ROItypes.py +++ b/examples/ROItypes.py @@ -5,7 +5,7 @@ import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) -from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtCore, QtGui import numpy as np import pyqtgraph as pg From 7d6de09e0f7e21c31d57b5fdd0d1a1c1c8139ecd Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 1 Mar 2012 23:03:24 -0500 Subject: [PATCH 007/238] pyside compatibility fix --- graphicsItems/GradientEditorItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphicsItems/GradientEditorItem.py b/graphicsItems/GradientEditorItem.py index 1ae2e4cc..d3eaaf86 100644 --- a/graphicsItems/GradientEditorItem.py +++ b/graphicsItems/GradientEditorItem.py @@ -315,7 +315,7 @@ class GradientEditorItem(TickSliderItem): def showMenu(self, ev): self.menu.popup(ev.screenPos().toQPoint()) - def contextMenuClicked(self, b): + def contextMenuClicked(self, b=None): global Gradients act = self.sender() self.loadPreset(act.name) From 615ddb364862ea54ba12a64f0ea50d38e3dbf596 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 1 Mar 2012 23:23:33 -0500 Subject: [PATCH 008/238] revert Qt.py to use pyqt4 --- Qt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Qt.py b/Qt.py index 068662b2..eb63b7c1 100644 --- a/Qt.py +++ b/Qt.py @@ -1,6 +1,6 @@ ## Do all Qt imports from here to allow easier PyQt / PySide compatibility -from PySide import QtGui, QtCore, QtOpenGL, QtSvg -#from PyQt4 import QtGui, QtCore, QtOpenGL, QtSvg -#if not hasattr(QtCore, 'Signal'): -# QtCore.Signal = QtCore.pyqtSignal +#from PySide import QtGui, QtCore, QtOpenGL, QtSvg +from PyQt4 import QtGui, QtCore, QtOpenGL, QtSvg +if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal From e263baa06a2632418e2f2ea0d26b17f9dfe7d7f8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 6 Mar 2012 01:19:41 -0500 Subject: [PATCH 009/238] minor updates for CanvasItem --- canvas/CanvasItem.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/canvas/CanvasItem.py b/canvas/CanvasItem.py index 3900af2d..6298d364 100644 --- a/canvas/CanvasItem.py +++ b/canvas/CanvasItem.py @@ -6,7 +6,7 @@ import TransformGuiTemplate import debug class SelectBox(ROI): - def __init__(self, scalable=False): + def __init__(self, scalable=False, rotatable=True): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) ROI.__init__(self, [0,0], [1,1], invertible=True) center = [0.5, 0.5] @@ -14,8 +14,9 @@ class SelectBox(ROI): if scalable: self.addScaleHandle([1, 1], center, lockAspect=True) self.addScaleHandle([0, 0], center, lockAspect=True) - self.addRotateHandle([0, 1], center) - self.addRotateHandle([1, 0], center) + if rotatable: + self.addRotateHandle([0, 1], center) + self.addRotateHandle([1, 0], center) class CanvasItem(QtCore.QObject): @@ -30,7 +31,7 @@ class CanvasItem(QtCore.QObject): transformCopyBuffer = None def __init__(self, item, **opts): - defOpts = {'name': None, 'z': None, 'movable': True, 'scalable': False, 'visible': True, 'parent':None} #'pos': [0,0], 'scale': [1,1], 'angle':0, + defOpts = {'name': None, 'z': None, 'movable': True, 'scalable': False, 'rotatable': True, 'visible': True, 'parent':None} #'pos': [0,0], 'scale': [1,1], 'angle':0, defOpts.update(opts) self.opts = defOpts self.selectedAlone = False ## whether this item is the only one selected @@ -105,7 +106,7 @@ class CanvasItem(QtCore.QObject): ## every CanvasItem implements its own individual selection box ## so that subclasses are free to make their own. - self.selectBox = SelectBox(scalable=self.opts['scalable']) + self.selectBox = SelectBox(scalable=self.opts['scalable'], rotatable=self.opts['rotatable']) #self.canvas.scene().addItem(self.selectBox) self.selectBox.hide() self.selectBox.setZValue(1e6) From 6a7021797f575c938f91eb62a412c84013ef59cf Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 6 Mar 2012 01:20:42 -0500 Subject: [PATCH 010/238] exception message fix --- Point.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Point.py b/Point.py index c16d4df6..68980745 100644 --- a/Point.py +++ b/Point.py @@ -46,7 +46,7 @@ class Point(QtCore.QPointF): elif i == 1: return self.y() else: - raise IndexError("Point has no index %d" % i) + raise IndexError("Point has no index %s" % str(i)) def __setitem__(self, i, x): if i == 0: @@ -54,7 +54,7 @@ class Point(QtCore.QPointF): elif i == 1: return self.setY(x) else: - raise IndexError("Point has no index %d" % i) + raise IndexError("Point has no index %s" % str(i)) def __radd__(self, a): return self._math_('__radd__', a) From 872fcb17fffb7e355e780d8813f40478658aa7fb Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 6 Mar 2012 01:22:02 -0500 Subject: [PATCH 011/238] Added basic OpenGL scenegraph system - rotate/scalable view widget - volumetric data item --- opengl/GLGraphicsItem.py | 60 +++++++++++ opengl/GLViewWidget.py | 199 +++++++++++++++++++++++++++++++++++ opengl/__init__.py | 23 ++++ opengl/items/GLBoxItem.py | 47 +++++++++ opengl/items/GLVolumeItem.py | 179 +++++++++++++++++++++++++++++++ opengl/items/__init__.py | 0 6 files changed, 508 insertions(+) create mode 100644 opengl/GLGraphicsItem.py create mode 100644 opengl/GLViewWidget.py create mode 100644 opengl/__init__.py create mode 100644 opengl/items/GLBoxItem.py create mode 100644 opengl/items/GLVolumeItem.py create mode 100644 opengl/items/__init__.py diff --git a/opengl/GLGraphicsItem.py b/opengl/GLGraphicsItem.py new file mode 100644 index 00000000..7baa3b7e --- /dev/null +++ b/opengl/GLGraphicsItem.py @@ -0,0 +1,60 @@ +from pyqtgraph.Qt import QtGui, QtCore + +class GLGraphicsItem(QtCore.QObject): + def __init__(self, parentItem=None): + QtCore.QObject.__init__(self) + self.__parent = None + self.__view = None + self.__children = set() + self.setParentItem(parentItem) + self.setDepthValue(0) + + def setParentItem(self, item): + if self.__parent is not None: + self.__parent.__children.remove(self) + if item is not None: + item.__children.add(self) + self.__parent = item + + def parentItem(self): + return self.__parent + + def childItems(self): + return list(self.__children) + + def _setView(self, v): + self.__view = v + + def view(self): + return self.__view + + def setDepthValue(self, value): + """ + Sets the depth value of this item. Default is 0. + This controls the order in which items are drawn--those with a greater depth value will be drawn later. + Items with negative depth values are drawn before their parent. + (This is analogous to QGraphicsItem.zValue) + The depthValue does NOT affect the position of the item or the values it imparts to the GL depth buffer. + '""" + self.__depthValue = value + + def depthValue(self): + """Return the depth value of this item. See setDepthValue for mode information.""" + return self.__depthValue + + def initializeGL(self): + """ + Called after an item is added to a GLViewWidget. + The widget's GL context is made current before this method is called. + (So this would be an appropriate time to generate lists, upload textures, etc.) + """ + pass + + def paint(self): + """ + Called by the GLViewWidget to draw this item. + It is the responsibility of the item to set up its own modelview matrix, + but the caller will take care of pushing/popping. + """ + pass + \ No newline at end of file diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py new file mode 100644 index 00000000..ae579003 --- /dev/null +++ b/opengl/GLViewWidget.py @@ -0,0 +1,199 @@ +from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL +from OpenGL.GL import * +import numpy as np + +Vector = QtGui.QVector3D + +class GLViewWidget(QtOpenGL.QGLWidget): + """ + Basic widget for displaying 3D data + - Rotation/scale controls + - Axis/grid display + - Export options + + """ + def __init__(self, parent=None): + QtOpenGL.QGLWidget.__init__(self, parent) + self.opts = { + 'center': Vector(0,0,0), ## will always appear at the center of the widget + 'distance': 10.0, ## distance of camera from center + 'fov': 60, ## horizontal field of view in degrees + 'elevation': 30, ## camera's angle of elevation in degrees + 'azimuth': 45, ## camera's azimuthal angle in degrees + ## (rotation around z-axis 0 points along x-axis) + } + self.items = [] + + def addItem(self, item): + self.items.append(item) + if hasattr(item, 'initializeGL'): + self.makeCurrent() + item.initializeGL() + item._setView(self) + #print "set view", item, self, item.view() + self.updateGL() + + def initializeGL(self): + glClearColor(0.0, 0.0, 0.0, 0.0) + glEnable(GL_DEPTH_TEST) + + glEnable( GL_ALPHA_TEST ) + self.resizeGL(self.width(), self.height()) + self.generateAxes() + #self.generatePoints() + + def resizeGL(self, w, h): + glViewport(0, 0, w, h) + #self.updateGL() + + def setProjection(self): + ## Create the projection matrix + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + w = self.width() + h = self.height() + dist = self.opts['distance'] + fov = self.opts['fov'] + + nearClip = dist * 0.001 + farClip = dist * 1000. + + r = nearClip * np.tan(fov) + t = r * h / w + glFrustum( -r, r, -t, t, nearClip, farClip) + + def setModelview(self): + glMatrixMode(GL_MODELVIEW) + glLoadIdentity() + glTranslatef( 0.0, 0.0, -self.opts['distance']) + glRotatef(self.opts['elevation']-90, 1, 0, 0) + glRotatef(self.opts['azimuth']+90, 0, 0, -1) + center = self.opts['center'] + glTranslatef(center.x(), center.y(), center.z()) + + + def paintGL(self): + self.setProjection() + self.setModelview() + + glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) + glDisable( GL_DEPTH_TEST ) + #print "draw list:", self.axisList + glCallList(self.axisList) ## draw axes + #glCallList(self.pointList) + #self.drawPoints() + #self.drawAxes() + + self.drawItemTree() + + def drawItemTree(self, item=None): + if item is None: + items = [x for x in self.items if x.parentItem() is None] + else: + items = item.childItems() + items.append(item) + items.sort(lambda a,b: cmp(a.depthValue(), b.depthValue())) + for i in items: + if i is item: + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + i.paint() + glMatrixMode(GL_MODELVIEW) + glPopMatrix() + else: + self.drawItemTree(i) + + + def cameraPosition(self): + """Return current position of camera based on center, dist, elevation, and azimuth""" + center = self.opts['center'] + dist = self.opts['distance'] + elev = self.opts['elevation'] * np.pi/180. + azim = self.opts['azimuth'] * np.pi/180. + + pos = Vector( + center.x() + dist * np.cos(elev) * np.cos(azim), + center.y() + dist * np.cos(elev) * np.sin(azim), + center.z() + dist * np.sin(elev) + ) + + return pos + + + + def generateAxes(self): + self.axisList = glGenLists(1) + glNewList(self.axisList, GL_COMPILE) + + #glShadeModel(GL_FLAT) + #glFrontFace(GL_CCW) + #glEnable( GL_LIGHT_MODEL_TWO_SIDE ) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + #glAlphaFunc( GL_ALWAYS,0.5 ) + glEnable( GL_POINT_SMOOTH ) + glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + glColor4f(1, 1, 1, .3) + for x in range(-10, 11): + glVertex3f(x, -10, 0) + glVertex3f(x, 10, 0) + for y in range(-10, 11): + glVertex3f(-10, y, 0) + glVertex3f( 10, y, 0) + + + glColor4f(0, 1, 0, .6) # z is green + glVertex3f(0, 0, 0) + glVertex3f(0, 0, 5) + + glColor4f(1, 1, 0, .6) # y is yellow + glVertex3f(0, 0, 0) + glVertex3f(0, 5, 0) + + glColor4f(0, 0, 1, .6) # x is blue + glVertex3f(0, 0, 0) + glVertex3f(5, 0, 0) + glEnd() + glEndList() + + def generatePoints(self): + self.pointList = glGenLists(1) + glNewList(self.pointList, GL_COMPILE) + width = 7 + alpha = 0.02 + n = 40 + glPointSize( width ) + glBegin(GL_POINTS) + for x in range(-n, n+1): + r = (n-x)/(2.*n) + glColor4f(r, r, r, alpha) + for y in range(-n, n+1): + for z in range(-n, n+1): + glVertex3f(x, y, z) + glEnd() + glEndList() + + + def mousePressEvent(self, ev): + self.mousePos = ev.pos() + + def mouseMoveEvent(self, ev): + diff = ev.pos() - self.mousePos + self.mousePos = ev.pos() + self.opts['azimuth'] -= diff.x() + self.opts['elevation'] = np.clip(self.opts['elevation'] + diff.y(), -90, 90) + #print self.opts['azimuth'], self.opts['elevation'] + self.updateGL() + + def mouseReleaseEvent(self, ev): + pass + + def wheelEvent(self, ev): + self.opts['distance'] *= 0.999**ev.delta() + self.updateGL() + + + diff --git a/opengl/__init__.py b/opengl/__init__.py new file mode 100644 index 00000000..3d501e9d --- /dev/null +++ b/opengl/__init__.py @@ -0,0 +1,23 @@ +from GLViewWidget import GLViewWidget + +import os +def importAll(path): + d = os.path.join(os.path.split(__file__)[0], path) + files = [] + for f in os.listdir(d): + if os.path.isdir(os.path.join(d, f)): + files.append(f) + elif f[-3:] == '.py' and f != '__init__.py': + files.append(f[:-3]) + + for modName in files: + mod = __import__(path+"."+modName, globals(), locals(), fromlist=['*']) + if hasattr(mod, '__all__'): + names = mod.__all__ + else: + names = [n for n in dir(mod) if n[0] != '_'] + for k in names: + if hasattr(mod, k): + globals()[k] = getattr(mod, k) + +importAll('items') diff --git a/opengl/items/GLBoxItem.py b/opengl/items/GLBoxItem.py new file mode 100644 index 00000000..ffaa6861 --- /dev/null +++ b/opengl/items/GLBoxItem.py @@ -0,0 +1,47 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem + +__all__ = ['GLBoxItem'] + +class GLBoxItem(GLGraphicsItem): + def paint(self): + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + #glAlphaFunc( GL_ALWAYS,0.5 ) + glEnable( GL_POINT_SMOOTH ) + glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + glColor4f(1, 1, 1, .3) + w = 10 + glVertex3f(-w, -w, -w) + glVertex3f(-w, -w, w) + glVertex3f( w, -w, -w) + glVertex3f( w, -w, w) + glVertex3f(-w, w, -w) + glVertex3f(-w, w, w) + glVertex3f( w, w, -w) + glVertex3f( w, w, w) + + glVertex3f(-w, -w, -w) + glVertex3f(-w, w, -w) + glVertex3f( w, -w, -w) + glVertex3f( w, w, -w) + glVertex3f(-w, -w, w) + glVertex3f(-w, w, w) + glVertex3f( w, -w, w) + glVertex3f( w, w, w) + + glVertex3f(-w, -w, -w) + glVertex3f( w, -w, -w) + glVertex3f(-w, w, -w) + glVertex3f( w, w, -w) + glVertex3f(-w, -w, w) + glVertex3f( w, -w, w) + glVertex3f(-w, w, w) + glVertex3f( w, w, w) + + glEnd() + + \ No newline at end of file diff --git a/opengl/items/GLVolumeItem.py b/opengl/items/GLVolumeItem.py new file mode 100644 index 00000000..548cd2bd --- /dev/null +++ b/opengl/items/GLVolumeItem.py @@ -0,0 +1,179 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtGui +import numpy as np + +__all__ = ['GLVolumeItem'] + +class GLVolumeItem(GLGraphicsItem): + def initializeGL(self): + n = 128 + self.data = np.random.randint(0, 255, size=4*n**3).astype(np.uint8).reshape((n,n,n,4)) + self.data[...,3] *= 0.1 + for i in range(n): + self.data[i,:,:,0] = i*256./n + glEnable(GL_TEXTURE_3D) + self.texture = glGenTextures(1) + glBindTexture(GL_TEXTURE_3D, self.texture) + #glTexImage3D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, void *data ); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) + #glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BORDER_COLOR, ) ## black/transparent by default + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, n, n, n, 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data) + glDisable(GL_TEXTURE_3D) + + self.lists = {} + for ax in [0,1,2]: + for d in [-1, 1]: + l = glGenLists(1) + self.lists[(ax,d)] = l + glNewList(l, GL_COMPILE) + self.drawVolume(ax, d) + glEndList() + + + def paint(self): + + glEnable(GL_TEXTURE_3D) + glBindTexture(GL_TEXTURE_3D, self.texture) + + glDisable(GL_DEPTH_TEST) + #glDisable(GL_CULL_FACE) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + + view = self.view() + cam = view.cameraPosition() + cam = np.array([cam.x(), cam.y(), cam.z()]) + ax = np.argmax(abs(cam)) + d = 1 if cam[ax] > 0 else -1 + glCallList(self.lists[(ax,d)]) ## draw axes + glDisable(GL_TEXTURE_3D) + + def drawVolume(self, ax, d): + slices = 256 + N = 5 + + imax = [0,1,2] + imax.remove(ax) + + tp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]] + vp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]] + tp[0][imax[0]] = 0 + tp[0][imax[1]] = 0 + tp[1][imax[0]] = 1 + tp[1][imax[1]] = 0 + tp[2][imax[0]] = 1 + tp[2][imax[1]] = 1 + tp[3][imax[0]] = 0 + tp[3][imax[1]] = 1 + + vp[0][imax[0]] = -N + vp[0][imax[1]] = -N + vp[1][imax[0]] = N + vp[1][imax[1]] = -N + vp[2][imax[0]] = N + vp[2][imax[1]] = N + vp[3][imax[0]] = -N + vp[3][imax[1]] = N + r = range(slices) + if d == -1: + r = r[::-1] + + glBegin(GL_QUADS) + for i in r: + z = float(i)/(slices-1.) + w = float(i)*10./(slices-1.) - 5. + + tp[0][ax] = z + tp[1][ax] = z + tp[2][ax] = z + tp[3][ax] = z + + vp[0][ax] = w + vp[1][ax] = w + vp[2][ax] = w + vp[3][ax] = w + + + glTexCoord3f(*tp[0]) + glVertex3f(*vp[0]) + glTexCoord3f(*tp[1]) + glVertex3f(*vp[1]) + glTexCoord3f(*tp[2]) + glVertex3f(*vp[2]) + glTexCoord3f(*tp[3]) + glVertex3f(*vp[3]) + glEnd() + + + + + + + + + + ## Interesting idea: + ## remove projection/modelview matrixes, recreate in texture coords. + ## it _sorta_ works, but needs tweaking. + #mvm = glGetDoublev(GL_MODELVIEW_MATRIX) + #pm = glGetDoublev(GL_PROJECTION_MATRIX) + #m = QtGui.QMatrix4x4(mvm.flatten()).inverted()[0] + #p = QtGui.QMatrix4x4(pm.flatten()).inverted()[0] + + #glMatrixMode(GL_PROJECTION) + #glPushMatrix() + #glLoadIdentity() + #N=1 + #glOrtho(-N,N,-N,N,-100,100) + + #glMatrixMode(GL_MODELVIEW) + #glLoadIdentity() + + + #glMatrixMode(GL_TEXTURE) + #glLoadIdentity() + #glMultMatrixf(m.copyDataTo()) + + #view = self.view() + #w = view.width() + #h = view.height() + #dist = view.opts['distance'] + #fov = view.opts['fov'] + #nearClip = dist * .1 + #farClip = dist * 5. + #r = nearClip * np.tan(fov) + #t = r * h / w + + #p = QtGui.QMatrix4x4() + #p.frustum( -r, r, -t, t, nearClip, farClip) + #glMultMatrixf(p.inverted()[0].copyDataTo()) + + + #glBegin(GL_QUADS) + + #M=1 + #for i in range(500): + #z = i/500. + #w = -i/500. + #glTexCoord3f(-M, -M, z) + #glVertex3f(-N, -N, w) + #glTexCoord3f(M, -M, z) + #glVertex3f(N, -N, w) + #glTexCoord3f(M, M, z) + #glVertex3f(N, N, w) + #glTexCoord3f(-M, M, z) + #glVertex3f(-N, N, w) + #glEnd() + #glDisable(GL_TEXTURE_3D) + + #glMatrixMode(GL_PROJECTION) + #glPopMatrix() + + + diff --git a/opengl/items/__init__.py b/opengl/items/__init__.py new file mode 100644 index 00000000..e69de29b From 8dbce440e437ddb20fc5500be77c125d134c0147 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 6 Mar 2012 01:22:41 -0500 Subject: [PATCH 012/238] example for GL scenegraph --- examples/GLViewWidget.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 examples/GLViewWidget.py diff --git a/examples/GLViewWidget.py b/examples/GLViewWidget.py new file mode 100644 index 00000000..4fdbf129 --- /dev/null +++ b/examples/GLViewWidget.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.opengl as gl + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.show() + + +b = gl.GLBoxItem() +w.addItem(b) + +v = gl.GLVolumeItem() +w.addItem(v) + From 269374ef841751f2556688d7f73875c72631737c Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 6 Mar 2012 01:23:10 -0500 Subject: [PATCH 013/238] removed some extra files --- .../build/doctrees/apireference.doctree | Bin 3040 -> 0 bytes .../build/doctrees/functions.doctree | Bin 54676 -> 0 bytes .../doctrees/graphicsItems/arrowitem.doctree | Bin 5249 -> 0 bytes .../doctrees/graphicsItems/axisitem.doctree | Bin 11126 -> 0 bytes .../doctrees/graphicsItems/buttonitem.doctree | Bin 5775 -> 0 bytes .../doctrees/graphicsItems/curvearrow.doctree | Bin 6107 -> 0 bytes .../doctrees/graphicsItems/curvepoint.doctree | Bin 6727 -> 0 bytes .../graphicsItems/gradienteditoritem.doctree | Bin 6649 -> 0 bytes .../graphicsItems/gradientlegend.doctree | Bin 7223 -> 0 bytes .../graphicsItems/graphicslayout.doctree | Bin 8137 -> 0 bytes .../graphicsItems/graphicsobject.doctree | Bin 17301 -> 0 bytes .../graphicsItems/graphicswidget.doctree | Bin 5637 -> 0 bytes .../doctrees/graphicsItems/griditem.doctree | Bin 4913 -> 0 bytes .../graphicsItems/histogramlutitem.doctree | Bin 4895 -> 0 bytes .../doctrees/graphicsItems/imageitem.doctree | Bin 16931 -> 0 bytes .../graphicsItems/infiniteline.doctree | Bin 11533 -> 0 bytes .../doctrees/graphicsItems/labelitem.doctree | Bin 10554 -> 0 bytes .../graphicsItems/linearregionitem.doctree | Bin 8537 -> 0 bytes .../graphicsItems/plotcurveitem.doctree | Bin 9555 -> 0 bytes .../graphicsItems/plotdataitem.doctree | Bin 30382 -> 0 bytes .../doctrees/graphicsItems/plotitem.doctree | Bin 31151 -> 0 bytes .../build/doctrees/graphicsItems/roi.doctree | Bin 21560 -> 0 bytes .../doctrees/graphicsItems/scalebar.doctree | Bin 6287 -> 0 bytes .../graphicsItems/scatterplotitem.doctree | Bin 17644 -> 0 bytes .../graphicsItems/uigraphicsitem.doctree | Bin 15483 -> 0 bytes .../doctrees/graphicsItems/viewbox.doctree | Bin 27898 -> 0 bytes .../doctrees/graphicsItems/vtickgroup.doctree | Bin 5951 -> 0 bytes documentation/build/doctrees/style.doctree | Bin 5421 -> 0 bytes .../build/doctrees/widgets/checktable.doctree | Bin 4775 -> 0 bytes .../doctrees/widgets/colorbutton.doctree | Bin 5607 -> 0 bytes .../doctrees/widgets/datatreewidget.doctree | Bin 6995 -> 0 bytes .../build/doctrees/widgets/filedialog.doctree | Bin 4763 -> 0 bytes .../doctrees/widgets/gradientwidget.doctree | Bin 5742 -> 0 bytes .../widgets/graphicslayoutwidget.doctree | Bin 5215 -> 0 bytes .../doctrees/widgets/graphicsview.doctree | Bin 12436 -> 0 bytes .../widgets/histogramlutwidget.doctree | Bin 5455 -> 0 bytes .../build/doctrees/widgets/imageview.doctree | Bin 12149 -> 0 bytes .../doctrees/widgets/joystickbutton.doctree | Bin 4859 -> 0 bytes .../doctrees/widgets/multiplotwidget.doctree | Bin 5295 -> 0 bytes .../build/doctrees/widgets/plotwidget.doctree | Bin 5476 -> 0 bytes .../doctrees/widgets/progressdialog.doctree | Bin 10374 -> 0 bytes .../doctrees/widgets/rawimagewidget.doctree | Bin 7770 -> 0 bytes .../build/doctrees/widgets/spinbox.doctree | Bin 12210 -> 0 bytes .../doctrees/widgets/tablewidget.doctree | Bin 11304 -> 0 bytes .../build/doctrees/widgets/treewidget.doctree | Bin 7154 -> 0 bytes .../doctrees/widgets/verticallabel.doctree | Bin 5483 -> 0 bytes .../build/html/_sources/widgets/imageview.txt | 8 - .../build/html/widgets/imageview.html | 158 ------- graphicsItems/ImageItem.old | 398 ------------------ graphicsItems/ViewBox.pyc.renamed1 | Bin 22064 -> 0 bytes 50 files changed, 564 deletions(-) delete mode 100644 documentation/build/doctrees/apireference.doctree delete mode 100644 documentation/build/doctrees/functions.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/arrowitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/axisitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/buttonitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/curvearrow.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/curvepoint.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/gradienteditoritem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/gradientlegend.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/graphicslayout.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/graphicsobject.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/graphicswidget.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/griditem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/histogramlutitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/imageitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/infiniteline.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/labelitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/linearregionitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/plotcurveitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/plotdataitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/plotitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/roi.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/scalebar.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/scatterplotitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/uigraphicsitem.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/viewbox.doctree delete mode 100644 documentation/build/doctrees/graphicsItems/vtickgroup.doctree delete mode 100644 documentation/build/doctrees/style.doctree delete mode 100644 documentation/build/doctrees/widgets/checktable.doctree delete mode 100644 documentation/build/doctrees/widgets/colorbutton.doctree delete mode 100644 documentation/build/doctrees/widgets/datatreewidget.doctree delete mode 100644 documentation/build/doctrees/widgets/filedialog.doctree delete mode 100644 documentation/build/doctrees/widgets/gradientwidget.doctree delete mode 100644 documentation/build/doctrees/widgets/graphicslayoutwidget.doctree delete mode 100644 documentation/build/doctrees/widgets/graphicsview.doctree delete mode 100644 documentation/build/doctrees/widgets/histogramlutwidget.doctree delete mode 100644 documentation/build/doctrees/widgets/imageview.doctree delete mode 100644 documentation/build/doctrees/widgets/joystickbutton.doctree delete mode 100644 documentation/build/doctrees/widgets/multiplotwidget.doctree delete mode 100644 documentation/build/doctrees/widgets/plotwidget.doctree delete mode 100644 documentation/build/doctrees/widgets/progressdialog.doctree delete mode 100644 documentation/build/doctrees/widgets/rawimagewidget.doctree delete mode 100644 documentation/build/doctrees/widgets/spinbox.doctree delete mode 100644 documentation/build/doctrees/widgets/tablewidget.doctree delete mode 100644 documentation/build/doctrees/widgets/treewidget.doctree delete mode 100644 documentation/build/doctrees/widgets/verticallabel.doctree delete mode 100644 documentation/build/html/_sources/widgets/imageview.txt delete mode 100644 documentation/build/html/widgets/imageview.html delete mode 100644 graphicsItems/ImageItem.old delete mode 100644 graphicsItems/ViewBox.pyc.renamed1 diff --git a/documentation/build/doctrees/apireference.doctree b/documentation/build/doctrees/apireference.doctree deleted file mode 100644 index 1620ca32384d7d38e0620151f1b3adcdd4c5aa82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3040 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 diff --git a/documentation/build/doctrees/graphicsItems/arrowitem.doctree b/documentation/build/doctrees/graphicsItems/arrowitem.doctree deleted file mode 100644 index 8a90ddb68c5aa439d7ce3e5422a617ade3cab882..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5249 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 AO8@`> diff --git a/documentation/build/doctrees/graphicsItems/axisitem.doctree b/documentation/build/doctrees/graphicsItems/axisitem.doctree deleted file mode 100644 index b0deac26328c4cda3a687dc224f471c3dc599c0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11126 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 diff --git a/documentation/build/doctrees/graphicsItems/buttonitem.doctree b/documentation/build/doctrees/graphicsItems/buttonitem.doctree deleted file mode 100644 index ce962738e59669da725827d5eef5e4acde6a8794..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5775 zcmbVQ2Y4LC6_#X6I-S)l7r-SJ#c%?wqnKvEl!O)$r7<9qTsC{RbDG6_yZ2^yEh)(o zNCH7gNGFZ-9uh)&NF@oWq>|oy@4dJG&E8&-<@*Ak^xd~R`)1z%{?}&an!bt?RN~0> z!f8KnWSHmIn)IVQ^-f(&eF?28h#}vu$!MuALt2|qe_<}))6-LqqbTs*NY-dzc5Wj^ z>Pr;OYrC$d&o|#L;ctU8y>r_S@^RX-T&!SOl$+^|#riNVhmjirPT*UPtcEm{P+pAI zZ5>LhCd1I42ZnVe8Wy9KSZm<4!XVZaNh7l&@5oA^ZJte|vtq)Iqo5iDksn0T3L{%b zG&a*K2H6_d328i`^#wH`@@=L}s6qVYR4x{|igG;%I%q?QHm>9;loeK#r%j+~b3%uJ zCT%Y?&>Sjy!~kFeARVUq#6V+w%gSZ6tn1i!E8sp)Tjvy*DypC+i(b4>78e4&uUNBv zH?%yvTx6-DU7_7Y&n*|(YsDt##msTW{$i@HXx!meLUu)1Y}q-j!-%$BcP(vC=ssmJ z7}@j83hfYs4Z~+xt1W7~+9CRlIGv~f_N0P3vZRhGshuTtbV(ITv4T$3&~)-jMVHA% zMEvfRtDgdfPEF`EC|0a#IxgFBxf0fu>n~2*j+5q$>pOBWPp1Rs842xywCG83%`z>;l5X_2)fFd*^l_5L#wV77W*uu0Ppy_!LKKMCaJB zY_?(8D%g$k1mE!v7i~hL|xnljP74JfHfAiLKlG-#Ts6o0me%cdH{R*qz>~7ZVicl zq3g-xCl_2NQfJQwKAzJAQ{enOwM7h_#hH)j+Dpv;Xs052Ae;Ljb_6|G4ccO$ z7C13Te~8*TH)M=$lrefJ+<#d@qTKE=x|}EFs8mtZ}~_KDtLXz5>pBWkRn4Ig0JQIgJ}`)vr$I zHJM{T18S-=J^Hm>di3iGYOev1?OY2}lZ93ZY!NVtDxd;qino#xy}q+aD{jZPB=m+A z>P;hlV?kZ8tPaw5)kUU&r;F3A>JoKpdt-T1L2(zoEulAaG@RLt2F{4)Zu=gz(_7Mj zRi+|Y&g?ks_}=qY4~u# zl)F0<`UoTKsE>epM+2&W#pSU-`_Co@oYwu;?d34Myy9Z+36^nJRU zzRy5k=XClk6n-wD&u0qnYAL)sp)at)RVw33E5F!H>zAN)l{M(gQ2mvJzM84N+o(>f ztK$0kwS>N&eaynqRLpO5QD=&Cx(I%4-jIuRk+%J?8t9tT^vzi@(mWfQgUh!P`nJg7 zk#`CqeJ7#sit%u%Rt`KXlMLy5CHlTt-*K`uvwu*cABv4D#4YpbMa9&3;OF1D&D zs_!WlqaeVQgDa!zbfD*pDyn%!`iB?-0~N_j(?7B5XkFIX28n}o1q=RbR*Z5_sautx z#C z4zvaMfdiVY^i|ou4l5M8KJis;ZY9#R`BZWW^`q@S!~Em*@XkNK7!xJENt@ZG91t#@kldw2-Ttv6z-xvip4N7 zvdfm)TI-z{naPRK&LGuCvtYlt>_R{~hMxx;w`{t=Z|Yud7PI@F|NDkYPqDh8dFe}B z>mdS;#XCxXiPn~Hjx(df9DrG+(Z}=59gSkMu&{s)DfVN&DK?JWUxaX2)Vo+}TPM3K zQgj+3SDDlqCRz2$HPf zFOxi>J{f~0vDsvj>xWXC%TVdFXC1v8Uy-ltaMOsKPsdJ4$3_e*Svl9I^5St>Y+Biq z%tfd^jVJY(t2%u;o)direhX>>w`qg8W5}u7VlmNiA#3`g-eZK%#4q;C$6?2=&oUEn up=EBiva3$q$l>-4_ikb+YZ;+F8=@t>7r$bFW6}xrIe1C*x%iFC)BgjeNXXv+ diff --git a/documentation/build/doctrees/graphicsItems/curvearrow.doctree b/documentation/build/doctrees/graphicsItems/curvearrow.doctree deleted file mode 100644 index 532f70172cbd9438946c890aa9c68e8299c26643..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6107 zcmc&&d6*nU72j;KyR(z*CfOW7Ldb9gCL!4g5UxN1;SLLHR0oHF7s_@V!_n9Lwp>!EZ27<%c9)Gf>~$}!A#noX|g$7^oc>x20=PP^^3vg_?qrzv~1|ucdM{{p4Lt&SgNFgx-5C| zURjz6^xjh4_TA9(>`IBHN_LgDl{~jn;$17XY+lMX&gfrC@k(Yp+-kTxlJ&6E)^l8k z5gmEM^|UUbqbg!3vZvW9v|bD~6{CX4+fk(5N*uxFM<;YlQRJMs!EzxTTNXoA&kjR8 zA6FK+ie25SdB--C#X>gj@nt$ejoD%#l`ocQqZ+hhvB-0MNt-76u~^g9=tR}8VB^ij zZtK7qJlJImOgx@Ysi+P&uGisSTh$S2wOXUrsr6#Oh|@_5WKS-tW6SEevf5Bq$Cp*H z94la|CekV0Kv&3Fc>K2R)lY?mPD|)?04qisk1KXusfG>Z`m^J<H-^iXC(K$9Wn`|m}5N?R+YM#yowOt9F*F|j+f2H%8*#$)eMvzjyaK9LIg1YVc zbkS753El%}2bF6$BhbZD0}wFu=o0KH7Bu6#)g>Lo=+fc=R%1~sbm4eWtg*`z;CNX= zm-8-fo~M4%IPPYs4?FMJy1CE@!X1-=FER+5;Vqztsv%np)&nPoxgVz1P7ND1n+c2_j*Y(}AyMhvE4q>=$U~UH_K~Dan0JJ=Dyo1}4&N-x* znA?lkH6S*BG1?1qUPASZT&)MWl$j4^K|&48P*WXbh7B3BSHw(oFD7A=NjvH&0+TqQ zeHoLW2NNWFSSga)b3>$(8Bm%{XfB(Y?cJE6G*6fw%iJEr7Xs)-3B5Q2y2Sv=xPt$K=f-N_N2U^c z35;{DZ85<`+DM#U5rn-k95tb0Nf8>rWSgTKgRB zELLBG>^j`C3B8sr`nrQybYekPNxUAL{C~368{klHOz2J6FvXU;E%jWkLf)LvTQX;X zL2+m(Q+~X)NBQx#qS`rCgyplMytuJYZ0GoPS}Y*VsleIDRdPgcpI?6{t~72>=p9|~ zrp9<@QC+xS9ipUA7n^8F7pFVaZgofJ$@#9L;sW8$gx<{_dMb#vDYY&V52#YOJq;ga z%7OQoa^SrxXR8CtfcL?~?@#Ch^UHt_vhYd?0V_ML>jn}OZe2D?9@ z?=e_A`*%ZqzZcXG0M(vKKLpT^68dolbiV;g4-Umg!%q_WY4$PmZF3m>tOtrYchW`h zYx9*wERMA8hqXZ0rKX=xijmgs*<9^@kh4ib0el3=TbM;E#S(#)=zbVsi z#qxP~Z!`OMW%|8X*(Gk7Pk$)WAH{IhO)KD#{#2$vbAlC1s?AHjxitO-cls8?c01s& zW%?V_Y6KeP7yW%whT6r|xYk=}6->)0 zr2m$6--5nAkqc!M;oJ!I0=#w{qBU)fXeWQA?Zs%G=ze?;^g{e5dJ)TtvCJ|SUy-55 z-cS$V3y7@%SfCwrIvNr;8^4N^Q4iwVq*$VBRZKwFBi5cC!kAdAqNuUGREmNC%^Fum zwedhtmsC{uN;=09!!S-&nw$7=iZI%c4c1Vi5mCkBd9(N^pMnjm8q~Sb6Y9lgbR-HG zGUK>}R~+EW>6{+n=Mm5Lr(-l=Fwv2El*js|ub1+}u!9pEEtYyiFT*=4VD`iEo!Lh_;tCQMFS0%k7D$Q zjIodAr3b_y>cW~kt&cHFnmvbW?{h;p3ba1fjIOGBfxXv~xaxZr+8G)xHn_=g_;{08 z2UY8>z;3&d6*kzg8z8h_ER_B}eY{yLRlE_(*nyUHPoH3hm&sY=Oo>h(RPb#t)I~Es z#@0i}&ophCR@rFQT%0zqEDjdE$q0{fJajnniDqJ&uA_JI!zi@tbc=H^xL>SI(}MJ*Nn0>`q^xESQlASE1X**xOz?zy z7Y55>wTUFx5Bcta#1qifq5J3k#8F;;11{R?>UxFS|{pI)Y^tS{tA zeWvZHFT!)8FUD_CjiEC+gsx3a-5HCqdChHeg6d0*@NWE~KtBqZm*H0ovQIjpz8o)!p2Tll8UG(%uq2!S diff --git a/documentation/build/doctrees/graphicsItems/curvepoint.doctree b/documentation/build/doctrees/graphicsItems/curvepoint.doctree deleted file mode 100644 index c98a291658a6fb2ed20c70781757701030c86fab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6727 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 diff --git a/documentation/build/doctrees/graphicsItems/gradienteditoritem.doctree b/documentation/build/doctrees/graphicsItems/gradienteditoritem.doctree deleted file mode 100644 index b1029004df6603a1e48d00bac85ce8ddf469fd13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6649 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 Ll4U%@(&+yHTGYz) diff --git a/documentation/build/doctrees/graphicsItems/gradientlegend.doctree b/documentation/build/doctrees/graphicsItems/gradientlegend.doctree deleted file mode 100644 index 912cb974046a488635ffcaf71a0cbdae1df61a9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7223 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 Q(+-rvORNGsqsr)i0T7jdB>(^b diff --git a/documentation/build/doctrees/graphicsItems/graphicslayout.doctree b/documentation/build/doctrees/graphicsItems/graphicslayout.doctree deleted file mode 100644 index c22d0900709741831dfabf99843a688c9f40c7a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8137 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 diff --git a/documentation/build/doctrees/graphicsItems/graphicsobject.doctree b/documentation/build/doctrees/graphicsItems/graphicsobject.doctree deleted file mode 100644 index b892464b888bcf221db4ff86d90884e2b0cbdb1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17301 zcmeHP2b3Je(Ut^vx?AZ^LLhRO6KM~mJz+2=9GEDA;E+Hg*5LDMZg=ixR=YdfHM6>t z&De-6a>fZ82b^)hIp>^n&c+evod2(?XLe_!@Aq#%;JyEy-po!{b#+yB^;bPT-MV~H zzTo9+fm`x36|Z3XDSj;5l^~@Sj_y#4LbXfU9IRMnJJ?>eeYI<-`qJ4W7c5v%-L8Te zvZ_wzEWWyV|AMKaoewfIc5s?ks}$VI%m#}f%2)mC7iJIs7j-tf_UviioLUUtbT8h5 zz0J+ro5-s8(;k)804f~Pt3uVCvrFgMm6^a%gJ9mf2eV3n;m{ro8IiUbhI-_yCEy+g zw>SbW@Wcgb=|py z4IeIFzp!TR=GUhDzzq-%UL{wsr+u|ss8*Q6Rf~K(SGIlMnn4WgKB@LFhx0Y!&dvE= zjqcHSe4YpXpc%oSEG@TNU4sCa>$^8Brw< z{0-o55PvEBEy3SXbGXH;`#MAT8^+%<{4IBu*UW*uwNH>06CB5e*7YE|a*)sd6t;(W>S zef(ZMX%0+T`K`n~yJphdg*zcLsm7drEwfMaubos!IZLgYxogR-*y?fX7eQDA3F>HP zkpn*;lWz48f~ExDJRWX7HdIeYJBEykW?)dqIU~-2&MN0n=Ww%6s;J`~D11WNIdalj zJ?X5ObTX69+R2)OcAcoT6I&bGWX}Pe#yhoe5Q1iiF^YvE0V3%RyGsm@FzWB_s{ zTdfmxYc|(Ni{A~mV$=g*l%qRPXIZdtA~LXpe!$^;N}Ub$&I#4IE$Z#cRJES9J1>pF z`%j_?Eg&MqjsdI34P%Mfvj(Rfsc`{T_nM!tz%5kNE=~}5IV`NLG2tlB0yQeVv z`=(tUU-_L$s|#JMwkp6e8MrBB!|dr$&9K@1u`3N&xe4LTrY#fG8da3Twz(YwJq`m^ zbjB01)K-{S3ROAo{prr3tlL3gW3;DK1$ud*s)=QJipb2N&<|tDkw0U}{o)K#^ z*FziNJvdxb(E+(a}L3`JR>N>P1 zV^9VN+_58y8$BfI71T~{^ZH(G-TnQ;EC&Z_KXQ0CuE>S7w^emZ|Q8BV|-( zSlxOmXqh#uX8|<^>z-22WLscXgFexR>1f!tvjdZ}8;Gbn|*uF6^rfM^z z>V-NG9Q1=D&VNxS=kFRj|HW|rqu;3e(K9az)l0c&*ueXA&+x?OWubaGie=)10f32* zBQu^?^qBFyGVQF(u0~hJ?=SFmx?4mWfLX^YjPj&CP_Js9@;E%Xc_37;Zc#y|HLpoK z=kIj7%wn7iBui@H>b1^B=e5b~{kpWnbCL%`^?HuG1EFjjBNyn`)4J}_^itp+Wi}i`ayAg`X%IZDP@x7sXU+mj==w9bs_x@0QATEwuki7Q6UM+mc z;k@PN~i>oud-w5YElJK5sU9|XoJl?+0W4wKl$J^0Ry`}cNh8^Cg1NEg2 z!|uzW`bxq#4yz*L?yK`3cVEM}`&x3`eO-^cZ-nZbkexlYeI&Hn!1k_iFVoGIRKA zsQ$*L+z(SOY~=DFUiQ=ZAJfupHU-G$Nc~3M^9ak1GN%YUL-%%5X}adutWs&aA**=) zXeJfUKwCn?Kz)8$E#r+ZFEtAM)=GF(x1bvzQ2=gVm8L%E7{M_0 zi@x2~dT6nb*9oq*OPUuRW1Tag0mx~GWEw#ClH+WkqT`~knseI z_GSL@F7jwUP+{NxxP^3ph>|WWvNm;Crvrt4P=cONgYlgVp>paBop`2d^y%O+mHo#ywF+w7cz7>H!)IVU7b~NC^IZlT3TzfvyO~3%5}#7quLw+k?5o&aSLg+C}0#L z71;-^Aht#rGn%muV%I(jKx{LnP54YkW6&)IueG9QSMWMY$m_BhJs=_p^8zh=mE5zj+bX6yC1A+hl{## zh&@&?305aaO@P&jOjz6rtj0k>oScMPNGIdk3|3E!DsIW{)2sCkAax3AYLGfrWS=I_ zdX*L+6{!uS*~la~oi2=Rqooc`XE48Bpl!&ZGf@gV*5MY?St3G%61Uy}rL%>8PJ*6j zUO?$wp>=J(0ZQvd)OiU}3HJ*qo!?s&O@J<-bb*N4phac5zY`NcS+X}Ge;FPbaSU*g zF^liejxoibpZx}09qwbb&vyY#hF`Vvb|!TWuQc%@5Q7=+SlY)tu@d0(iCvI|LRmMA znpq8ITdyTGwT6m%nF)ZWMcQ?V4QX!0cMG;rvHbwDBaybB5b9R261JwyE1I!kO`i^*XO>iOxqKR8bo5kfuG3peK^VY<3u`p&eV?FU)|0qd3dUF9i3F^hU z=Mqu1Ywo#J$m_C4HFA#@GOzT5@B9Hh86rBQpGg#l^c0cbNI#Kkl74bRn{WEr0&b)q zz9rL-B|r+ZXr3iHfJC= zs<^vovlsgp|bF{!9_g}RhWOgT@-mBz@RE_ zAt{lfQxWUXNJS)cKS57uAgL%2T2~Dksi-ERwk1R*`ax3BY;RFC2fC!9?IP-_T2%Hy z_$iT!wn@g3{Wb=0w~fUOhk3SeEEj4 z%_`N9Sh^yA!O$En2_TL`dKz0ZQe?Lv2Sg)8a=>L`Ser{5IUwXZvmu-F z;#pYqNhfJt!R2p#*!9KB6|8C!e{U8Uz~5V#FxUxyZv_Q0a~p0U-HvNB{@xK)+>+fF zZtbo|2lTxYRWS4;2-L$59K#skLpx&u&9z)~{51EfJKd019Xu1$~91Kid~k(CBc zM*~W5`qk2Go0bineht?d|L>^vTF8dSUx!;r4~iy6G18U2l5{#R0KZ-s-=G=mD7q6K zd*o3x+P6z@gmy8EzDYFg3Zrip^1AHWc$h{q=7po!$QsaFAfW?}z7@qGy-mb7a5U0P zaP;j$n=g*O1Khw-z9n(=og##B^j-KB(!1r^$Q}TDI^kE{JcJ^vn8eTbNL}FPdzrAL z6MnuA6hzDWaSQ1KxHjYG2cwExI<4Objz{fx0MHMit_IK#i}a7kvyole(lbz;O-&-` zM}@U*0M!xnW6VF^CW$_da#-^T+(P=K2+?T68Z;pEQ$qiAf}T)7AoMdr>#9Iw^7>g3 z^|^$oL?;MjeZIFS`U2#iF^ZAK?4efp`lc{`OEcEt>uPxK5rr>)L*Rn|^lfMsL)Uji%dXJ%T_Ni& zJ8}Ja!4~$|2lPGE?f_fgM{!6$5YY|TinJ2gdPr#Vg{>cg8?eQH)HT#)RQcko7Pqh>)M-7Sbv| zQ-2crpA+;%;{sEE5n9*A8!+`(5%sr(sD$GMrvBbr6#WBqfvJCrs0G-w&foi6W>(Qs zF3*@+Kk<+9e5<~Si#6WO7$W#Il3Wzl@4&PW!Vw#+Gyp1!RRO3)(rlZ-4S?E(tG3Oo z{xfRr3fbtkKHNg;7fp;}q$_)+6`B?cdnHNam08l_nA)*6F4WT%sVUgbesYo>esbxZ&FGwv1H$aMSNs!u2gfK|0z^{;Y zm*yHc_+Bq3lv1n-nfOd53bE{wQp3h;|gJu79B8a zKUCJ3wZDizK%VsqVRPM)BAc89t^mcUWD}+s=$N&Kbjo=p2Dv_emi*;zA z*TF(RBtcJTAn0|d(7I~SK(E6@)ZqzHiGC3DO7|8;qo50V9U-EQ)S|L)YfQ3u>eRAZ zsccn!W175jhiR95Ue}+G5>S@d(G-1PB3TKncrn_oPVRKp>y7nRGbugc^9|lwXSjNM zwcu#M!LnlX^>i*ntJ$iNBD)7T84VY~$u(kNn`;|5nc+I)k97x(!3u=pTHHc9 zN^CZYkyp5<6Nrox75bmWyQodP=S2rnZKB74b;2B~AAaN$cXYG}JqA~E00n&J#iz$h z;jvt}+}~cF@=Ce5mQPPW@gyC`#NC<>Q;NLfg?a*0_h_k_6NVFoVVoHT^R7O-7WDB>IvvkA;W?C4 z)~7S1>X}@1DAq%3xDI9bKFyTnygb57cR3ss#g`PH&X#KDFmb@Q1AZUz>0CS}4wzst z@$r>HYBR5q`zcxvx<}{X8q)d9Yp#g>nd7sMuyU(U6QDrP-O(O)4C1J36*BBf9_RY# z0?;PRr8J#K1sr>=QHeI7%-qijg6aulV}a-4w6tpn(;0kh9dm+mX^b{92i8pSby<#B zUZ`6buG&>L)y65)JcM5)!iV`hNEJtK%6!t=r%h5i6nNZZtfR>i`}nlscG}Fphe}pu zriK%^s93P+VlG=`SBOvhnS%x1^%z*?Dm@9$Y=Cr9u4HesOLPg!aFlZ?OvMRb?2yC) zDP1azDaR@W5!;iQtQxy=+$L~TDh95hpHQq`2*4D?y91&*0NR>E*B((v|s=XKl^dIJa5K)z7+68N}<%17K>o4hM}e%E=}Ms(@*y zxr<%dMxF@MhF5*NR)Ce|5>=&mxjk3)5XkYF8lRMuuVC-t$hVxtjl4o4bBR8goa4bl zzAz8-ct`|tAf?M%dW>o)Hv4M9^qS*n8~?^h9X=i|qi9x3ax2MBra5LClqzd*vXr*- z^RisQ^?4^5pHE{aK2_=t0%@>1G40OL)0mBioHdn`6$-ixCF=*w;pQNvkQw{TP3xf2 z)A{$}=*T)fgP%(O=39MyDEfbOl$$Q+CI_RFPl$pYc%HyPSdQ36|6C!ZOF0PZ5OgKi zJSc*o+1Xi~EW`1dia-$dE{}oPAET?7YyV~sw+z#(!BUjrGt4lZYq<0puDr-C(6#(y z7#1c&<2n@XH22fcV3%xxF1S8$a&o8(=(ZCq7-&(S$pG@{dK6BYd&+3SRv7Ho2KHc# z%RWK60aT3HB{-H}FS}8f4T)Fk$bS5t#bWrkZbS2X>%2s&-wCN12K zD_(Vu=8qY7NNboN$=go+p9$csJI=M^M7uc{ry-y21nVT-g{#@m!Cdg^Zajo^53aST G%>Mu#HN diff --git a/documentation/build/doctrees/graphicsItems/graphicswidget.doctree b/documentation/build/doctrees/graphicsItems/graphicswidget.doctree deleted file mode 100644 index c6218a6b168c675314eb61b6b70629f5e2d4e4c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5637 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 Jhu?ZO^*^iH>%;&6 diff --git a/documentation/build/doctrees/graphicsItems/infiniteline.doctree b/documentation/build/doctrees/graphicsItems/infiniteline.doctree deleted file mode 100644 index a39e2dbcb07baa1db254b8a582feb803e0b02dde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11533 zcmeHNd6*nkwNFB3GSkUS781ajgcK^!3CVO693dKDPxrybWzTOWjeqaaC(sRo-bIMn%Beg~!ZW`p9<%a3|MjfgR zmDI34Tn&k!e%bdzs+wx;q@JspRgVnDR_iA94MrGvQ=S*NUSO8}z@R{_pIEN1Vk6mL zx*<|W=B$efWJZf<*Zz&4^%DNsezl?R8mKEJHj`s%rd8rh98cJ)Pvv$ zGG-IGM)YNRKNNtdYSdbx_b2U}yUCE*q|mjiaBfa*DNe&|1hBB8hF{2PJ4Sw>j=koP+8QaX zqOS^!I$KB`r>{y>QxDd2Mi7u)2?N;wA(47$ULUB1O@{N;@g;p#)iHb@?fr?R` zA@<&hCH)9CbiSmvSsM+#FNWV4#gR)-l#*YpZpf-X{p^WDDY+lDCd8StpdN6H8XUWNj~n7Pwjo zuSayV+HcNbSdA~F|43Nr)JUBM<`(nX6;_O}QuUjb?aqxEwOTwhZMSC5<eLpWqr)$(F-&33hx zVI;fpF(W>bx%DR&uX;G|RR(kD=nl!k&Lv@2`0c!*>hQg)of3K^DGrJOl37*h2#U)h_4rEGkE+N> z15=^n$Y8pBVK7~x_r*rK5>rIql+DBN35T;LDlK6m!^M;)qdTXb2pg3n^(3}Ye``<1~4h)R2_0zk+NB? zT-IJ=8QIokanv*mHq%Qm2ZD7HQw<2`M#@Wtvw8>@OV)&BDpCXt>xzspgCdN0%V$Y~ zUXp~6gtKRMOsWHrWj0cCsVu68EJz8kQ(%%~`HtSj1xH*GB4a$~+yxZRVCRdv-5o&qMgy*dh3E}fTL1Mnw)wYnA{wxx3lgRbU)Abn~pK> zYlg=4h2~8mOU8w-=9q3hu*P!xE!&TK8^*j=F|``V)y+k2%z^x>iuw##WuS+j#(~`J`kx7rojPIqCB#s zF8WZ9y6D4s>+Iro*gRcksb3FJ;ubnll`^}#&Fr2? z{hFCA$VT1j{iYYk--6=;3hH-Y`};`!A!U0{o9(@k`XjUL%17Ny|I~}=KY(c$zWPV7 z{HI9$bIS5wVHxK$i!;`rBlRz7XFALzNBwIrR{z$$io^y6Z-ek&MqK0lJ81tUQvZ?C z{xfpNAq=7iDtC;v2H=0j>x2$@)qim+`R^Vo)3W7Q=kdxtyDinJbs5`DjT|ip4p$l3-w&lD5Y*$>3GP<>YN> zmt2g6YOq)H2p{pHSvz1jp{Y%w>3~TAQn~{}lYJ1yiQxzpe=H}1jT!CK_V6;2Gp$q& zobk;%nMm~7E)CT}8+V6xnzW>3$8s2gEot$JA|munZPvm&3X56J;7l}^RD;ZI-;}nW zLM%;Fm9#{eOeJDyt~uK-LMrnD4APW~3p|ngBC#1%y0T%rJG6!|SBlrW%vOv572mNkNDs&+_2JnbzP>Ar32`J{|qnqOBGzStBUCqz&;?a`D7L6oY1CVPD(GVUH z4da(XuNT5vra3yz(qLwQ(kod?S|{J#3bgH!qxFD*8XNG4=t%s=108B?l#iv=*o1xy zbu1kvxJSph#pmMr_kn=X^fxjP=ot3X=rnsc9uOAS+w>suSGRk4hY=wjr7$6G`v$@fTcvm}Oi@qZyQaO(&w zFNq&}!P6{ZvLN8K6zveye2I2)$G{>hDjGu%LZ^U7M7!|Yv9zLxC5Vfxu2QQmTwT$_ zfh7wpI$7wRBH!^ctA#c?&H5$c@=A*yAz%yUo^-KA;|w3~%B583k!XYbr{WROX+l7J zGtHbOuq~{;Xt(sAp6Smp=S3KuA$?0TKNJ1D5~H(()T3fjay}sThxwUFq7Sdf=xl*m zn&cjaA8(VObI<@~&c!35^8`CqhIwWODoT5Lv_S36KxN#|H9nm$eM|FyjP!G^-Y2v! zh-nq~Aky|5GbXm3q>ePGF2}R7b(9p6xvoO;^zK5ECO{2FVKLjoL~_i9!e@b8t!*88 zEaQy-Kglr(Vle7OctmuuupXICgk!zqAYm(9A|Q{8L5gKqVG_r!C{&Fgtya0FmO2!3 ziQ(2X#NlOJo8z-;!CJ6cS0S3i`NmS!C|wF(i`!_wNU_jHE0e7+(iwI9uOj7TUBMBG|}uB0AeF%^ztBxm*CRhyhzE7NFw~mCXuQT;UrSSTSTUO zj}(uEDjgemZN1`U*fyF1C}NTY&E}H2P(UuRxFfe%F0s*rIGM&Hq8a>l<`O4Cyj-qw zEH1?&$)o|aIGMOY*pu%OIk{{}4}n?HY&K~M+=3C*$|j28$2-Xop%E(hctjKkg?I*L z-tBY}O8^o*^7-G0gaeTHlI9MfR?8D6o!wFJ7XFY zp#|zZ6_1FXCg`y`%(XkATqRJ4GEkWqkc4uz^ervG)6p+$zh?-kYhqHx-3Yo%&3F#Y zV_|PM!iFsF`33W+<9M@t!kw(}Tbm8_I+p!o^h~BYI?duCkt9P!63MfK!2)^OiR4pn;EUeY#E>U%-v){P{-3bIK`}PcKAsiC)B=>pQLm3B4Cf?@PFMV;5>! z0A4Bp*E7Ii)sBzeeR`R+z8t@tU-UKUjU?Xcp;w?8caU1Tw5~7FD+TRU_+{yv9$|Zd zUM=7Qb}hcBL^t3Yca?^+!us?YL47Tw4yAI$621;?5xt&!*6{%r;Zjl=SDxx-;L{rf z?Ty?yfYBcyLVUUrpPBubDUw6GT24~4pQD@5@6ns^8_}B?R^O1?v&=VqpyiBDZ$S^_ zJQDnHGKNcA*b1weZWT9J=&k6R)Ys5d6$rS%6_P`5Lz_No1wnInp%8c;uCv)@Fg4~; zyr1#;Q6%Z$t9rJ+c z(EHJb3rlODD({_#K7A;WFTNc`A7VJ`C^n!j$xNW}{+q$jm#YZ<%I5A4U6N zeTx?c%`jMs{>PY5KZ{lk=vMw1;+tM>*eK8OxTBAw4PISa3meUHD};Tz4ec0>*z&dA zDK7#3L<~GUYmm#jfUapY=#v7wiQP73JL3H^_JW}b;{8+T)CbVF^Y~q-)acXvx!#<^ zr9oIe2yw|xjQtrV-KY2CqqnUG0s4XvixqGBW>|xk4Truc&FjoLY{!|n z-3S$2+$^FmN&5!29xgA+jkClmUly9H1+I+OGxo3`g%;scC${VZ!p-PpzOpgD!P-u*OqIx&xr!DE=K&O;0|J@X+jEn zj=sfUJmic@dDgB47JVBn69f8iXOPl&7_d*@zZW8Xm)}<+2>HAsI9kZBn_!#qis+Xnp!Bd@S)bT_{YL%(cb{1lCc_0f1@Fdb7S zE*L(r%E={4`WZkl%o2~u@bT&AXe{YR$yl;ooB_zmlIgNP;;JUDW#U4nKA2_~pZ);Q65Wemy`N*W Q=F=bX5z(LU8&<~t2Lxxf82|tP diff --git a/documentation/build/doctrees/graphicsItems/labelitem.doctree b/documentation/build/doctrees/graphicsItems/labelitem.doctree deleted file mode 100644 index c40e04fc5cb0e83b4c3a7d6563e6765622664f31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10554 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 QWyBxQi0O}bMwQ|J1z5RdssI20 diff --git a/documentation/build/doctrees/graphicsItems/linearregionitem.doctree b/documentation/build/doctrees/graphicsItems/linearregionitem.doctree deleted file mode 100644 index 40deb4cc264d585f8cd14738e4348202cdc635ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8537 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%0D2e>yX^_c>wz!q`@mTT_WI}nVqD1C+nPxhf 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 W-#qWY0>91S-v0wVWdXSW diff --git a/documentation/build/doctrees/graphicsItems/plotitem.doctree b/documentation/build/doctrees/graphicsItems/plotitem.doctree deleted file mode 100644 index c80871dc71a470b7015aef6171feb3bcf574ef48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31151 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 N!wu#g@W0&G`G2C*dQ1QS diff --git a/documentation/build/doctrees/graphicsItems/roi.doctree b/documentation/build/doctrees/graphicsItems/roi.doctree deleted file mode 100644 index ad83ccd5272de86afceed00c3bdd418b5dd12ab4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21560 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 CvFO?W diff --git a/documentation/build/doctrees/graphicsItems/scalebar.doctree b/documentation/build/doctrees/graphicsItems/scalebar.doctree deleted file mode 100644 index 3a56a7bf36f16f0068e1beb3da1d156a6c45f0f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6287 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 HkM;i#1E}~O diff --git a/documentation/build/doctrees/graphicsItems/vtickgroup.doctree b/documentation/build/doctrees/graphicsItems/vtickgroup.doctree deleted file mode 100644 index 9ecc872bd0f4ace1a28c677782eca5d308a041f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5951 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 diff --git a/documentation/build/doctrees/style.doctree b/documentation/build/doctrees/style.doctree deleted file mode 100644 index bf141c9eb2864d1a239d2652603cf3c30b407ae6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5421 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 F@j7T&e%1g0 diff --git a/documentation/build/doctrees/widgets/checktable.doctree b/documentation/build/doctrees/widgets/checktable.doctree deleted file mode 100644 index d64858a9d00470e951d0f210c8d040cca12c2ca9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4775 zcmcIocYGX26_#a7x;smJW=@4pw(0lK__ufnBz1Ms*w|hFB?fZ|P^t<2P?wj|%_r0?3?KOk07r9C7 z2WlpaJg!RmXOo9y(-Jcm2=2}w zcIpAA;n_9S-KV%YW7~e{$F@x)kSamS)dyp9uuw_ZMwQf6?8kr;g|^2Vii#uWU1#@$3p;IvhgNSD)Cg_S(+M$*xzX}rQFog|JLQ51(!%xx7rGNui) zYuJeP#`hFWq_oiz!}uG)UxAgn@JuGG;0izRV1qW*Xmc-5io4oSiMD{Ittp)bnxwOk zq1na;*f3xNAZ-_eY&g5VqgRdOEt!PA3+_v_vswo$Wf3)bIY{Ptc_EVX<)#z*$_|`* zSxc22m-d$fzh2f+D|ez$&I4!sFSqTL7kqDy$Ev*I*^E>%?Yj0T?M~@dbv6<^bD9s@ z!$vZfXo{7bIF^1riNW@*Q@V}C3SQFEa*9r`u@N_Ll*0EjYOGLq+EwYSFBYFf{% z(QU<~!-m@OR*j}b!AaPLWFN?#NvUj!Z6;&7 zQtfQ9U2GLQ#BQ;N4H{EOB~GoKX{dYvRnBuxg2bO_5o;W3QpZd=a_6zgPR6 zz|fsjx(f^oZD}T{J4xMDE#ZfYGmhuAtHlpJzF49IfO*%H&V@7^XydAASmrj}O~X3R z(rAFG4bB3KNDk?9RinFGNDx1qLjfJE?m-HyxH)ARuBZHPsuSvIowG6B!+~d&%(0VT zLp8f4x+kEWpVGa0psmw)=>m=H-WEzCYD2wnnGJhU)A2*Ps5)p0`2^Te{VLrH=;G>- zu3?=DbO~Y#h35LD;*u_4bRX*k*0iXtd?a3+NJM!S7!Rd%UmfL5tK7HTC`g*2nx+b9 zJzwl##q)GoBY_Um{Xp^fYF%`H?a>1?*YrR!?6BcxBnV7ZRD1K{ayHcV_zG0`P{Y^N^pNFle;;}Zf1MD0$P{OZYH;|#N9``Fxp1^H8NN3b`i5kE) zmy*!9bc61s+)Nj?(e5+kYhYLP12zv}tCdjzaLts$9Nb(VaBXA}khM}GoDi3Hfhig= zCRS1-QT>p_8IqOqq6A2eq_mJDY4t&Zeh!vmE(2eoXDkBJQc4fckLIzPG>}#a(<3yt zNA|;(p1evrYD)Ad*tjjXGc9r9vN%O=6c?KcY%8bh#iioV6hpBXmXF zl+tT8(5@^7&KonFrUG8qPs{6jD&P%(`Novql;gRns{(}1@i(XRmi%TO9h22>?PJCi z5NfFX@8~gst&gP>szxN6T+-VrY^*b@nsMswDZPUg@KrZAMej`MU2H-vHS1Ae=aP!v zU8DD~jjJYIbNanCdLP@|BW{~p@2}AZSkd*{4h7|ivI zN}s9GXIVk4Hu$VClVRZSJ+Mw$E|bAJPJ3k7-3w}tIhYZ=?Eu? zGl3t>4zaNeL^hWHup-W>uw7irNZLRed0^CT{}fK>1qj@a*o20ta74gh05|E!Y{=vF zWR8APVH3Pr=Q^-U0at?an0~s>rM?-SDK@skD`-+p49x`r|TN%fln|r%L8mOYy{mAI*UNtm#{9MaVKAPe&O# zfug^lo-`*|a?Q4qdHmO9w!Xcr**bgaZ-A+Dv@6U04ii1Z(?8fy5;yi8pntMbc3da) zzi`?QyF*0(Wg05D_=CaozFk z4u!I$PY(K?T(7^3Bf-1kEk|*2neA*BEDyNp8elsXcD7Z^F^HftvH+R9S8^N|Yiz42 z0zXt-nwKOz)S>j`2HZvItj8M&6leR|MEly9p(Jmsa-&wB)Y+EaXM=e`A}95s0kg=< zP57S5&G^)BNe*FHIxZ%~)&zOn{i4P^pX3%JycM6A)Xu_`Cr>j6vF@6^H-80!eI2_w hwsKa?Tep(iAX<~#@yUjD9(qddz>ie!#Ai~UxdkBOoWcMA diff --git a/documentation/build/doctrees/widgets/colorbutton.doctree b/documentation/build/doctrees/widgets/colorbutton.doctree deleted file mode 100644 index e9352b2bf15d4fe1369cc6faec90133ecdd4df05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5607 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 diff --git a/documentation/build/doctrees/widgets/datatreewidget.doctree b/documentation/build/doctrees/widgets/datatreewidget.doctree deleted file mode 100644 index e9673c847cb734c4b6d099d63bd38b54f296de0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6995 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 V{2Kinm3E+Z;w4rtexvf(zX8Q%IO6~S diff --git a/documentation/build/doctrees/widgets/filedialog.doctree b/documentation/build/doctrees/widgets/filedialog.doctree deleted file mode 100644 index 17e770c9cf535b209befd0b13fdf625fb7ad9cb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4763 zcmcIocYGX26_#Y5ba$3yNiK24g2Z8!U|j+RObCWR;v~o%%@Zzx%d&SnceByn?!B2^ zO9Ga}K)|9C2%$r$0YdM+_uhLiq4(auncF>`Wc&W(C;jfXxBKS3?|rZAdw0!H-3{w; znv6XoEDFk+Fa54gmPA;I505K@O#z_Qn)^&%JMa*J}-5w_Ey)RcS^HJ26}DdjY4j=7yjwO)@%L3<;t*836(6d z%_QAQV$nHbyVxprh}~ij%NcPxR{-qoEpd8PoKY2ftK!V6u&S|uO_5r29cJbbdy3cL$!8 zQ^!t%4b@yP(mep}f`sn52HJZ4lYx|SZi-0ST_PT{Q+pbMGOEK(KjUYU@s z-5t&-oQ8*okU>of{L!Jh9RX8j0$MBubI26c@!nax|fZ3`x5W5_EI06mjW$3LRq+kd_j9Savjv-IRf}N|+w5 zu|1+6w&dhB(p6KWN5bCK30(twrtVB(fQqFwXF8?OJwV5_%}4dK`DpN=E7)kF$AF4! z6MAgs&(($s^gMow)?(MgAToo(;{fe=Ld)4v6sk$%PO7A0^mq;E3H<;)(Gt3mJ}IFm zXR3~i9cZM&bkL{t>7Y-w#KDRMXR>j_nwhp%@^>ZV(+Hsm-5LFwkLYQuTd2^@^16hc zzQ(+1lh3fkMa$w8T~SwCMxGcBQe+zkmm3#uzS0CWXXcSOpX-Lnza z>pC0?)6t$|I@)u^h$BwyWY0t3o}bVQR(G-&YT-&{)5$G~D^NzO8`q00p{wl1gkG#6 z=6kpVoEuU&rpjK@FNiNiR`njHmjU0)6M99)>5V3_gsz{P5_)BJJF{SFqR^|Td0r3q9y)nabb5HIIo!xIr=*`)~EHWma-_pm3 zDIHW#=ikwz0oxEsCs2)0wz#CXmf2W$J~boM+Y)*^%i~hFG)3=7=$&jrEwyT)Z)cK< z-c_Y{v&mJ{tvUUkD!rF&UL$UsNAIiB`&psxbzVV=K2W6(>dAqPW@EG&&OU^xxr=aR z;QnxxKB8f@Ly7)JA1$-I=cW_&$1qWk^zD{lzU_=J+ud6Z%|8pHJuu7Msebu=VT-;PZ;U2pF57 zI+QLphc?Gt7Su6Z(U;0>R5t1m#R?HipT5j?iYRKIS1Lteh_%P#QDZig&60>(eu=)q z3SgklwQBk*w2rrVTl>H<%-5mtYh^aBFu1kR09$%fQuK8MG77bg49+)LPEU7B^i4MA zJ3%wX;sSY>)3?|VHi~bvg6m+qj+|DTzM~-;a&4a<GzQQ?SyK_70?k66y- zwYW(?F0%>Vs&O6IC7&z7c|<>1X1l{UYRA#3bp8~hE7+(|7<^_!~75jxIdBKr^1`J)?Y0)psY>Vc-fkjeB&5n2rcq2}~VtG8+ zxA&a=F8!KK@~Ao!hIslr%f(S+-vRmuE2hVF zLjMz|y`VQl^e&0 zz`$Idjjv86If&aV$BrD@cR&v5>ydQzmh1Gl+W&uTmFu;kLX!s^TYe~I4&ULtNwF^J za@gD*)#;NJy&Tae_oQ{du&^-OUcwjyBr4 zDU?Nha>#S#27NJ(1n-Hr9L3FLwzE^P+~=lifbB@w=_W15AcD$B17z}E$#LAQvaO~F zyg+ejei-3_4y7wM;wegJ1AcWtadz%abncBAO0u>pC$;jp#1t3NLe$H?Q}A7{+_Np3O1Tk(k*?QG0<@-%Z0E3Vmfv!6Y%qhlY( hCe8|3>sE3bM5}T;KH0F&Ls!Wi_>#z-_>60_{{tt!mRbM+ diff --git a/documentation/build/doctrees/widgets/gradientwidget.doctree b/documentation/build/doctrees/widgets/gradientwidget.doctree deleted file mode 100644 index 6d09b2c6e0f444eb359d0edfc92cdce636abb0e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5742 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 MCD9k)H?EA|51$LLIRF3v diff --git a/documentation/build/doctrees/widgets/graphicslayoutwidget.doctree b/documentation/build/doctrees/widgets/graphicslayoutwidget.doctree deleted file mode 100644 index 6fc16da9f247b1fc085089824fa13e51cfafbfb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5215 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 diff --git a/documentation/build/doctrees/widgets/graphicsview.doctree b/documentation/build/doctrees/widgets/graphicsview.doctree deleted file mode 100644 index 179bef42902ae1ad6d85143a1dd876e643acfc86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12436 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 diff --git a/documentation/build/doctrees/widgets/joystickbutton.doctree b/documentation/build/doctrees/widgets/joystickbutton.doctree deleted file mode 100644 index 4e22f97e174bae06d02076c8f3627aefcc26bae8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4859 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 diff --git a/documentation/build/doctrees/widgets/multiplotwidget.doctree b/documentation/build/doctrees/widgets/multiplotwidget.doctree deleted file mode 100644 index c548d00196cfbe220ccaba342825a83200f9ae2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5295 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 diff --git a/documentation/build/doctrees/widgets/plotwidget.doctree b/documentation/build/doctrees/widgets/plotwidget.doctree deleted file mode 100644 index 29b5c88905e3db6f5b0e86ac7636e56aa0bc8adf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5476 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 VFB{XL=qPzAUSfF~exusle*yIBbF=^e diff --git a/documentation/build/doctrees/widgets/progressdialog.doctree b/documentation/build/doctrees/widgets/progressdialog.doctree deleted file mode 100644 index 0c6a7f2c6f99217919fef5e111cd73a7fa39f810..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10374 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 RO6(RM!b3#A!8I&o{u^RKDUSdE diff --git a/documentation/build/doctrees/widgets/rawimagewidget.doctree b/documentation/build/doctrees/widgets/rawimagewidget.doctree deleted file mode 100644 index 8bcfeb1b13bc4713c391f267bbcf7a3cc8793c4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7770 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 diff --git a/documentation/build/doctrees/widgets/spinbox.doctree b/documentation/build/doctrees/widgets/spinbox.doctree deleted file mode 100644 index f3b88a281239619f3f08a306c07cf7a54ed2346d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12210 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 diff --git a/documentation/build/doctrees/widgets/tablewidget.doctree b/documentation/build/doctrees/widgets/tablewidget.doctree deleted file mode 100644 index 21539df76c1e7391e6eb7963f1f8facf975c44aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11304 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 f*%L1V; diff --git a/documentation/build/doctrees/widgets/treewidget.doctree b/documentation/build/doctrees/widgets/treewidget.doctree deleted file mode 100644 index 3d17105765b1a75a0e515527823f7e27573d5da1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7154 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 diff --git a/documentation/build/doctrees/widgets/verticallabel.doctree b/documentation/build/doctrees/widgets/verticallabel.doctree deleted file mode 100644 index c4d66a574b2c32db97c8db7d2ed317a335ba0e85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5483 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 iqFJJK8 ztR`?nlv4*1G0}0yX!bl^G{UFh7boRCIA!b8%|zVJm}{Es>JB$dxJ|-6lE`P - - - - - - ImageView — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ImageView¶

-
-
-class pyqtgraph.ImageView(parent=None, name='ImageView', *args)¶
-
-
-__init__(parent=None, name='ImageView', *args)¶
-
- -
-
-jumpFrames(n)¶
-

If this is a video, move ahead n frames

-
- -
-
-setImage(img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None)¶
-

Set the image to be displayed in the widget. -Options are:

-
-

img: ndarray; the image to be displayed. -autoRange: bool; whether to scale/pan the view to fit the image. -autoLevels: bool; whether to update the white/black levels to fit the image. -levels: (min, max); the white and black level values to use. -axes: {‘t’:0, ‘x’:1, ‘y’:2, ‘c’:3}; Dictionary indicating the interpretation for each axis.

-
-This is only needed to override the default guess.
-
-
- -
-
-timeIndex(slider)¶
-

Return the time and frame index indicated by a slider

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

PlotWidget

-

Next topic

-

DataTreeWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/graphicsItems/ImageItem.old b/graphicsItems/ImageItem.old deleted file mode 100644 index 726814e0..00000000 --- a/graphicsItems/ImageItem.old +++ /dev/null @@ -1,398 +0,0 @@ -from pyqtgraph.Qt import QtGui, QtCore -import numpy as np -try: - import scipy.weave as weave - from scipy.weave import converters -except: - pass -import pyqtgraph.functions as fn -import pyqtgraph.debug as debug -from GraphicsObject import GraphicsObject - -__all__ = ['ImageItem'] -class ImageItem(GraphicsObject): - """ - GraphicsObject displaying an image. Optimized for rapid update (ie video display) - - """ - - - sigImageChanged = QtCore.Signal() - - ## performance gains from this are marginal, and it's rather unreliable. - useWeave = False - - def __init__(self, image=None, copy=True, parent=None, border=None, mode=None, *args): - #QObjectWorkaround.__init__(self) - GraphicsObject.__init__(self) - #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) - self.qimage = QtGui.QImage() - self.pixmap = None - self.paintMode = mode - #self.useWeave = True - self.blackLevel = None - self.whiteLevel = None - self.alpha = 1.0 - self.image = None - self.clipLevel = None - self.drawKernel = None - if border is not None: - border = fn.mkPen(border) - self.border = border - - #QtGui.QGraphicsPixmapItem.__init__(self, parent, *args) - #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) - if image is not None: - self.updateImage(image, copy, autoRange=True) - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - - #self.item = QtGui.QGraphicsPixmapItem(parent=self) - - def setCompositionMode(self, mode): - self.paintMode = mode - self.update() - - def setAlpha(self, alpha): - self.alpha = alpha - self.updateImage() - - #def boundingRect(self): - #return self.pixmapItem.boundingRect() - #return QtCore.QRectF(0, 0, self.qimage.width(), self.qimage.height()) - - def width(self): - if self.pixmap is None: - return None - return self.pixmap.width() - - def height(self): - if self.pixmap is None: - return None - return self.pixmap.height() - - def boundingRect(self): - if self.pixmap is None: - return QtCore.QRectF(0., 0., 0., 0.) - return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) - - def setClipLevel(self, level=None): - self.clipLevel = level - - #def paint(self, p, opt, widget): - #pass - #if self.pixmap is not None: - #p.drawPixmap(0, 0, self.pixmap) - #print "paint" - - def setLevels(self, white=None, black=None): - if white is not None: - self.whiteLevel = white - if black is not None: - self.blackLevel = black - self.updateImage() - - def getLevels(self): - return self.whiteLevel, self.blackLevel - - def updateImage(self, *args, **kargs): - ## can we make any assumptions here that speed things up? - ## dtype, range, size are all the same? - defaults = { - 'autoRange': False, - } - defaults.update(kargs) - return self.setImage(*args, **defaults) - - def setImage(self, image=None, copy=True, autoRange=True, clipMask=None, white=None, black=None, axes=None): - prof = debug.Profiler('ImageItem.updateImage 0x%x' %id(self)) - #debug.printTrace() - if axes is None: - axh = {'x': 0, 'y': 1, 'c': 2} - else: - axh = axes - #print "Update image", black, white - if white is not None: - self.whiteLevel = white - if black is not None: - self.blackLevel = black - - gotNewData = False - if image is None: - if self.image is None: - return - else: - gotNewData = True - if self.image is None or image.shape != self.image.shape: - self.prepareGeometryChange() - if copy: - self.image = image.view(np.ndarray).copy() - else: - self.image = image.view(np.ndarray) - #print " image max:", self.image.max(), "min:", self.image.min() - prof.mark('1') - - # Determine scale factors - if autoRange or self.blackLevel is None: - if self.image.dtype is np.ubyte: - self.blackLevel = 0 - self.whiteLevel = 255 - else: - self.blackLevel = self.image.min() - self.whiteLevel = self.image.max() - #print "Image item using", self.blackLevel, self.whiteLevel - - if self.blackLevel != self.whiteLevel: - scale = 255. / (self.whiteLevel - self.blackLevel) - else: - scale = 0. - - prof.mark('2') - - ## Recolor and convert to 8 bit per channel - # Try using weave, then fall back to python - shape = self.image.shape - black = float(self.blackLevel) - white = float(self.whiteLevel) - - if black == 0 and white == 255 and self.image.dtype == np.ubyte: - im = self.image - elif self.image.dtype in [np.ubyte, np.uint16]: - # use lookup table instead - npts = 2**(self.image.itemsize * 8) - lut = self.getLookupTable(npts, black, white) - im = lut[self.image] - else: - im = self.applyColorScaling(self.image, black, scale) - - prof.mark('3') - - try: - im1 = np.empty((im.shape[axh['y']], im.shape[axh['x']], 4), dtype=np.ubyte) - except: - print im.shape, axh - raise - alpha = np.clip(int(255 * self.alpha), 0, 255) - prof.mark('4') - # Fill image - if im.ndim == 2: - im2 = im.transpose(axh['y'], axh['x']) - im1[..., 0] = im2 - im1[..., 1] = im2 - im1[..., 2] = im2 - im1[..., 3] = alpha - elif im.ndim == 3: #color image - im2 = im.transpose(axh['y'], axh['x'], axh['c']) - if im2.shape[2] > 4: - raise Exception("ImageItem got image with more than 4 color channels (shape is %s; axes are %s)" % (str(im.shape), str(axh))) - ## [B G R A] Reorder colors - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - - for i in range(0, im.shape[axh['c']]): - im1[..., order[i]] = im2[..., i] - - ## fill in unused channels with 0 or alpha - for i in range(im.shape[axh['c']], 3): - im1[..., i] = 0 - if im.shape[axh['c']] < 4: - im1[..., 3] = alpha - - else: - raise Exception("Image must be 2 or 3 dimensions") - #self.im1 = im1 - # Display image - prof.mark('5') - if self.clipLevel is not None or clipMask is not None: - if clipMask is not None: - mask = clipMask.transpose() - else: - mask = (self.image < self.clipLevel).transpose() - im1[..., 0][mask] *= 0.5 - im1[..., 1][mask] *= 0.5 - im1[..., 2][mask] = 255 - prof.mark('6') - #print "Final image:", im1.dtype, im1.min(), im1.max(), im1.shape - self.ims = im1.tostring() ## Must be held in memory here because qImage won't do it for us :( - prof.mark('7') - qimage = QtGui.QImage(buffer(self.ims), im1.shape[1], im1.shape[0], QtGui.QImage.Format_ARGB32) - prof.mark('8') - self.pixmap = QtGui.QPixmap.fromImage(qimage) - prof.mark('9') - ##del self.ims - #self.item.setPixmap(self.pixmap) - - self.update() - prof.mark('10') - - if gotNewData: - #self.emit(QtCore.SIGNAL('imageChanged')) - self.sigImageChanged.emit() - - prof.finish() - - def getLookupTable(self, num, black, white): - num = int(num) - black = int(black) - white = int(white) - if white < black: - b = black - black = white - white = b - key = (num, black, white) - lut = np.empty(num, dtype=np.ubyte) - lut[:black] = 0 - rng = lut[black:white] - try: - rng[:] = np.linspace(0, 255, white-black)[:len(rng)] - except: - print key, rng.shape - lut[white:] = 255 - return lut - - - def applyColorScaling(self, img, offset, scale): - try: - if not ImageItem.useWeave: - raise Exception('Skipping weave compile') - #sim = np.ascontiguousarray(self.image) ## should not be needed - sim = img.reshape(img.size) - #sim.shape = sim.size - im = np.empty(sim.shape, dtype=np.ubyte) - n = im.size - - code = """ - for( int i=0; i 255.0 ) - a = 255.0; - else if( a < 0.0 ) - a = 0.0; - im(i) = a; - } - """ - - weave.inline(code, ['sim', 'im', 'n', 'offset', 'scale'], type_converters=converters.blitz, compiler = 'gcc') - #sim.shape = shape - im.shape = img.shape - except: - if ImageItem.useWeave: - ImageItem.useWeave = False - #sys.excepthook(*sys.exc_info()) - #print "==============================================================================" - #print "Weave compile failed, falling back to slower version." - #img.shape = shape - im = ((img - offset) * scale).clip(0.,255.).astype(np.ubyte) - return im - - - def getPixmap(self): - return self.pixmap.copy() - - def getHistogram(self, bins=500, step=3): - """returns x and y arrays containing the histogram values for the current image. - The step argument causes pixels to be skipped when computing the histogram to save time.""" - if self.image is None: - return None,None - stepData = self.image[::step, ::step] - hist = np.histogram(stepData, bins=bins) - return hist[1][:-1], hist[0] - - def setPxMode(self, b): - """Set whether the item ignores transformations and draws directly to screen pixels.""" - self.setFlag(self.ItemIgnoresTransformations, b) - - def setScaledMode(self): - self.setPxMode(False) - - def mousePressEvent(self, ev): - if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: - self.drawAt(ev.pos(), ev) - ev.accept() - else: - ev.ignore() - - def mouseMoveEvent(self, ev): - #print "mouse move", ev.pos() - if self.drawKernel is not None: - self.drawAt(ev.pos(), ev) - - def mouseReleaseEvent(self, ev): - pass - - def tabletEvent(self, ev): - print ev.device() - print ev.pointerType() - print ev.pressure() - - def drawAt(self, pos, ev=None): - pos = [int(pos.x()), int(pos.y())] - dk = self.drawKernel - kc = self.drawKernelCenter - sx = [0,dk.shape[0]] - sy = [0,dk.shape[1]] - tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] - ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] - - for i in [0,1]: - dx1 = -min(0, tx[i]) - dx2 = min(0, self.image.shape[0]-tx[i]) - tx[i] += dx1+dx2 - sx[i] += dx1+dx2 - - dy1 = -min(0, ty[i]) - dy2 = min(0, self.image.shape[1]-ty[i]) - ty[i] += dy1+dy2 - sy[i] += dy1+dy2 - - #print sx - #print sy - #print tx - #print ty - #print self.image.shape - #print self.image[tx[0]:tx[1], ty[0]:ty[1]].shape - #print dk[sx[0]:sx[1], sy[0]:sy[1]].shape - ts = (slice(tx[0],tx[1]), slice(ty[0],ty[1])) - ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) - #src = dk[sx[0]:sx[1], sy[0]:sy[1]] - #mask = self.drawMask[sx[0]:sx[1], sy[0]:sy[1]] - mask = self.drawMask - src = dk - #print self.image[ts].shape, src.shape - - if callable(self.drawMode): - self.drawMode(dk, self.image, mask, ss, ts, ev) - else: - src = src[ss] - if self.drawMode == 'set': - if mask is not None: - mask = mask[ss] - self.image[ts] = self.image[ts] * (1-mask) + src * mask - else: - self.image[ts] = src - elif self.drawMode == 'add': - self.image[ts] += src - else: - raise Exception("Unknown draw mode '%s'" % self.drawMode) - self.updateImage() - - def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): - self.drawKernel = kernel - self.drawKernelCenter = center - self.drawMode = mode - self.drawMask = mask - - def paint(self, p, *args): - - #QtGui.QGraphicsPixmapItem.paint(self, p, *args) - if self.pixmap is None: - return - if self.paintMode is not None: - p.setCompositionMode(self.paintMode) - p.drawPixmap(self.boundingRect(), self.pixmap, QtCore.QRectF(0, 0, self.pixmap.width(), self.pixmap.height())) - if self.border is not None: - p.setPen(self.border) - p.drawRect(self.boundingRect()) - - def pixelSize(self): - """return size of a single pixel in the image""" - br = self.sceneBoundingRect() - return br.width()/self.pixmap.width(), br.height()/self.pixmap.height() diff --git a/graphicsItems/ViewBox.pyc.renamed1 b/graphicsItems/ViewBox.pyc.renamed1 deleted file mode 100644 index 29807d792d9aafb084c0454aba7c8538d67b04f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22064 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 Date: Fri, 9 Mar 2012 12:38:15 -0500 Subject: [PATCH 014/238] OpenGL scenegraph updates - volumetric rendering - isosurfaces, mesh rendering - basic transformation and parent/child functionality --- examples/GLMeshItem.py | 67 +++++ examples/GLViewWidget.py | 13 +- examples/GLVolumeItem.py | 57 ++++ examples/Plotting.py | 15 + functions.py | 534 +++++++++++++++++++++++++++++++++++ graphicsItems/AxisItem.py | 13 +- opengl/GLGraphicsItem.py | 83 +++++- opengl/GLViewWidget.py | 88 +----- opengl/MeshData.py | 25 ++ opengl/items/GLAxisItem.py | 51 ++++ opengl/items/GLBoxItem.py | 85 ++++-- opengl/items/GLGridItem.py | 48 ++++ opengl/items/GLMeshItem.py | 93 ++++++ opengl/items/GLVolumeItem.py | 72 +++-- opengl/shaders.py | 41 +++ 15 files changed, 1150 insertions(+), 135 deletions(-) create mode 100644 examples/GLMeshItem.py create mode 100644 examples/GLVolumeItem.py create mode 100644 opengl/MeshData.py create mode 100644 opengl/items/GLAxisItem.py create mode 100644 opengl/items/GLGridItem.py create mode 100644 opengl/items/GLMeshItem.py create mode 100644 opengl/shaders.py diff --git a/examples/GLMeshItem.py b/examples/GLMeshItem.py new file mode 100644 index 00000000..07f9f949 --- /dev/null +++ b/examples/GLMeshItem.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +## This example uses the isosurface function to convert a scalar field +## (a hydrogen orbital) into a mesh for 3D display. + +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +import pyqtgraph.opengl as gl + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.show() + +g = gl.GLGridItem() +g.scale(2,2,1) +w.addItem(g) + +import numpy as np + +def psi(i, j, k, offset=(25, 25, 50)): + x = i-offset[0] + y = j-offset[1] + z = k-offset[2] + th = np.arctan2(z, (x**2+y**2)**0.5) + phi = np.arctan2(y, x) + r = (x**2 + y**2 + z **2)**0.5 + a0 = 1 + #ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) + ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) + + return ps + + #return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 + + +print "Generating scalar field.." +data = np.abs(np.fromfunction(psi, (50,50,100))) + + +#data = np.fromfunction(lambda i,j,k: np.sin(0.2*((i-25)**2+(j-15)**2+k**2)**0.5), (50,50,50)); +print "Generating isosurface.." +faces = pg.isosurface(data, data.max()/4.) +m = gl.GLMeshItem(faces) +w.addItem(m) +m.translate(-25, -25, -50) + + + +#data = np.zeros((5,5,5)) +#data[2,2,1:4] = 1 +#data[2,1:4,2] = 1 +#data[1:4,2,2] = 1 +#tr.translate(-2.5, -2.5, 0) +#data = np.ones((2,2,2)) +#data[0, 1, 0] = 0 +#faces = pg.isosurface(data, 0.5) +#m = gl.GLMeshItem(faces) +#w.addItem(m) +#m.setTransform(tr) + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/GLViewWidget.py b/examples/GLViewWidget.py index 4fdbf129..802fa289 100644 --- a/examples/GLViewWidget.py +++ b/examples/GLViewWidget.py @@ -8,12 +8,21 @@ import pyqtgraph.opengl as gl app = QtGui.QApplication([]) w = gl.GLViewWidget() +w.opts['distance'] = 20 w.show() +ax = gl.GLAxisItem() +ax.setSize(5,5,5) +w.addItem(ax) b = gl.GLBoxItem() w.addItem(b) -v = gl.GLVolumeItem() -w.addItem(v) +ax2 = gl.GLAxisItem() +ax2.setParentItem(b) +b.translate(1,1,1) + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/GLVolumeItem.py b/examples/GLVolumeItem.py new file mode 100644 index 00000000..163f4b7a --- /dev/null +++ b/examples/GLVolumeItem.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +## Add path to library (just for examples; you do not need this) +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph.opengl as gl + +app = QtGui.QApplication([]) +w = gl.GLViewWidget() +w.opts['distance'] = 200 +w.show() + + +#b = gl.GLBoxItem() +#w.addItem(b) +g = gl.GLGridItem() +g.scale(10, 10, 1) +w.addItem(g) + +import numpy as np +## Hydrogen electron probability density +def psi(i, j, k, offset=(50,50,100)): + x = i-offset[0] + y = j-offset[1] + z = k-offset[2] + th = np.arctan2(z, (x**2+y**2)**0.5) + phi = np.arctan2(y, x) + r = (x**2 + y**2 + z **2)**0.5 + a0 = 2 + #ps = (1./81.) * (2./np.pi)**0.5 * (1./a0)**(3/2) * (6 - r/a0) * (r/a0) * np.exp(-r/(3*a0)) * np.cos(th) + ps = (1./81.) * 1./(6.*np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * np.exp(-r/(3*a0)) * (3 * np.cos(th)**2 - 1) + + return ps + + #return ((1./81.) * (1./np.pi)**0.5 * (1./a0)**(3/2) * (r/a0)**2 * (r/a0) * np.exp(-r/(3*a0)) * np.sin(th) * np.cos(th) * np.exp(2 * 1j * phi))**2 + + +data = np.fromfunction(psi, (100,100,200)) +positive = np.log(np.clip(data, 0, data.max())**2) +negative = np.log(np.clip(-data, 0, -data.min())**2) + +d2 = np.empty(data.shape + (4,), dtype=np.ubyte) +d2[..., 0] = positive * (255./positive.max()) +d2[..., 1] = negative * (255./negative.max()) +d2[..., 2] = d2[...,1] +d2[..., 3] = d2[..., 0]*0.3 + d2[..., 1]*0.3 +d2[..., 3] = (d2[..., 3].astype(float) / 255.) **2 * 255 + +v = gl.GLVolumeItem(d2) +v.translate(-50,-50,-100) +w.addItem(v) + + +## Start Qt event loop unless running in interactive mode. +if sys.flags.interactive != 1: + app.exec_() diff --git a/examples/Plotting.py b/examples/Plotting.py index b4018b7d..84979f02 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -67,6 +67,21 @@ y = np.sin(np.linspace(0, 10, 1000)) + np.random.normal(size=1000, scale=0.1) p7.plot(y, fillLevel=-0.3, brush=(50,50,200,100)) +x2 = np.linspace(-100, 100, 1000) +data2 = np.sin(x2) / x2 +p8 = win.addPlot(title="Region Selection") +p8.plot(data2, pen=(255,255,255,200)) +lr = pg.LinearRegionItem([400,700]) +lr.setZValue(-10) +p8.addItem(lr) + +p9 = win.addPlot(title="Zoom on selected region") +p9.plot(data2) +def update(): + p9.setXRange(*lr.getRegion()) +lr.sigRegionChanged.connect(update) +update() + ## Start Qt event loop unless running in interactive mode. if sys.flags.interactive != 1: app.exec_() diff --git a/functions.py b/functions.py index 3a249d9c..ab60e63e 100644 --- a/functions.py +++ b/functions.py @@ -120,6 +120,18 @@ def siEval(s): return v * 1000**n +class Color(QtGui.QColor): + def __init__(self, *args): + QtGui.QColor.__init__(self, mkColor(*args)) + + def glColor(self): + """Return (r,g,b,a) normalized for use in opengl""" + return (self.red()/255., self.green()/255., self.blue()/255., self.alpha()/255.) + + def __getitem__(self, ind): + return (self.red, self.green, self.blue, self.alpha)[ind]() + + def mkColor(*args): """ Convenience function for constructing QColor from a variety of argument types. Accepted arguments are: @@ -632,4 +644,526 @@ def rescaleData(data, scale, offset): data = newData.reshape(data.shape) return data + +#def isosurface(data, level): + #""" + #Generate isosurface from volumetric data using marching tetrahedra algorithm. + #See Paul Bourke, "Polygonising a Scalar Field Using Tetrahedrons" (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) + + #*data* 3D numpy array of scalar values + #*level* The level at which to generate an isosurface + #""" + + #facets = [] + + ### mark everything below the isosurface level + #mask = data < level + + #### make eight sub-fields + #fields = np.empty((2,2,2), dtype=object) + #slices = [slice(0,-1), slice(1,None)] + #for i in [0,1]: + #for j in [0,1]: + #for k in [0,1]: + #fields[i,j,k] = mask[slices[i], slices[j], slices[k]] + + + + ### split each cell into 6 tetrahedra + ### these all have the same 'orienation'; points 1,2,3 circle + ### clockwise around point 0 + #tetrahedra = [ + #[(0,1,0), (1,1,1), (0,1,1), (1,0,1)], + #[(0,1,0), (0,1,1), (0,0,1), (1,0,1)], + #[(0,1,0), (0,0,1), (0,0,0), (1,0,1)], + #[(0,1,0), (0,0,0), (1,0,0), (1,0,1)], + #[(0,1,0), (1,0,0), (1,1,0), (1,0,1)], + #[(0,1,0), (1,1,0), (1,1,1), (1,0,1)] + #] + + ### each tetrahedron will be assigned an index + ### which determines how to generate its facets. + ### this structure is: + ### facets[index][facet1, facet2, ...] + ### where each facet is triangular and its points are each + ### interpolated between two points on the tetrahedron + ### facet = [(p1a, p1b), (p2a, p2b), (p3a, p3b)] + ### facet points always circle clockwise if you are looking + ### at them from below the isosurface. + #indexFacets = [ + #[], ## all above + #[[(0,1), (0,2), (0,3)]], # 0 below + #[[(1,0), (1,3), (1,2)]], # 1 below + #[[(0,2), (1,3), (1,2)], [(0,2), (0,3), (1,3)]], # 0,1 below + #[[(2,0), (2,1), (2,3)]], # 2 below + #[[(0,3), (1,2), (2,3)], [(0,3), (0,1), (1,2)]], # 0,2 below + #[[(1,0), (2,3), (2,0)], [(1,0), (1,3), (2,3)]], # 1,2 below + #[[(3,0), (3,1), (3,2)]], # 3 above + #[[(3,0), (3,2), (3,1)]], # 3 below + #[[(1,0), (2,0), (2,3)], [(1,0), (2,3), (1,3)]], # 0,3 below + #[[(0,3), (2,3), (1,2)], [(0,3), (1,2), (0,1)]], # 1,3 below + #[[(2,0), (2,3), (2,1)]], # 0,1,3 below + #[[(0,2), (1,2), (1,3)], [(0,2), (1,3), (0,3)]], # 2,3 below + #[[(1,0), (1,2), (1,3)]], # 0,2,3 below + #[[(0,1), (0,3), (0,2)]], # 1,2,3 below + #[] ## all below + #] + + #for tet in tetrahedra: + + ### get the 4 fields for this tetrahedron + #tetFields = [fields[c] for c in tet] + + ### generate an index for each grid cell + #index = tetFields[0] + tetFields[1]*2 + tetFields[2]*4 + tetFields[3]*8 + + ### add facets + #for i in xrange(index.shape[0]): # data x-axis + #for j in xrange(index.shape[1]): # data y-axis + #for k in xrange(index.shape[2]): # data z-axis + #for f in indexFacets[index[i,j,k]]: # faces to generate for this tet + #pts = [] + #for l in [0,1,2]: # points in this face + #p1 = tet[f[l][0]] # tet corner 1 + #p2 = tet[f[l][1]] # tet corner 2 + #pts.append([(p1[x]+p2[x])*0.5+[i,j,k][x]+0.5 for x in [0,1,2]]) ## interpolate between tet corners + #facets.append(pts) + + #return facets + + + + +def isosurface(data, level): + """ + Generate isosurface from volumetric data using marching tetrahedra algorithm. + See Paul Bourke, "Polygonising a Scalar Field" + (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) + + *data* 3D numpy array of scalar values + *level* The level at which to generate an isosurface + + This function is SLOW; plenty of room for optimization here. + """ + + ## map from grid cell index to edge index. + ## grid cell index tells us which corners are below the isosurface, + ## edge index tells us which edges are cut by the isosurface. + ## (Data stolen from Bourk; see above.) + edgeTable = [ + 0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c, + 0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00, + 0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c, + 0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90, + 0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c, + 0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30, + 0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac, + 0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0, + 0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c, + 0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60, + 0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc, + 0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0, + 0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c, + 0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950, + 0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc , + 0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0, + 0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc, + 0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0, + 0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c, + 0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650, + 0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc, + 0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0, + 0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c, + 0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460, + 0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac, + 0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0, + 0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c, + 0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230, + 0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c, + 0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190, + 0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c, + 0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ] + + ## Table of triangles to use for filling each grid cell. + ## Each set of three integers tells us which three edges to + ## draw a triangle between. + ## (Data stolen from Bourk; see above.) + triTable = [ + [], + [0, 8, 3], + [0, 1, 9], + [1, 8, 3, 9, 8, 1], + [1, 2, 10], + [0, 8, 3, 1, 2, 10], + [9, 2, 10, 0, 2, 9], + [2, 8, 3, 2, 10, 8, 10, 9, 8], + [3, 11, 2], + [0, 11, 2, 8, 11, 0], + [1, 9, 0, 2, 3, 11], + [1, 11, 2, 1, 9, 11, 9, 8, 11], + [3, 10, 1, 11, 10, 3], + [0, 10, 1, 0, 8, 10, 8, 11, 10], + [3, 9, 0, 3, 11, 9, 11, 10, 9], + [9, 8, 10, 10, 8, 11], + [4, 7, 8], + [4, 3, 0, 7, 3, 4], + [0, 1, 9, 8, 4, 7], + [4, 1, 9, 4, 7, 1, 7, 3, 1], + [1, 2, 10, 8, 4, 7], + [3, 4, 7, 3, 0, 4, 1, 2, 10], + [9, 2, 10, 9, 0, 2, 8, 4, 7], + [2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4], + [8, 4, 7, 3, 11, 2], + [11, 4, 7, 11, 2, 4, 2, 0, 4], + [9, 0, 1, 8, 4, 7, 2, 3, 11], + [4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1], + [3, 10, 1, 3, 11, 10, 7, 8, 4], + [1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4], + [4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3], + [4, 7, 11, 4, 11, 9, 9, 11, 10], + [9, 5, 4], + [9, 5, 4, 0, 8, 3], + [0, 5, 4, 1, 5, 0], + [8, 5, 4, 8, 3, 5, 3, 1, 5], + [1, 2, 10, 9, 5, 4], + [3, 0, 8, 1, 2, 10, 4, 9, 5], + [5, 2, 10, 5, 4, 2, 4, 0, 2], + [2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8], + [9, 5, 4, 2, 3, 11], + [0, 11, 2, 0, 8, 11, 4, 9, 5], + [0, 5, 4, 0, 1, 5, 2, 3, 11], + [2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5], + [10, 3, 11, 10, 1, 3, 9, 5, 4], + [4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10], + [5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3], + [5, 4, 8, 5, 8, 10, 10, 8, 11], + [9, 7, 8, 5, 7, 9], + [9, 3, 0, 9, 5, 3, 5, 7, 3], + [0, 7, 8, 0, 1, 7, 1, 5, 7], + [1, 5, 3, 3, 5, 7], + [9, 7, 8, 9, 5, 7, 10, 1, 2], + [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3], + [8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2], + [2, 10, 5, 2, 5, 3, 3, 5, 7], + [7, 9, 5, 7, 8, 9, 3, 11, 2], + [9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11], + [2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7], + [11, 2, 1, 11, 1, 7, 7, 1, 5], + [9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11], + [5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0], + [11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0], + [11, 10, 5, 7, 11, 5], + [10, 6, 5], + [0, 8, 3, 5, 10, 6], + [9, 0, 1, 5, 10, 6], + [1, 8, 3, 1, 9, 8, 5, 10, 6], + [1, 6, 5, 2, 6, 1], + [1, 6, 5, 1, 2, 6, 3, 0, 8], + [9, 6, 5, 9, 0, 6, 0, 2, 6], + [5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8], + [2, 3, 11, 10, 6, 5], + [11, 0, 8, 11, 2, 0, 10, 6, 5], + [0, 1, 9, 2, 3, 11, 5, 10, 6], + [5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11], + [6, 3, 11, 6, 5, 3, 5, 1, 3], + [0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6], + [3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9], + [6, 5, 9, 6, 9, 11, 11, 9, 8], + [5, 10, 6, 4, 7, 8], + [4, 3, 0, 4, 7, 3, 6, 5, 10], + [1, 9, 0, 5, 10, 6, 8, 4, 7], + [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4], + [6, 1, 2, 6, 5, 1, 4, 7, 8], + [1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7], + [8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6], + [7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9], + [3, 11, 2, 7, 8, 4, 10, 6, 5], + [5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11], + [0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6], + [9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6], + [8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6], + [5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11], + [0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7], + [6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9], + [10, 4, 9, 6, 4, 10], + [4, 10, 6, 4, 9, 10, 0, 8, 3], + [10, 0, 1, 10, 6, 0, 6, 4, 0], + [8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10], + [1, 4, 9, 1, 2, 4, 2, 6, 4], + [3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4], + [0, 2, 4, 4, 2, 6], + [8, 3, 2, 8, 2, 4, 4, 2, 6], + [10, 4, 9, 10, 6, 4, 11, 2, 3], + [0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6], + [3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10], + [6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1], + [9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3], + [8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1], + [3, 11, 6, 3, 6, 0, 0, 6, 4], + [6, 4, 8, 11, 6, 8], + [7, 10, 6, 7, 8, 10, 8, 9, 10], + [0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10], + [10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0], + [10, 6, 7, 10, 7, 1, 1, 7, 3], + [1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7], + [2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9], + [7, 8, 0, 7, 0, 6, 6, 0, 2], + [7, 3, 2, 6, 7, 2], + [2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7], + [2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7], + [1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11], + [11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1], + [8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6], + [0, 9, 1, 11, 6, 7], + [7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0], + [7, 11, 6], + [7, 6, 11], + [3, 0, 8, 11, 7, 6], + [0, 1, 9, 11, 7, 6], + [8, 1, 9, 8, 3, 1, 11, 7, 6], + [10, 1, 2, 6, 11, 7], + [1, 2, 10, 3, 0, 8, 6, 11, 7], + [2, 9, 0, 2, 10, 9, 6, 11, 7], + [6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8], + [7, 2, 3, 6, 2, 7], + [7, 0, 8, 7, 6, 0, 6, 2, 0], + [2, 7, 6, 2, 3, 7, 0, 1, 9], + [1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6], + [10, 7, 6, 10, 1, 7, 1, 3, 7], + [10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8], + [0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7], + [7, 6, 10, 7, 10, 8, 8, 10, 9], + [6, 8, 4, 11, 8, 6], + [3, 6, 11, 3, 0, 6, 0, 4, 6], + [8, 6, 11, 8, 4, 6, 9, 0, 1], + [9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6], + [6, 8, 4, 6, 11, 8, 2, 10, 1], + [1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6], + [4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9], + [10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3], + [8, 2, 3, 8, 4, 2, 4, 6, 2], + [0, 4, 2, 4, 6, 2], + [1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8], + [1, 9, 4, 1, 4, 2, 2, 4, 6], + [8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1], + [10, 1, 0, 10, 0, 6, 6, 0, 4], + [4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3], + [10, 9, 4, 6, 10, 4], + [4, 9, 5, 7, 6, 11], + [0, 8, 3, 4, 9, 5, 11, 7, 6], + [5, 0, 1, 5, 4, 0, 7, 6, 11], + [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5], + [9, 5, 4, 10, 1, 2, 7, 6, 11], + [6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5], + [7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2], + [3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6], + [7, 2, 3, 7, 6, 2, 5, 4, 9], + [9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7], + [3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0], + [6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8], + [9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7], + [1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4], + [4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10], + [7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10], + [6, 9, 5, 6, 11, 9, 11, 8, 9], + [3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5], + [0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11], + [6, 11, 3, 6, 3, 5, 5, 3, 1], + [1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6], + [0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10], + [11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5], + [6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3], + [5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2], + [9, 5, 6, 9, 6, 0, 0, 6, 2], + [1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8], + [1, 5, 6, 2, 1, 6], + [1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6], + [10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0], + [0, 3, 8, 5, 6, 10], + [10, 5, 6], + [11, 5, 10, 7, 5, 11], + [11, 5, 10, 11, 7, 5, 8, 3, 0], + [5, 11, 7, 5, 10, 11, 1, 9, 0], + [10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1], + [11, 1, 2, 11, 7, 1, 7, 5, 1], + [0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11], + [9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7], + [7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2], + [2, 5, 10, 2, 3, 5, 3, 7, 5], + [8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5], + [9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2], + [9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2], + [1, 3, 5, 3, 7, 5], + [0, 8, 7, 0, 7, 1, 1, 7, 5], + [9, 0, 3, 9, 3, 5, 5, 3, 7], + [9, 8, 7, 5, 9, 7], + [5, 8, 4, 5, 10, 8, 10, 11, 8], + [5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0], + [0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5], + [10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4], + [2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8], + [0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11], + [0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5], + [9, 4, 5, 2, 11, 3], + [2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4], + [5, 10, 2, 5, 2, 4, 4, 2, 0], + [3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9], + [5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2], + [8, 4, 5, 8, 5, 3, 3, 5, 1], + [0, 4, 5, 1, 0, 5], + [8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5], + [9, 4, 5], + [4, 11, 7, 4, 9, 11, 9, 10, 11], + [0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11], + [1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11], + [3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4], + [4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2], + [9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3], + [11, 7, 4, 11, 4, 2, 2, 4, 0], + [11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4], + [2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9], + [9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7], + [3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10], + [1, 10, 2, 8, 7, 4], + [4, 9, 1, 4, 1, 7, 7, 1, 3], + [4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1], + [4, 0, 3, 7, 4, 3], + [4, 8, 7], + [9, 10, 8, 10, 11, 8], + [3, 0, 9, 3, 9, 11, 11, 9, 10], + [0, 1, 10, 0, 10, 8, 8, 10, 11], + [3, 1, 10, 11, 3, 10], + [1, 2, 11, 1, 11, 9, 9, 11, 8], + [3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9], + [0, 2, 11, 8, 0, 11], + [3, 2, 11], + [2, 3, 8, 2, 8, 10, 10, 8, 9], + [9, 10, 2, 0, 9, 2], + [2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8], + [1, 10, 2], + [1, 3, 8, 9, 1, 8], + [0, 9, 1], + [0, 3, 8], + [] + ] + + ## translation between edge index and + ## the vertex indexes that bound the edge + edgeKey = [ + [(0,0,0), (1,0,0)], + [(1,0,0), (1,1,0)], + [(1,1,0), (0,1,0)], + [(0,1,0), (0,0,0)], + [(0,0,1), (1,0,1)], + [(1,0,1), (1,1,1)], + [(1,1,1), (0,1,1)], + [(0,1,1), (0,0,1)], + [(0,0,0), (0,0,1)], + [(1,0,0), (1,0,1)], + [(1,1,0), (1,1,1)], + [(0,1,0), (0,1,1)], + ] + + + + facets = [] + + ## mark everything below the isosurface level + mask = data < level + + ### make eight sub-fields and compute indexes for grid cells + index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) + fields = np.empty((2,2,2), dtype=object) + slices = [slice(0,-1), slice(1,None)] + for i in [0,1]: + for j in [0,1]: + for k in [0,1]: + fields[i,j,k] = mask[slices[i], slices[j], slices[k]] + vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme + #print i,j,k," : ", fields[i,j,k], 2**vertIndex + index += fields[i,j,k] * 2**vertIndex + #print index + #print index + + ## add facets + for i in xrange(index.shape[0]): # data x-axis + for j in xrange(index.shape[1]): # data y-axis + for k in xrange(index.shape[2]): # data z-axis + tris = triTable[index[i,j,k]] + for l in range(0, len(tris), 3): ## faces for this grid cell + edges = tris[l:l+3] + pts = [] + for m in [0,1,2]: # points in this face + p1 = edgeKey[edges[m]][0] + p2 = edgeKey[edges[m]][1] + v1 = data[i+p1[0], j+p1[1], k+p1[2]] + v2 = data[i+p2[0], j+p2[1], k+p2[2]] + f = (level-v1) / (v2-v1) + fi = 1.0 - f + p = ( ## interpolate between corners + p1[0]*fi + p2[0]*f + i + 0.5, + p1[1]*fi + p2[1]*f + j + 0.5, + p1[2]*fi + p2[2]*f + k + 0.5 + ) + pts.append(p) + facets.append(pts) + + return facets + + +def meshNormals(data): + """ + Return list of normal vectors and list of faces which reference the normals + data must be list of triangles; each triangle is a list of three points + [ [(x,y,z), (x,y,z), (x,y,z)], ...] + Return values are + normals: [(x,y,z), ...] + faces: [(n1, n2, n3), ...] + """ + + normals = [] + points = {} + for i, face in enumerate(data): + ## compute face normal + pts = [QtGui.QVector3D(*x) for x in face] + norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + normals.append(norm) + + ## remember each point was associated with this normal + for p in face: + p = tuple(map(lambda x: np.round(x, 8), p)) + if p not in points: + points[p] = [] + points[p].append(i) + + ## compute averages + avgLookup = {} + avgNorms = [] + for k,v in points.iteritems(): + norms = [normals[i] for i in v] + a = norms[0] + if len(v) > 1: + for n in norms[1:]: + a = a + n + a = a / len(v) + avgLookup[k] = len(avgNorms) + avgNorms.append(a) + + ## generate return array + faces = [] + for i, face in enumerate(data): + f = [] + for p in face: + p = tuple(map(lambda x: np.round(x, 8), p)) + f.append(avgLookup[p]) + faces.append(tuple(f)) + + return avgNorms, faces + + + + + + + \ No newline at end of file diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 11834f01..27323049 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -297,7 +297,18 @@ class AxisItem(GraphicsWidget): pw = 10 ** (np.floor(np.log10(dif))-1) scaledIntervals = intervals * pw scaledTickCounts = dif / scaledIntervals - i1 = np.argwhere(scaledTickCounts < optimalTickCount)[0,0] + try: + i1 = np.argwhere(scaledTickCounts < optimalTickCount)[0,0] + except: + print "AxisItem can't determine tick spacing:" + print "scaledTickCounts", scaledTickCounts + print "optimalTickCount", optimalTickCount + print "dif", dif + print "scaledIntervals", scaledIntervals + print "intervals", intervals + print "pw", pw + print "pixelSpacing", pixelSpacing + i1 = 1 distBetweenIntervals = (optimalTickCount-scaledTickCounts[i1]) / (scaledTickCounts[i1-1]-scaledTickCounts[i1]) diff --git a/opengl/GLGraphicsItem.py b/opengl/GLGraphicsItem.py index 7baa3b7e..da6a37c6 100644 --- a/opengl/GLGraphicsItem.py +++ b/opengl/GLGraphicsItem.py @@ -6,6 +6,8 @@ class GLGraphicsItem(QtCore.QObject): self.__parent = None self.__view = None self.__children = set() + self.__transform = QtGui.QMatrix4x4() + self.__visible = True self.setParentItem(parentItem) self.setDepthValue(0) @@ -16,6 +18,11 @@ class GLGraphicsItem(QtCore.QObject): item.__children.add(self) self.__parent = item + if self.__parent is not None and self.view() is not self.__parent.view(): + if self.view() is not None: + self.view().removeItem(self) + self.__parent.view().addItem(self) + def parentItem(self): return self.__parent @@ -42,6 +49,69 @@ class GLGraphicsItem(QtCore.QObject): """Return the depth value of this item. See setDepthValue for mode information.""" return self.__depthValue + def setTransform(self, tr): + self.__transform = tr + self.update() + + def applyTransform(self, tr, local): + """ + Multiply this object's transform by *tr*. + If local is True, then *tr* is multiplied on the right of the current transform: + newTransform = transform * tr + If local is False, then *tr* is instead multiplied on the left: + newTransform = tr * transform + """ + if local: + self.setTransform(self.transform() * tr) + else: + self.setTransform(tr * self.transform()) + + def transform(self): + return self.__transform + + def translate(self, dx, dy, dz, local=False): + """ + Translate the object by (*dx*, *dy*, *dz*) in its parent's coordinate system. + If *local* is True, then translation takes place in local coordinates. + """ + tr = QtGui.QMatrix4x4() + tr.translate(dx, dy, dz) + self.applyTransform(tr, local=local) + + def rotate(self, angle, x, y, z, local=False): + """ + Rotate the object around the axis specified by (x,y,z). + *angle* is in degrees. + + """ + tr = QtGui.QMatrix4x4() + tr.rotate(angle, x, y, z) + self.applyTransform(tr, local=local) + + def scale(self, x, y, z, local=True): + """ + Scale the object by (*dx*, *dy*, *dz*) in its local coordinate system. + If *local* is False, then scale takes place in the parent's coordinates. + """ + tr = QtGui.QMatrix4x4() + tr.scale(x, y, z) + self.applyTransform(tr, local=local) + + + def hide(self): + self.setVisible(False) + + def show(self): + self.setVisible(True) + + def setVisible(self, vis): + self.__visible = vis + self.update() + + def visible(self): + return self.__visible + + def initializeGL(self): """ Called after an item is added to a GLViewWidget. @@ -57,4 +127,15 @@ class GLGraphicsItem(QtCore.QObject): but the caller will take care of pushing/popping. """ pass - \ No newline at end of file + + def update(self): + v = self.view() + if v is None: + return + v.updateGL() + + def mapFromParent(self, point): + tr = self.transform() + if tr is None: + return point + return tr.inverted()[0].map(point) \ No newline at end of file diff --git a/opengl/GLViewWidget.py b/opengl/GLViewWidget.py index ae579003..15077b74 100644 --- a/opengl/GLViewWidget.py +++ b/opengl/GLViewWidget.py @@ -33,14 +33,15 @@ class GLViewWidget(QtOpenGL.QGLWidget): #print "set view", item, self, item.view() self.updateGL() + def removeItem(self, item): + self.items.remove(item) + item._setView(None) + self.updateGL() + + def initializeGL(self): glClearColor(0.0, 0.0, 0.0, 0.0) - glEnable(GL_DEPTH_TEST) - - glEnable( GL_ALPHA_TEST ) self.resizeGL(self.width(), self.height()) - self.generateAxes() - #self.generatePoints() def resizeGL(self, w, h): glViewport(0, 0, w, h) @@ -75,15 +76,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): def paintGL(self): self.setProjection() self.setModelview() - glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) - glDisable( GL_DEPTH_TEST ) - #print "draw list:", self.axisList - glCallList(self.axisList) ## draw axes - #glCallList(self.pointList) - #self.drawPoints() - #self.drawAxes() - self.drawItemTree() def drawItemTree(self, item=None): @@ -94,14 +87,19 @@ class GLViewWidget(QtOpenGL.QGLWidget): items.append(item) items.sort(lambda a,b: cmp(a.depthValue(), b.depthValue())) for i in items: + if not i.visible(): + continue if i is item: + i.paint() + else: glMatrixMode(GL_MODELVIEW) glPushMatrix() - i.paint() + tr = i.transform() + a = np.array(tr.copyDataTo()).reshape((4,4)) + glMultMatrixf(a.transpose()) + self.drawItemTree(i) glMatrixMode(GL_MODELVIEW) glPopMatrix() - else: - self.drawItemTree(i) def cameraPosition(self): @@ -118,65 +116,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): ) return pos - - - def generateAxes(self): - self.axisList = glGenLists(1) - glNewList(self.axisList, GL_COMPILE) - - #glShadeModel(GL_FLAT) - #glFrontFace(GL_CCW) - #glEnable( GL_LIGHT_MODEL_TWO_SIDE ) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glEnable( GL_BLEND ) - glEnable( GL_ALPHA_TEST ) - #glAlphaFunc( GL_ALWAYS,0.5 ) - glEnable( GL_POINT_SMOOTH ) - glDisable( GL_DEPTH_TEST ) - glBegin( GL_LINES ) - - glColor4f(1, 1, 1, .3) - for x in range(-10, 11): - glVertex3f(x, -10, 0) - glVertex3f(x, 10, 0) - for y in range(-10, 11): - glVertex3f(-10, y, 0) - glVertex3f( 10, y, 0) - - - glColor4f(0, 1, 0, .6) # z is green - glVertex3f(0, 0, 0) - glVertex3f(0, 0, 5) - - glColor4f(1, 1, 0, .6) # y is yellow - glVertex3f(0, 0, 0) - glVertex3f(0, 5, 0) - - glColor4f(0, 0, 1, .6) # x is blue - glVertex3f(0, 0, 0) - glVertex3f(5, 0, 0) - glEnd() - glEndList() - - def generatePoints(self): - self.pointList = glGenLists(1) - glNewList(self.pointList, GL_COMPILE) - width = 7 - alpha = 0.02 - n = 40 - glPointSize( width ) - glBegin(GL_POINTS) - for x in range(-n, n+1): - r = (n-x)/(2.*n) - glColor4f(r, r, r, alpha) - for y in range(-n, n+1): - for z in range(-n, n+1): - glVertex3f(x, y, z) - glEnd() - glEndList() - - def mousePressEvent(self, ev): self.mousePos = ev.pos() diff --git a/opengl/MeshData.py b/opengl/MeshData.py new file mode 100644 index 00000000..f6d0ae7c --- /dev/null +++ b/opengl/MeshData.py @@ -0,0 +1,25 @@ +class MeshData(object): + """ + Class for storing 3D mesh data. May contain: + - list of vertex locations + - list of edges + - list of triangles + - colors per vertex, edge, or tri + - normals per vertex or tri + """ + + def __init__(self ...): + + + def generateFaceNormals(self): + + + def generateVertexNormals(self): + """ + Assigns each vertex the average of its connected face normals. + If face normals have not been computed yet, then generateFaceNormals will be called. + """ + + + def reverseNormals(self): + \ No newline at end of file diff --git a/opengl/items/GLAxisItem.py b/opengl/items/GLAxisItem.py new file mode 100644 index 00000000..79d2149d --- /dev/null +++ b/opengl/items/GLAxisItem.py @@ -0,0 +1,51 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph import QtGui + +__all__ = ['GLAxisItem'] + +class GLAxisItem(GLGraphicsItem): + def __init__(self, size=None): + GLGraphicsItem.__init__(self) + if size is None: + size = QtGui.QVector3D(1,1,1) + self.setSize(size=size) + + def setSize(self, x=None, y=None, z=None, size=None): + """ + Set the size of the axes (in its local coordinate system; this does not affect the transform) + Arguments can be x,y,z or size=QVector3D(). + """ + if size is not None: + x = size.x() + y = size.y() + z = size.z() + self.__size = [x,y,z] + self.update() + + def size(self): + return self.__size[:] + + + def paint(self): + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + glEnable( GL_POINT_SMOOTH ) + #glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + x,y,z = self.size() + glColor4f(0, 1, 0, .6) # z is green + glVertex3f(0, 0, 0) + glVertex3f(0, 0, z) + + glColor4f(1, 1, 0, .6) # y is yellow + glVertex3f(0, 0, 0) + glVertex3f(0, y, 0) + + glColor4f(0, 0, 1, .6) # x is blue + glVertex3f(0, 0, 0) + glVertex3f(x, 0, 0) + glEnd() diff --git a/opengl/items/GLBoxItem.py b/opengl/items/GLBoxItem.py index ffaa6861..648ac5e4 100644 --- a/opengl/items/GLBoxItem.py +++ b/opengl/items/GLBoxItem.py @@ -1,9 +1,42 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtGui +import pyqtgraph as pg __all__ = ['GLBoxItem'] class GLBoxItem(GLGraphicsItem): + def __init__(self, size=None, color=None): + GLGraphicsItem.__init__(self) + if size is None: + size = QtGui.QVector3D(1,1,1) + self.setSize(size=size) + if color is None: + color = (255,255,255,80) + self.setColor(color) + + def setSize(self, x=None, y=None, z=None, size=None): + """ + Set the size of the box (in its local coordinate system; this does not affect the transform) + Arguments can be x,y,z or size=QVector3D(). + """ + if size is not None: + x = size.x() + y = size.y() + z = size.z() + self.__size = [x,y,z] + self.update() + + def size(self): + return self.__size[:] + + def setColor(self, *args): + """Set the color of the box. Arguments are the same as those accepted by functions.mkColor()""" + self.__color = pg.Color(*args) + + def color(self): + return self.__color + def paint(self): glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glEnable( GL_BLEND ) @@ -13,34 +46,34 @@ class GLBoxItem(GLGraphicsItem): glDisable( GL_DEPTH_TEST ) glBegin( GL_LINES ) - glColor4f(1, 1, 1, .3) - w = 10 - glVertex3f(-w, -w, -w) - glVertex3f(-w, -w, w) - glVertex3f( w, -w, -w) - glVertex3f( w, -w, w) - glVertex3f(-w, w, -w) - glVertex3f(-w, w, w) - glVertex3f( w, w, -w) - glVertex3f( w, w, w) + glColor4f(*self.color().glColor()) + x,y,z = self.size() + glVertex3f(0, 0, 0) + glVertex3f(0, 0, z) + glVertex3f(x, 0, 0) + glVertex3f(x, 0, z) + glVertex3f(0, y, 0) + glVertex3f(0, y, z) + glVertex3f(x, y, 0) + glVertex3f(x, y, z) - glVertex3f(-w, -w, -w) - glVertex3f(-w, w, -w) - glVertex3f( w, -w, -w) - glVertex3f( w, w, -w) - glVertex3f(-w, -w, w) - glVertex3f(-w, w, w) - glVertex3f( w, -w, w) - glVertex3f( w, w, w) + glVertex3f(0, 0, 0) + glVertex3f(0, y, 0) + glVertex3f(x, 0, 0) + glVertex3f(x, y, 0) + glVertex3f(0, 0, z) + glVertex3f(0, y, z) + glVertex3f(x, 0, z) + glVertex3f(x, y, z) - glVertex3f(-w, -w, -w) - glVertex3f( w, -w, -w) - glVertex3f(-w, w, -w) - glVertex3f( w, w, -w) - glVertex3f(-w, -w, w) - glVertex3f( w, -w, w) - glVertex3f(-w, w, w) - glVertex3f( w, w, w) + glVertex3f(0, 0, 0) + glVertex3f(x, 0, 0) + glVertex3f(0, y, 0) + glVertex3f(x, y, 0) + glVertex3f(0, 0, z) + glVertex3f(x, 0, z) + glVertex3f(0, y, z) + glVertex3f(x, y, z) glEnd() diff --git a/opengl/items/GLGridItem.py b/opengl/items/GLGridItem.py new file mode 100644 index 00000000..e34929b3 --- /dev/null +++ b/opengl/items/GLGridItem.py @@ -0,0 +1,48 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph import QtGui + +__all__ = ['GLGridItem'] + +class GLGridItem(GLGraphicsItem): + def __init__(self, size=None, color=None): + GLGraphicsItem.__init__(self) + if size is None: + size = QtGui.QVector3D(1,1,1) + self.setSize(size=size) + + def setSize(self, x=None, y=None, z=None, size=None): + """ + Set the size of the axes (in its local coordinate system; this does not affect the transform) + Arguments can be x,y,z or size=QVector3D(). + """ + if size is not None: + x = size.x() + y = size.y() + z = size.z() + self.__size = [x,y,z] + self.update() + + def size(self): + return self.__size[:] + + + def paint(self): + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + glEnable( GL_POINT_SMOOTH ) + #glDisable( GL_DEPTH_TEST ) + glBegin( GL_LINES ) + + x,y,z = self.size() + glColor4f(1, 1, 1, .3) + for x in range(-10, 11): + glVertex3f(x, -10, 0) + glVertex3f(x, 10, 0) + for y in range(-10, 11): + glVertex3f(-10, y, 0) + glVertex3f( 10, y, 0) + + glEnd() diff --git a/opengl/items/GLMeshItem.py b/opengl/items/GLMeshItem.py new file mode 100644 index 00000000..1efc3ffe --- /dev/null +++ b/opengl/items/GLMeshItem.py @@ -0,0 +1,93 @@ +from OpenGL.GL import * +from .. GLGraphicsItem import GLGraphicsItem +from pyqtgraph.Qt import QtGui +import pyqtgraph as pg +from .. import shaders +import numpy as np + + + +__all__ = ['GLMeshItem'] + +class GLMeshItem(GLGraphicsItem): + def __init__(self, faces): + self.faces = faces + self.normals, self.faceNormals = pg.meshNormals(faces) + + GLGraphicsItem.__init__(self) + + def initializeGL(self): + + #balloonVertexShader = shaders.compileShader(""" + #varying vec3 normal; + #void main() { + #normal = normalize(gl_NormalMatrix * gl_Normal); + #//vec4 color = normal; + #//normal.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 2.0), 1.0); + #gl_FrontColor = gl_Color; + #gl_BackColor = gl_Color; + #gl_Position = ftransform(); + #}""", GL_VERTEX_SHADER) + #balloonFragmentShader = shaders.compileShader(""" + #varying vec3 normal; + #void main() { + #vec4 color = gl_Color; + #color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0); + #gl_FragColor = color; + #}""", GL_FRAGMENT_SHADER) + #self.shader = shaders.compileProgram(balloonVertexShader, balloonFragmentShader) + + self.shader = shaders.getShader('balloon') + + l = glGenLists(1) + self.triList = l + glNewList(l, GL_COMPILE) + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + #glAlphaFunc( GL_ALWAYS,0.5 ) + glEnable( GL_POINT_SMOOTH ) + glDisable( GL_DEPTH_TEST ) + glColor4f(1, 1, 1, .1) + glBegin( GL_TRIANGLES ) + for i, f in enumerate(self.faces): + pts = [QtGui.QVector3D(*x) for x in f] + if pts[0] is None: + print f + continue + #norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + for j in [0,1,2]: + norm = self.normals[self.faceNormals[i][j]] + glNormal3f(norm.x(), norm.y(), norm.z()) + #j = (i+1) % 3 + glVertex3f(*f[j]) + glEnd() + glEndList() + + + l = glGenLists(1) + self.meshList = l + glNewList(l, GL_COMPILE) + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + glEnable( GL_BLEND ) + glEnable( GL_ALPHA_TEST ) + #glAlphaFunc( GL_ALWAYS,0.5 ) + glEnable( GL_POINT_SMOOTH ) + glEnable( GL_DEPTH_TEST ) + glColor4f(1, 1, 1, .3) + glBegin( GL_LINES ) + for f in self.faces: + for i in [0,1,2]: + j = (i+1) % 3 + glVertex3f(*f[i]) + glVertex3f(*f[j]) + glEnd() + glEndList() + + + def paint(self): + shaders.glUseProgram(self.shader) + glCallList(self.triList) + shaders.glUseProgram(0) + #glCallList(self.meshList) diff --git a/opengl/items/GLVolumeItem.py b/opengl/items/GLVolumeItem.py index 548cd2bd..a261a573 100644 --- a/opengl/items/GLVolumeItem.py +++ b/opengl/items/GLVolumeItem.py @@ -6,23 +6,28 @@ import numpy as np __all__ = ['GLVolumeItem'] class GLVolumeItem(GLGraphicsItem): + def __init__(self, data, sliceDensity=1, smooth=True): + self.sliceDensity = sliceDensity + self.smooth = smooth + self.data = data + GLGraphicsItem.__init__(self) + def initializeGL(self): - n = 128 - self.data = np.random.randint(0, 255, size=4*n**3).astype(np.uint8).reshape((n,n,n,4)) - self.data[...,3] *= 0.1 - for i in range(n): - self.data[i,:,:,0] = i*256./n glEnable(GL_TEXTURE_3D) self.texture = glGenTextures(1) glBindTexture(GL_TEXTURE_3D, self.texture) - #glTexImage3D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, void *data ); - glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) - glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + if self.smooth: + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + else: + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST) + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) - #glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BORDER_COLOR, ) ## black/transparent by default - glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, n, n, n, 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data) + shape = self.data.shape + + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, shape[0], shape[1], shape[2], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.data.transpose((2,1,0,3))) glDisable(GL_TEXTURE_3D) self.lists = {} @@ -40,14 +45,16 @@ class GLVolumeItem(GLGraphicsItem): glEnable(GL_TEXTURE_3D) glBindTexture(GL_TEXTURE_3D, self.texture) - glDisable(GL_DEPTH_TEST) + glEnable(GL_DEPTH_TEST) #glDisable(GL_CULL_FACE) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) glEnable( GL_BLEND ) glEnable( GL_ALPHA_TEST ) + glColor4f(1,1,1,1) view = self.view() - cam = view.cameraPosition() + center = QtGui.QVector3D(*[x/2. for x in self.data.shape[:3]]) + cam = self.mapFromParent(view.cameraPosition()) - center cam = np.array([cam.x(), cam.y(), cam.z()]) ax = np.argmax(abs(cam)) d = 1 if cam[ax] > 0 else -1 @@ -55,7 +62,6 @@ class GLVolumeItem(GLGraphicsItem): glDisable(GL_TEXTURE_3D) def drawVolume(self, ax, d): - slices = 256 N = 5 imax = [0,1,2] @@ -63,31 +69,35 @@ class GLVolumeItem(GLGraphicsItem): tp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]] vp = [[0,0,0],[0,0,0],[0,0,0],[0,0,0]] - tp[0][imax[0]] = 0 - tp[0][imax[1]] = 0 - tp[1][imax[0]] = 1 - tp[1][imax[1]] = 0 - tp[2][imax[0]] = 1 - tp[2][imax[1]] = 1 - tp[3][imax[0]] = 0 - tp[3][imax[1]] = 1 + nudge = [0.5/x for x in self.data.shape] + tp[0][imax[0]] = 0+nudge[imax[0]] + tp[0][imax[1]] = 0+nudge[imax[1]] + tp[1][imax[0]] = 1-nudge[imax[0]] + tp[1][imax[1]] = 0+nudge[imax[1]] + tp[2][imax[0]] = 1-nudge[imax[0]] + tp[2][imax[1]] = 1-nudge[imax[1]] + tp[3][imax[0]] = 0+nudge[imax[0]] + tp[3][imax[1]] = 1-nudge[imax[1]] - vp[0][imax[0]] = -N - vp[0][imax[1]] = -N - vp[1][imax[0]] = N - vp[1][imax[1]] = -N - vp[2][imax[0]] = N - vp[2][imax[1]] = N - vp[3][imax[0]] = -N - vp[3][imax[1]] = N + vp[0][imax[0]] = 0 + vp[0][imax[1]] = 0 + vp[1][imax[0]] = self.data.shape[imax[0]] + vp[1][imax[1]] = 0 + vp[2][imax[0]] = self.data.shape[imax[0]] + vp[2][imax[1]] = self.data.shape[imax[1]] + vp[3][imax[0]] = 0 + vp[3][imax[1]] = self.data.shape[imax[1]] + slices = self.data.shape[ax] * self.sliceDensity r = range(slices) if d == -1: r = r[::-1] glBegin(GL_QUADS) + tzVals = np.linspace(nudge[ax], 1.0-nudge[ax], slices) + vzVals = np.linspace(0, self.data.shape[ax], slices) for i in r: - z = float(i)/(slices-1.) - w = float(i)*10./(slices-1.) - 5. + z = tzVals[i] + w = vzVals[i] tp[0][ax] = z tp[1][ax] = z diff --git a/opengl/shaders.py b/opengl/shaders.py new file mode 100644 index 00000000..b1216e35 --- /dev/null +++ b/opengl/shaders.py @@ -0,0 +1,41 @@ +from OpenGL.GL import * +from OpenGL.GL import shaders + +## For centralizing and managing vertex/fragment shader programs. + + +Shaders = { + 'balloon': ( ## increases fragment alpha as the normal turns orthogonal to the view + """ + varying vec3 normal; + void main() { + normal = normalize(gl_NormalMatrix * gl_Normal); + //vec4 color = normal; + //normal.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 2.0), 1.0); + gl_FrontColor = gl_Color; + gl_BackColor = gl_Color; + gl_Position = ftransform(); + } + """, + """ + varying vec3 normal; + void main() { + vec4 color = gl_Color; + color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0); + gl_FragColor = color; + } + """ + ), +} +CompiledShaders = {} + +def getShader(name): + global Shaders, CompiledShaders + + if name not in CompiledShaders: + vshader, fshader = Shaders[name] + vcomp = shaders.compileShader(vshader, GL_VERTEX_SHADER) + fcomp = shaders.compileShader(fshader, GL_FRAGMENT_SHADER) + prog = shaders.compileProgram(vcomp, fcomp) + CompiledShaders[name] = prog, vcomp, fcomp + return CompiledShaders[name][0] From 81a32b0d1e1d4565b496aeb1e3715fd19f1e12c4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 11 Mar 2012 11:59:45 -0400 Subject: [PATCH 015/238] Cleaned up and centralized export functionality Moved GraphicsScene to its own directory, added exportDialog Removed old export options from PlotItem / ViewBox (will re-enable once they are working again) --- .../GraphicsScene.py | 308 +++--------------- GraphicsScene/__init__.py | 1 + GraphicsScene/exportDialog.py | 120 +++++++ GraphicsScene/exportDialogTemplate.py | 65 ++++ GraphicsScene/exportDialogTemplate.ui | 93 ++++++ GraphicsScene/mouseEvents.py | 249 ++++++++++++++ __init__.py | 39 ++- documentation/source/style.rst | 7 +- exporters/Exporter.py | 94 ++++++ exporters/ImageExporter.py | 62 ++++ exporters/SVGExporter.py | 64 ++++ exporters/__init__.py | 7 + graphicsItems/GraphicsItemMethods.py | 23 ++ graphicsItems/PlotItem/PlotItem.py | 19 +- graphicsItems/ViewBox/ViewBoxMenu.py | 9 +- parametertree/ParameterTree.py | 12 +- parametertree/__main__.py | 12 +- parametertree/parameterTypes.py | 9 + widgets/GraphicsView.py | 106 +++--- 19 files changed, 955 insertions(+), 344 deletions(-) rename GraphicsScene.py => GraphicsScene/GraphicsScene.py (71%) create mode 100644 GraphicsScene/__init__.py create mode 100644 GraphicsScene/exportDialog.py create mode 100644 GraphicsScene/exportDialogTemplate.py create mode 100644 GraphicsScene/exportDialogTemplate.ui create mode 100644 GraphicsScene/mouseEvents.py create mode 100644 exporters/Exporter.py create mode 100644 exporters/ImageExporter.py create mode 100644 exporters/SVGExporter.py create mode 100644 exporters/__init__.py diff --git a/GraphicsScene.py b/GraphicsScene/GraphicsScene.py similarity index 71% rename from GraphicsScene.py rename to GraphicsScene/GraphicsScene.py index e9316796..8f1481bb 100644 --- a/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -3,7 +3,9 @@ import weakref from pyqtgraph.Point import Point import pyqtgraph.functions as fn import pyqtgraph.ptime as ptime +from mouseEvents import * import debug +import exportDialog try: import sip @@ -63,6 +65,8 @@ class GraphicsScene(QtGui.QGraphicsScene): sigMouseMoved = QtCore.Signal(object) ## emits position of mouse on every move sigMouseClicked = QtCore.Signal(object) ## emitted when MouseClickEvent is not accepted by any items under the click. + ExportDirectory = None + @classmethod def registerObject(cls, obj): """ @@ -78,6 +82,8 @@ class GraphicsScene(QtGui.QGraphicsScene): QtGui.QGraphicsScene.__init__(self) self.setClickRadius(clickRadius) self.setMoveDistance(moveDistance) + self.exportDirectory = None + self.clickEvents = [] self.dragButtons = [] self.mouseGrabber = None @@ -89,6 +95,11 @@ class GraphicsScene(QtGui.QGraphicsScene): #self.searchRect.setPen(fn.mkPen(200,0,0)) #self.addItem(self.searchRect) + self.contextMenu = [QtGui.QAction("Export...", self)] + self.contextMenu[0].triggered.connect(self.showExportDialog) + + self.exportDialog = None + def setClickRadius(self, r): """ @@ -420,19 +431,22 @@ class GraphicsScene(QtGui.QGraphicsScene): ##if item not in seen: #yield item - def getViewWidget(self, widget): - ## same pyqt bug -- mouseEvent.widget() doesn't give us the original python object. - ## [[doesn't seem to work correctly]] - if HAVE_SIP and isinstance(self, sip.wrapper): - addr = sip.unwrapinstance(sip.cast(widget, QtGui.QWidget)) - #print "convert", widget, addr - for v in self.views(): - addr2 = sip.unwrapinstance(sip.cast(v, QtGui.QWidget)) - #print " check:", v, addr2 - if addr2 == addr: - return v - else: - return widget + def getViewWidget(self): + return self.views()[0] + + #def getViewWidget(self, widget): + ### same pyqt bug -- mouseEvent.widget() doesn't give us the original python object. + ### [[doesn't seem to work correctly]] + #if HAVE_SIP and isinstance(self, sip.wrapper): + #addr = sip.unwrapinstance(sip.cast(widget, QtGui.QWidget)) + ##print "convert", widget, addr + #for v in self.views(): + #addr2 = sip.unwrapinstance(sip.cast(v, QtGui.QWidget)) + ##print " check:", v, addr2 + #if addr2 == addr: + #return v + #else: + #return widget def addParentContextMenus(self, item, menu, event): """ @@ -464,11 +478,12 @@ class GraphicsScene(QtGui.QGraphicsScene): #items = self.itemsNearEvent(ev) menusToAdd = [] - while item.parentItem() is not None: + while item is not self: item = item.parentItem() - #for item in items: - #if item is sender: - #continue + + if item is None: + item = self + if not hasattr(item, "getContextMenus"): continue @@ -484,10 +499,24 @@ class GraphicsScene(QtGui.QGraphicsScene): menu.addSeparator() for m in menusToAdd: - menu.addMenu(m) + if isinstance(m, QtGui.QMenu): + menu.addMenu(m) + elif isinstance(m, QtGui.QAction): + menu.addAction(m) + else: + raise Exception("Cannot add object %s (type=%s) to QMenu." % (str(m), str(type(m)))) return menu + def getContextMenus(self, event): + self.contextMenuItem = event.acceptedItem + return self.contextMenu + + def showExportDialog(self): + if self.exportDialog is None: + self.exportDialog = exportDialog.ExportDialog(self) + self.exportDialog.show(self.contextMenuItem) + @staticmethod def translateGraphicsItem(item): ## for fixing pyqt bugs where the wrong item is returned @@ -501,247 +530,4 @@ class GraphicsScene(QtGui.QGraphicsScene): return map(GraphicsScene.translateGraphicsItem, items) -class MouseDragEvent: - def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False): - self.start = start - self.finish = finish - self.accepted = False - self.currentItem = None - self._buttonDownScenePos = {} - self._buttonDownScreenPos = {} - for btn in [QtCore.Qt.LeftButton, QtCore.Qt.MidButton, QtCore.Qt.RightButton]: - self._buttonDownScenePos[int(btn)] = moveEvent.buttonDownScenePos(btn) - self._buttonDownScreenPos[int(btn)] = moveEvent.buttonDownScreenPos(btn) - self._scenePos = moveEvent.scenePos() - self._screenPos = moveEvent.screenPos() - if lastEvent is None: - self._lastScenePos = pressEvent.scenePos() - self._lastScreenPos = pressEvent.screenPos() - else: - self._lastScenePos = lastEvent.scenePos() - self._lastScreenPos = lastEvent.screenPos() - self._buttons = moveEvent.buttons() - self._button = pressEvent.button() - self._modifiers = moveEvent.modifiers() - - def accept(self): - self.accepted = True - self.acceptedItem = self.currentItem - - def ignore(self): - self.accepted = False - - def isAccepted(self): - return self.accepted - - def scenePos(self): - return Point(self._scenePos) - - def screenPos(self): - return Point(self._screenPos) - - def buttonDownScenePos(self, btn=None): - if btn is None: - btn = self.button() - return Point(self._buttonDownScenePos[int(btn)]) - - def buttonDownScreenPos(self, btn=None): - if btn is None: - btn = self.button() - return Point(self._buttonDownScreenPos[int(btn)]) - - def lastScenePos(self): - return Point(self._lastScenePos) - - def lastScreenPos(self): - return Point(self._lastScreenPos) - - def buttons(self): - return self._buttons - - def button(self): - """Return the button that initiated the drag (may be different from the buttons currently pressed)""" - return self._button - - def pos(self): - return Point(self.currentItem.mapFromScene(self._scenePos)) - - def lastPos(self): - return Point(self.currentItem.mapFromScene(self._lastScenePos)) - - def buttonDownPos(self, btn=None): - if btn is None: - btn = self.button() - return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)])) - - def isStart(self): - return self.start - - def isFinish(self): - return self.finish - def __repr__(self): - lp = self.lastPos() - p = self.pos() - return "(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish())) - - def modifiers(self): - return self._modifiers - - - -class MouseClickEvent: - def __init__(self, pressEvent, double=False): - self.accepted = False - self.currentItem = None - self._double = double - self._scenePos = pressEvent.scenePos() - self._screenPos = pressEvent.screenPos() - self._button = pressEvent.button() - self._buttons = pressEvent.buttons() - self._modifiers = pressEvent.modifiers() - self._time = ptime.time() - - - def accept(self): - self.accepted = True - self.acceptedItem = self.currentItem - - def ignore(self): - self.accepted = False - - def isAccepted(self): - return self.accepted - - def scenePos(self): - return Point(self._scenePos) - - def screenPos(self): - return Point(self._screenPos) - - def buttons(self): - return self._buttons - - def button(self): - return self._button - - def double(self): - return self._double - - def pos(self): - return Point(self.currentItem.mapFromScene(self._scenePos)) - - def lastPos(self): - return Point(self.currentItem.mapFromScene(self._lastScenePos)) - - def modifiers(self): - return self._modifiers - - def __repr__(self): - p = self.pos() - return "" % (p.x(), p.y(), int(self.button())) - - def time(self): - return self._time - - - -class HoverEvent: - """ - This event class both informs items that the mouse cursor is nearby and allows items to - communicate with one another about whether each item will accept _potential_ mouse events. - - It is common for multiple overlapping items to receive hover events and respond by changing - their appearance. This can be misleading to the user since, in general, only one item will - respond to mouse events. To avoid this, items make calls to event.acceptClicks(button) - and/or acceptDrags(button). - - Each item may make multiple calls to acceptClicks/Drags, each time for a different button. - If the method returns True, then the item is guaranteed to be - the recipient of the claimed event IF the user presses the specified mouse button before - moving. If claimEvent returns False, then this item is guaranteed NOT to get the specified - event (because another has already claimed it) and the item should change its appearance - accordingly. - - event.isEnter() returns True if the mouse has just entered the item's shape; - event.isExit() returns True if the mouse has just left. - """ - def __init__(self, moveEvent, acceptable): - self.enter = False - self.acceptable = acceptable - self.exit = False - self.__clickItems = weakref.WeakValueDictionary() - self.__dragItems = weakref.WeakValueDictionary() - self.currentItem = None - if moveEvent is not None: - self._scenePos = moveEvent.scenePos() - self._screenPos = moveEvent.screenPos() - self._lastScenePos = moveEvent.lastScenePos() - self._lastScreenPos = moveEvent.lastScreenPos() - self._buttons = moveEvent.buttons() - self._modifiers = moveEvent.modifiers() - else: - self.exit = True - - - - def isEnter(self): - return self.enter - - def isExit(self): - return self.exit - - def acceptClicks(self, button): - """""" - if not self.acceptable: - return False - if button not in self.__clickItems: - self.__clickItems[button] = self.currentItem - return True - return False - - def acceptDrags(self, button): - if not self.acceptable: - return False - if button not in self.__dragItems: - self.__dragItems[button] = self.currentItem - return True - return False - - def scenePos(self): - return Point(self._scenePos) - - def screenPos(self): - return Point(self._screenPos) - - def lastScenePos(self): - return Point(self._lastScenePos) - - def lastScreenPos(self): - return Point(self._lastScreenPos) - - def buttons(self): - return self._buttons - - def pos(self): - return Point(self.currentItem.mapFromScene(self._scenePos)) - - def lastPos(self): - return Point(self.currentItem.mapFromScene(self._lastScenePos)) - - def __repr__(self): - lp = self.lastPos() - p = self.pos() - return "(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit())) - - def modifiers(self): - return self._modifiers - - def clickItems(self): - return self.__clickItems - - def dragItems(self): - return self.__dragItems - - - \ No newline at end of file diff --git a/GraphicsScene/__init__.py b/GraphicsScene/__init__.py new file mode 100644 index 00000000..ed246252 --- /dev/null +++ b/GraphicsScene/__init__.py @@ -0,0 +1 @@ +from GraphicsScene import * diff --git a/GraphicsScene/exportDialog.py b/GraphicsScene/exportDialog.py new file mode 100644 index 00000000..f9ed5763 --- /dev/null +++ b/GraphicsScene/exportDialog.py @@ -0,0 +1,120 @@ +import exportDialogTemplate +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +import pyqtgraph.exporters as exporters + + +class ExportDialog(QtGui.QWidget): + def __init__(self, scene): + QtGui.QWidget.__init__(self) + self.setVisible(False) + self.setWindowTitle("Export") + self.shown = False + self.currentExporter = None + self.scene = scene + + self.selectBox = QtGui.QGraphicsRectItem() + self.selectBox.setPen(pg.mkPen('y', width=3, style=QtCore.Qt.DashLine)) + self.selectBox.hide() + self.scene.addItem(self.selectBox) + + self.ui = exportDialogTemplate.Ui_Form() + self.ui.setupUi(self) + + self.ui.closeBtn.clicked.connect(self.close) + self.ui.exportBtn.clicked.connect(self.exportClicked) + self.ui.itemTree.currentItemChanged.connect(self.exportItemChanged) + self.ui.formatList.currentItemChanged.connect(self.exportFormatChanged) + + + def show(self, item=None): + if item is not None: + while not isinstance(item, pg.ViewBox) and not isinstance(item, pg.PlotItem) and item is not None: + item = item.parentItem() + self.updateItemList(select=item) + self.setVisible(True) + self.activateWindow() + self.raise_() + self.selectBox.setVisible(True) + + if not self.shown: + self.shown = True + vcenter = self.scene.getViewWidget().geometry().center() + self.setGeometry(vcenter.x()-self.width()/2, vcenter.y()-self.height()/2, self.width(), self.height()) + + def updateItemList(self, select=None): + self.ui.itemTree.clear() + si = QtGui.QTreeWidgetItem(["Entire Scene"]) + si.gitem = self.scene + self.ui.itemTree.addTopLevelItem(si) + self.ui.itemTree.setCurrentItem(si) + si.setExpanded(True) + for child in self.scene.items(): + if child.parentItem() is None: + self.updateItemTree(child, si, select=select) + + def updateItemTree(self, item, treeItem, select=None): + si = None + if isinstance(item, pg.ViewBox): + si = QtGui.QTreeWidgetItem(['ViewBox']) + elif isinstance(item, pg.PlotItem): + si = QtGui.QTreeWidgetItem(['Plot']) + + if si is not None: + si.gitem = item + treeItem.addChild(si) + treeItem = si + if si.gitem is select: + self.ui.itemTree.setCurrentItem(si) + + for ch in item.childItems(): + self.updateItemTree(ch, treeItem, select=select) + + + def exportItemChanged(self, item, prev): + if item.gitem is self.scene: + newBounds = self.scene.views()[0].viewRect() + else: + newBounds = item.gitem.sceneBoundingRect() + self.selectBox.setRect(newBounds) + self.selectBox.show() + self.updateFormatList() + + def updateFormatList(self): + current = self.ui.formatList.currentItem() + if current is not None: + current = str(current.text()) + self.ui.formatList.clear() + self.exporterClasses = {} + gotCurrent = False + for exp in exporters.listExporters(): + self.ui.formatList.addItem(exp.Name) + self.exporterClasses[exp.Name] = exp + if exp.Name == current: + self.ui.formatList.setCurrentRow(self.ui.formatList.count()-1) + gotCurrent = True + + if not gotCurrent: + self.ui.formatList.setCurrentRow(0) + + def exportFormatChanged(self, item, prev): + if item is None: + self.currentExporter = None + self.ui.paramTree.clear() + return + expClass = self.exporterClasses[str(item.text())] + exp = expClass(item=self.ui.itemTree.currentItem().gitem) + params = exp.parameters() + self.ui.paramTree.setParameters(params) + self.currentExporter = exp + + def exportClicked(self): + self.selectBox.hide() + self.currentExporter.export() + + def close(self): + self.selectBox.setVisible(False) + self.setVisible(False) + + + diff --git a/GraphicsScene/exportDialogTemplate.py b/GraphicsScene/exportDialogTemplate.py new file mode 100644 index 00000000..ae90100d --- /dev/null +++ b/GraphicsScene/exportDialogTemplate.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'exportDialogTemplate.ui' +# +# Created: Sat Mar 10 17:54:53 2012 +# by: PyQt4 UI code generator 4.8.5 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName(_fromUtf8("Form")) + Form.resize(241, 367) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + self.gridLayout = QtGui.QGridLayout(Form) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.label = QtGui.QLabel(Form) + self.label.setText(QtGui.QApplication.translate("Form", "Item to export:", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setObjectName(_fromUtf8("label")) + self.gridLayout.addWidget(self.label, 0, 0, 1, 3) + self.itemTree = QtGui.QTreeWidget(Form) + self.itemTree.setObjectName(_fromUtf8("itemTree")) + self.itemTree.headerItem().setText(0, _fromUtf8("1")) + self.itemTree.header().setVisible(False) + self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) + self.label_2 = QtGui.QLabel(Form) + self.label_2.setText(QtGui.QApplication.translate("Form", "Export format", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setObjectName(_fromUtf8("label_2")) + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) + self.formatList = QtGui.QListWidget(Form) + self.formatList.setObjectName(_fromUtf8("formatList")) + self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) + self.exportBtn = QtGui.QPushButton(Form) + self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) + self.exportBtn.setObjectName(_fromUtf8("exportBtn")) + self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) + self.closeBtn = QtGui.QPushButton(Form) + self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8)) + self.closeBtn.setObjectName(_fromUtf8("closeBtn")) + self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) + self.paramTree = ParameterTree(Form) + self.paramTree.setObjectName(_fromUtf8("paramTree")) + self.paramTree.headerItem().setText(0, _fromUtf8("1")) + self.paramTree.header().setVisible(False) + self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) + self.label_3 = QtGui.QLabel(Form) + self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setObjectName(_fromUtf8("label_3")) + self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + pass + +from pyqtgraph.parametertree import ParameterTree diff --git a/GraphicsScene/exportDialogTemplate.ui b/GraphicsScene/exportDialogTemplate.ui new file mode 100644 index 00000000..0d840253 --- /dev/null +++ b/GraphicsScene/exportDialogTemplate.ui @@ -0,0 +1,93 @@ + + + Form + + + + 0 + 0 + 241 + 367 + + + + Form + + + + 0 + + + + + Item to export: + + + + + + + false + + + + 1 + + + + + + + + Export format + + + + + + + + + + Export + + + + + + + Close + + + + + + + false + + + + 1 + + + + + + + + Export options + + + + + + + + ParameterTree + QTreeWidget +
pyqtgraph.parametertree
+
+
+ + +
diff --git a/GraphicsScene/mouseEvents.py b/GraphicsScene/mouseEvents.py new file mode 100644 index 00000000..eb21229a --- /dev/null +++ b/GraphicsScene/mouseEvents.py @@ -0,0 +1,249 @@ +from pyqtgraph.Point import Point +from pyqtgraph.Qt import QtCore, QtGui +import weakref +import pyqtgraph.ptime as ptime + +class MouseDragEvent: + def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False): + self.start = start + self.finish = finish + self.accepted = False + self.currentItem = None + self._buttonDownScenePos = {} + self._buttonDownScreenPos = {} + for btn in [QtCore.Qt.LeftButton, QtCore.Qt.MidButton, QtCore.Qt.RightButton]: + self._buttonDownScenePos[int(btn)] = moveEvent.buttonDownScenePos(btn) + self._buttonDownScreenPos[int(btn)] = moveEvent.buttonDownScreenPos(btn) + self._scenePos = moveEvent.scenePos() + self._screenPos = moveEvent.screenPos() + if lastEvent is None: + self._lastScenePos = pressEvent.scenePos() + self._lastScreenPos = pressEvent.screenPos() + else: + self._lastScenePos = lastEvent.scenePos() + self._lastScreenPos = lastEvent.screenPos() + self._buttons = moveEvent.buttons() + self._button = pressEvent.button() + self._modifiers = moveEvent.modifiers() + + def accept(self): + self.accepted = True + self.acceptedItem = self.currentItem + + def ignore(self): + self.accepted = False + + def isAccepted(self): + return self.accepted + + def scenePos(self): + return Point(self._scenePos) + + def screenPos(self): + return Point(self._screenPos) + + def buttonDownScenePos(self, btn=None): + if btn is None: + btn = self.button() + return Point(self._buttonDownScenePos[int(btn)]) + + def buttonDownScreenPos(self, btn=None): + if btn is None: + btn = self.button() + return Point(self._buttonDownScreenPos[int(btn)]) + + def lastScenePos(self): + return Point(self._lastScenePos) + + def lastScreenPos(self): + return Point(self._lastScreenPos) + + def buttons(self): + return self._buttons + + def button(self): + """Return the button that initiated the drag (may be different from the buttons currently pressed)""" + return self._button + + def pos(self): + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def buttonDownPos(self, btn=None): + if btn is None: + btn = self.button() + return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)])) + + def isStart(self): + return self.start + + def isFinish(self): + return self.finish + + def __repr__(self): + lp = self.lastPos() + p = self.pos() + return "(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish())) + + def modifiers(self): + return self._modifiers + + + +class MouseClickEvent: + def __init__(self, pressEvent, double=False): + self.accepted = False + self.currentItem = None + self._double = double + self._scenePos = pressEvent.scenePos() + self._screenPos = pressEvent.screenPos() + self._button = pressEvent.button() + self._buttons = pressEvent.buttons() + self._modifiers = pressEvent.modifiers() + self._time = ptime.time() + + + def accept(self): + self.accepted = True + self.acceptedItem = self.currentItem + + def ignore(self): + self.accepted = False + + def isAccepted(self): + return self.accepted + + def scenePos(self): + return Point(self._scenePos) + + def screenPos(self): + return Point(self._screenPos) + + def buttons(self): + return self._buttons + + def button(self): + return self._button + + def double(self): + return self._double + + def pos(self): + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def modifiers(self): + return self._modifiers + + def __repr__(self): + p = self.pos() + return "" % (p.x(), p.y(), int(self.button())) + + def time(self): + return self._time + + + +class HoverEvent: + """ + This event class both informs items that the mouse cursor is nearby and allows items to + communicate with one another about whether each item will accept _potential_ mouse events. + + It is common for multiple overlapping items to receive hover events and respond by changing + their appearance. This can be misleading to the user since, in general, only one item will + respond to mouse events. To avoid this, items make calls to event.acceptClicks(button) + and/or acceptDrags(button). + + Each item may make multiple calls to acceptClicks/Drags, each time for a different button. + If the method returns True, then the item is guaranteed to be + the recipient of the claimed event IF the user presses the specified mouse button before + moving. If claimEvent returns False, then this item is guaranteed NOT to get the specified + event (because another has already claimed it) and the item should change its appearance + accordingly. + + event.isEnter() returns True if the mouse has just entered the item's shape; + event.isExit() returns True if the mouse has just left. + """ + def __init__(self, moveEvent, acceptable): + self.enter = False + self.acceptable = acceptable + self.exit = False + self.__clickItems = weakref.WeakValueDictionary() + self.__dragItems = weakref.WeakValueDictionary() + self.currentItem = None + if moveEvent is not None: + self._scenePos = moveEvent.scenePos() + self._screenPos = moveEvent.screenPos() + self._lastScenePos = moveEvent.lastScenePos() + self._lastScreenPos = moveEvent.lastScreenPos() + self._buttons = moveEvent.buttons() + self._modifiers = moveEvent.modifiers() + else: + self.exit = True + + + + def isEnter(self): + return self.enter + + def isExit(self): + return self.exit + + def acceptClicks(self, button): + """""" + if not self.acceptable: + return False + if button not in self.__clickItems: + self.__clickItems[button] = self.currentItem + return True + return False + + def acceptDrags(self, button): + if not self.acceptable: + return False + if button not in self.__dragItems: + self.__dragItems[button] = self.currentItem + return True + return False + + def scenePos(self): + return Point(self._scenePos) + + def screenPos(self): + return Point(self._screenPos) + + def lastScenePos(self): + return Point(self._lastScenePos) + + def lastScreenPos(self): + return Point(self._lastScreenPos) + + def buttons(self): + return self._buttons + + def pos(self): + return Point(self.currentItem.mapFromScene(self._scenePos)) + + def lastPos(self): + return Point(self.currentItem.mapFromScene(self._lastScenePos)) + + def __repr__(self): + lp = self.lastPos() + p = self.pos() + return "(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit())) + + def modifiers(self): + return self._modifiers + + def clickItems(self): + return self.__clickItems + + def dragItems(self): + return self.__dragItems + + + \ No newline at end of file diff --git a/__init__.py b/__init__.py index bf371d2f..86953817 100644 --- a/__init__.py +++ b/__init__.py @@ -19,11 +19,44 @@ def setConfigOption(opt, value): def getConfigOption(opt): return CONFIG_OPTIONS[opt] + +## Rename orphaned .pyc files. This is *probably* safe :) + +def renamePyc(startDir): + ### Used to rename orphaned .pyc files + ### When a python file changes its location in the repository, usually the .pyc file + ### is left behind, possibly causing mysterious and difficult to track bugs. + + printed = False + startDir = os.path.abspath(startDir) + for path, dirs, files in os.walk(startDir): + for f in files: + fileName = os.path.join(path, f) + base, ext = os.path.splitext(fileName) + py = base + ".py" + if ext == '.pyc' and not os.path.isfile(py): + if not printed: + print "NOTE: Renaming orphaned .pyc files:" + printed = True + n = 1 + while True: + name2 = fileName + ".renamed%d" % n + if not os.path.exists(name2): + break + n += 1 + print " " + fileName + " ==>" + print " " + name2 + os.rename(fileName, name2) + +import os +path = os.path.split(__file__)[0] +renamePyc(path) + + ## Import almost everything to make it available from a single namespace ## don't import the more complex systems--canvas, parametertree, flowchart, dockarea ## these must be imported separately. -import os def importAll(path): d = os.path.join(os.path.split(__file__)[0], path) files = [] @@ -101,4 +134,6 @@ show = image ## for backward compatibility def mkQApp(): if QtGui.QApplication.instance() is None: global QAPP - QAPP = QtGui.QApplication([]) \ No newline at end of file + QAPP = QtGui.QApplication([]) + + diff --git a/documentation/source/style.rst b/documentation/source/style.rst index fc172420..ae6233bd 100644 --- a/documentation/source/style.rst +++ b/documentation/source/style.rst @@ -12,6 +12,11 @@ For these function arguments, the following values may be used: * QColor * QPen / QBrush where appropriate -Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes. +Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes:: + mkPen('y', width=3, style=QtCore.Qt.DashLine) ## Make a dashed yellow line 2px wide + mkPen(0.5) ## solid grey line 1px wide + mkPen(color=(200, 200, 255), style=QtCore.Qt.DotLine) ## Dotted pale-blue line + +See the Qt documentation for 'QPen' and 'PenStyle' for more options. Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt's QColor class diff --git a/exporters/Exporter.py b/exporters/Exporter.py new file mode 100644 index 00000000..25ae367c --- /dev/null +++ b/exporters/Exporter.py @@ -0,0 +1,94 @@ +from pyqtgraph.widgets.FileDialog import FileDialog +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import os +LastExportDirectory = None + + +class Exporter(object): + """ + Abstract class used for exporting graphics to file / printer / whatever. + """ + + def __init__(self, item): + """ + Initialize with the item to be exported. + Can be an individual graphics item or a scene. + """ + object.__init__(self) + self.item = item + + def item(self): + return self.item + + def parameters(self): + """Return the parameters used to configure this exporter.""" + raise Exception("Abstract method must be overridden in subclass.") + + def export(self): + """""" + raise Exception("Abstract method must be overridden in subclass.") + + def fileSaveDialog(self, filter=None, opts=None): + ## Show a file dialog, call self.export(fileName) when finished. + if opts is None: + opts = {} + self.fileDialog = FileDialog() + self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + if filter is not None: + if isinstance(filter, basestring): + self.fileDialog.setNameFilter(filter) + elif isinstance(filter, list): + self.fileDialog.setNameFilters(filter) + global LastExportDirectory + exportDir = LastExportDirectory + if exportDir is not None: + self.fileDialog.setDirectory(exportDir) + self.fileDialog.show() + self.fileDialog.opts = opts + self.fileDialog.fileSelected.connect(self.fileSaveFinished) + return + + def fileSaveFinished(self, fileName): + fileName = str(fileName) + global LastExportDirectory + LastExportDirectory = os.path.split(fileName)[0] + self.export(fileName=fileName, **self.fileDialog.opts) + + + def getScene(self): + if isinstance(self.item, pg.GraphicsScene): + return self.item + else: + return self.item.scene() + + def getSourceRect(self): + if isinstance(self.item, pg.GraphicsScene): + return self.item.getViewWidget().viewRect() + else: + return self.item.sceneBoundingRect() + + def getTargetRect(self): + if isinstance(self.item, pg.GraphicsScene): + return self.item.getViewWidget().rect() + else: + return self.item.mapRectToDevice(self.item.boundingRect()) + + + + + #def writePs(self, fileName=None, item=None): + #if fileName is None: + #self.fileSaveDialog(self.writeSvg, filter="PostScript (*.ps)") + #return + #if item is None: + #item = self + #printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) + #printer.setOutputFileName(fileName) + #painter = QtGui.QPainter(printer) + #self.render(painter) + #painter.end() + + #def writeToPrinter(self): + #pass diff --git a/exporters/ImageExporter.py b/exporters/ImageExporter.py new file mode 100644 index 00000000..138ddabb --- /dev/null +++ b/exporters/ImageExporter.py @@ -0,0 +1,62 @@ +from Exporter import Exporter +from pyqtgraph.parametertree import Parameter +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import pyqtgraph as pg +import numpy as np + +__all__ = ['ImageExporter'] + +class ImageExporter(Exporter): + Name = "Image File (PNG, TIF, JPG, ...)" + def __init__(self, item): + Exporter.__init__(self, item) + tr = self.getTargetRect() + + self.params = Parameter(name='params', type='group', children=[ + {'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)}, + {'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)}, + {'name': 'antialias', 'type': 'bool', 'value': True}, + {'name': 'background', 'type': 'color', 'value': (0,0,0,255)}, + ]) + self.params.param('width').sigValueChanged.connect(self.widthChanged) + self.params.param('height').sigValueChanged.connect(self.heightChanged) + + def widthChanged(self): + sr = self.getSourceRect() + ar = sr.height() / sr.width() + self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged) + + def heightChanged(self): + sr = self.getSourceRect() + ar = sr.width() / sr.height() + self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged) + + def parameters(self): + return self.params + + def export(self, fileName=None): + if fileName is None: + filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()] + preferred = ['*.png', '*.tif', '*.jpg'] + for p in preferred[::-1]: + if p in filter: + filter.remove(p) + filter.insert(0, p) + self.fileSaveDialog(filter=filter) + return + + targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) + sourceRect = self.getSourceRect() + #self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32) + #self.png.fill(pyqtgraph.mkColor(self.params['background'])) + bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte) + color = self.params['background'] + bg[:,:,0] = color.blue() + bg[:,:,1] = color.green() + bg[:,:,2] = color.red() + bg[:,:,3] = color.alpha() + self.png = pg.makeQImage(bg, alpha=True) + painter = QtGui.QPainter(self.png) + self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect) + self.png.save(fileName) + painter.end() \ No newline at end of file diff --git a/exporters/SVGExporter.py b/exporters/SVGExporter.py new file mode 100644 index 00000000..40489628 --- /dev/null +++ b/exporters/SVGExporter.py @@ -0,0 +1,64 @@ +from Exporter import Exporter +from pyqtgraph.parametertree import Parameter +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import re + +__all__ = ['SVGExporter'] + +class SVGExporter(Exporter): + Name = "Scalable Vector Graphics (SVG)" + def __init__(self, item): + Exporter.__init__(self, item) + tr = self.getTargetRect() + self.params = Parameter(name='params', type='group', children=[ + {'name': 'width', 'type': 'float', 'value': tr.width(), 'limits': (0, None)}, + {'name': 'height', 'type': 'float', 'value': tr.height(), 'limits': (0, None)}, + ]) + self.params.param('width').sigValueChanged.connect(self.widthChanged) + self.params.param('height').sigValueChanged.connect(self.heightChanged) + + def widthChanged(self): + sr = self.getSourceRect() + ar = sr.height() / sr.width() + self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged) + + def heightChanged(self): + sr = self.getSourceRect() + ar = sr.width() / sr.height() + self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged) + + def parameters(self): + return self.params + + def export(self, fileName=None): + if fileName is None: + self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)") + return + self.svg = QtSvg.QSvgGenerator() + self.svg.setFileName(fileName) + self.svg.setSize(QtCore.QSize(100,100)) + #self.svg.setResolution(600) + #self.svg.setViewBox() + targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) + sourceRect = self.getSourceRect() + painter = QtGui.QPainter(self.svg) + self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect) + painter.end() + + ## Workaround to set pen widths correctly + data = open(fileName).readlines() + for i in range(len(data)): + line = data[i] + m = re.match(r'( Date: Mon, 12 Mar 2012 10:04:59 -0400 Subject: [PATCH 016/238] Fixed import error Fixed a few bugs in AxisItem --- GraphicsScene/GraphicsScene.py | 2 +- graphicsItems/AxisItem.py | 96 +++++++++++++++++++++++--------- graphicsItems/ViewBox/ViewBox.py | 5 +- 3 files changed, 76 insertions(+), 27 deletions(-) diff --git a/GraphicsScene/GraphicsScene.py b/GraphicsScene/GraphicsScene.py index 8f1481bb..e6679c2d 100644 --- a/GraphicsScene/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -4,7 +4,7 @@ from pyqtgraph.Point import Point import pyqtgraph.functions as fn import pyqtgraph.ptime as ptime from mouseEvents import * -import debug +import pyqtgraph.debug as debug import exportDialog try: diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 27323049..60621ff0 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -19,6 +19,7 @@ class AxisItem(GraphicsWidget): GraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) self.showValues = showValues + self.picture = None self.orientation = orientation if orientation not in ['left', 'right', 'top', 'bottom']: raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") @@ -53,7 +54,7 @@ class AxisItem(GraphicsWidget): pen = QtGui.QPen(QtGui.QColor(100, 100, 100)) self.setPen(pen) - self.linkedView = None + self._linkedView = None if linkView is not None: self.linkToView(linkView) @@ -70,6 +71,8 @@ class AxisItem(GraphicsWidget): def setGrid(self, grid): """Set the alpha value for the grid, or False to disable.""" self.grid = grid + self.picture = None + self.prepareGeometryChange() self.update() @@ -98,6 +101,7 @@ class AxisItem(GraphicsWidget): p.setY(int(self.size().height()-br.height()+nudge)) #self.label.resize(s) self.label.setPos(p) + self.picture = None def showLabel(self, show=True): #self.drawLabel = show @@ -122,6 +126,7 @@ class AxisItem(GraphicsWidget): self.labelStyle = args self.label.setHtml(self.labelString()) self.resizeEvent() + self.picture = None self.update() def labelString(self): @@ -147,6 +152,7 @@ class AxisItem(GraphicsWidget): h += self.textHeight self.setMaximumHeight(h) self.setMinimumHeight(h) + self.picture = None def setWidth(self, w=None): @@ -159,6 +165,7 @@ class AxisItem(GraphicsWidget): def setPen(self, pen): self.pen = pen + self.picture = None self.update() def setScale(self, scale=None): @@ -191,6 +198,7 @@ class AxisItem(GraphicsWidget): if scale != self.scale: self.scale = scale self.setLabel() + self.picture = None self.update() def setRange(self, mn, mx): @@ -199,32 +207,34 @@ class AxisItem(GraphicsWidget): self.range = [mn, mx] if self.autoScale: self.setScale() + self.picture = None self.update() - def linkToView(self, view): - if self.orientation in ['right', 'left']: - if self.linkedView is not None and self.linkedView() is not None: - #view.sigYRangeChanged.disconnect(self.linkedViewChanged) - ## should be this instead? - self.linkedView().sigYRangeChanged.disconnect(self.linkedViewChanged) - self.linkedView = weakref.ref(view) - view.sigYRangeChanged.connect(self.linkedViewChanged) - #signal = QtCore.SIGNAL('yRangeChanged') + def linkedView(self): + """Return the ViewBox this axis is linked to""" + if self._linkedView is None: + return None else: - if self.linkedView is not None and self.linkedView() is not None: - #view.sigYRangeChanged.disconnect(self.linkedViewChanged) - ## should be this instead? - self.linkedView().sigXRangeChanged.disconnect(self.linkedViewChanged) - self.linkedView = weakref.ref(view) + return self._linkedView() + + def linkToView(self, view): + oldView = self.linkedView() + self._linkedView = weakref.ref(view) + if self.orientation in ['right', 'left']: + if oldView is not None: + oldView.sigYRangeChanged.disconnect(self.linkedViewChanged) + view.sigYRangeChanged.connect(self.linkedViewChanged) + else: + if oldView is not None: + oldView.sigXRangeChanged.disconnect(self.linkedViewChanged) view.sigXRangeChanged.connect(self.linkedViewChanged) - #signal = QtCore.SIGNAL('xRangeChanged') - def linkedViewChanged(self, view, newRange): self.setRange(*newRange) def boundingRect(self): - if self.linkedView is None or self.linkedView() is None or self.grid is False: + linkedView = self.linkedView() + if linkedView is None or self.grid is False: rect = self.mapRectFromParent(self.geometry()) ## extend rect if ticks go in negative direction if self.orientation == 'left': @@ -237,19 +247,32 @@ class AxisItem(GraphicsWidget): rect.setTop(rect.top() + min(0,self.tickLength)) return rect else: - return self.mapRectFromParent(self.geometry()) | self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) + return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) def paint(self, p, opt, widget): + if self.picture is None: + self.picture = QtGui.QPicture() + painter = QtGui.QPainter(self.picture) + try: + self.drawPicture(painter) + finally: + painter.end() + self.picture.play(p) + + + def drawPicture(self, p): + prof = debug.Profiler("AxisItem.paint", disabled=True) p.setPen(self.pen) #bounds = self.boundingRect() bounds = self.mapRectFromParent(self.geometry()) - if self.linkedView is None or self.linkedView() is None or self.grid is False: + linkedView = self.linkedView() + if linkedView is None or self.grid is False: tbounds = bounds else: - tbounds = self.mapRectFromScene(self.linkedView().mapRectToScene(self.linkedView().boundingRect())) + tbounds = linkedView.mapRectToItem(self, linkedView.boundingRect()) if self.orientation == 'left': span = (bounds.topRight(), bounds.bottomRight()) @@ -372,6 +395,13 @@ class AxisItem(GraphicsWidget): a = 255 * distBetweenIntervals else: a = 255 + + lineAlpha = a + textAlpha = a + + if self.grid is not False: + print self.grid + lineAlpha = int(lineAlpha * self.grid / 255.) if axis == 0: offset = self.range[0] * xs - bounds.height() @@ -384,14 +414,16 @@ class AxisItem(GraphicsWidget): p1 = [0, 0] p2 = [0, 0] p1[axis] = tickStart - p2[axis] = tickStop + h*tickDir + p2[axis] = tickStop + if self.grid is False: + p2[axis] += h*tickDir p1[1-axis] = p2[1-axis] = x if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]: continue if p1[1-axis] < 0: continue - p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a))) + p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha))) # draw tick only if there is none tickPos = p1[1-axis] if tickPos not in tickPositions: @@ -421,7 +453,7 @@ class AxisItem(GraphicsWidget): #p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a))) #p.drawText(rect, textFlags, vstr) - texts.append((rect, textFlags, vstr, a)) + texts.append((rect, textFlags, vstr, textAlpha)) prof.mark('draw ticks') for args in texts: @@ -446,9 +478,23 @@ class AxisItem(GraphicsWidget): GraphicsWidget.hide(self) def wheelEvent(self, ev): - if self.linkedView is None or self.linkedView() is None: return + if self.linkedView() is None: + return if self.orientation in ['left', 'right']: self.linkedView().wheelEvent(ev, axis=1) else: self.linkedView().wheelEvent(ev, axis=0) ev.accept() + + def mouseDragEvent(self, event): + if self.linkedView() is None: + return + if self.orientation in ['left', 'right']: + return self.linkedView().mouseDragEvent(event, axis=1) + else: + return self.linkedView().mouseDragEvent(event, axis=0) + + def mouseClickEvent(self, event): + if self.linkedView() is None: + return + return self.linkedView().mouseClickEvent(event) diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index f62269a4..0bc84c2c 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -662,7 +662,8 @@ class ViewBox(GraphicsWidget): #return [self.getMenu(event)] - def mouseDragEvent(self, ev): + def mouseDragEvent(self, ev, axis=None): + ## if axis is specified, event will only affect that axis. ev.accept() ## we accept all buttons pos = ev.pos() @@ -672,6 +673,8 @@ class ViewBox(GraphicsWidget): ## Ignore axes if mouse is disabled mask = np.array(self.state['mouseEnabled'], dtype=np.float) + if axis is not None: + mask[1-axis] = 0.0 ## Scale or translate based on mouse button if ev.button() & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton): From fe4e177d8eb85b4b960afa7cfa380db65df74010 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 12 Mar 2012 12:23:25 -0400 Subject: [PATCH 017/238] AxisItem fixes documentation update removed documentation builds from repo. --- canvas/Canvas.py | 2 +- canvas/CanvasItem.py | 2 +- .../build/doctrees/environment.pickle | Bin 77056 -> 0 bytes .../doctrees/graphicsItems/index.doctree | Bin 4809 -> 0 bytes .../build/doctrees/graphicswindow.doctree | Bin 3501 -> 0 bytes .../build/doctrees/how_to_use.doctree | Bin 9236 -> 0 bytes documentation/build/doctrees/images.doctree | Bin 11808 -> 0 bytes documentation/build/doctrees/index.doctree | Bin 6042 -> 0 bytes .../build/doctrees/introduction.doctree | Bin 13863 -> 0 bytes .../build/doctrees/parametertree.doctree | Bin 3458 -> 0 bytes documentation/build/doctrees/plotting.doctree | Bin 26167 -> 0 bytes .../build/doctrees/region_of_interest.doctree | Bin 4508 -> 0 bytes .../build/doctrees/widgets/dockarea.doctree | Bin 2862 -> 0 bytes .../build/doctrees/widgets/index.doctree | Bin 4236 -> 0 bytes .../doctrees/widgets/parametertree.doctree | Bin 2912 -> 0 bytes documentation/build/html/.buildinfo | 4 - .../build/html/_images/plottingClasses.png | Bin 68667 -> 0 bytes documentation/build/html/_modules/index.html | 89 - .../build/html/_modules/pyqtgraph.html | 192 - .../build/html/_sources/apireference.txt | 11 - .../build/html/_sources/functions.txt | 53 - .../html/_sources/graphicsItems/arrowitem.txt | 8 - .../html/_sources/graphicsItems/axisitem.txt | 8 - .../_sources/graphicsItems/buttonitem.txt | 8 - .../_sources/graphicsItems/curvearrow.txt | 8 - .../_sources/graphicsItems/curvepoint.txt | 8 - .../graphicsItems/gradienteditoritem.txt | 8 - .../_sources/graphicsItems/gradientlegend.txt | 8 - .../_sources/graphicsItems/graphicslayout.txt | 8 - .../_sources/graphicsItems/graphicsobject.txt | 8 - .../_sources/graphicsItems/graphicswidget.txt | 8 - .../html/_sources/graphicsItems/griditem.txt | 8 - .../graphicsItems/histogramlutitem.txt | 8 - .../html/_sources/graphicsItems/imageitem.txt | 8 - .../html/_sources/graphicsItems/index.txt | 37 - .../_sources/graphicsItems/infiniteline.txt | 8 - .../html/_sources/graphicsItems/labelitem.txt | 8 - .../graphicsItems/linearregionitem.txt | 8 - .../_sources/graphicsItems/plotcurveitem.txt | 8 - .../_sources/graphicsItems/plotdataitem.txt | 8 - .../html/_sources/graphicsItems/plotitem.txt | 7 - .../build/html/_sources/graphicsItems/roi.txt | 8 - .../html/_sources/graphicsItems/scalebar.txt | 8 - .../graphicsItems/scatterplotitem.txt | 8 - .../_sources/graphicsItems/uigraphicsitem.txt | 8 - .../html/_sources/graphicsItems/viewbox.txt | 8 - .../_sources/graphicsItems/vtickgroup.txt | 8 - .../build/html/_sources/graphicswindow.txt | 8 - .../build/html/_sources/how_to_use.txt | 47 - documentation/build/html/_sources/images.txt | 26 - documentation/build/html/_sources/index.txt | 32 - .../build/html/_sources/introduction.txt | 51 - .../build/html/_sources/parametertree.txt | 7 - .../build/html/_sources/plotting.txt | 73 - .../html/_sources/region_of_interest.txt | 19 - documentation/build/html/_sources/style.txt | 17 - .../html/_sources/widgets/checktable.txt | 8 - .../html/_sources/widgets/colorbutton.txt | 8 - .../html/_sources/widgets/datatreewidget.txt | 8 - .../build/html/_sources/widgets/dockarea.txt | 5 - .../html/_sources/widgets/filedialog.txt | 8 - .../html/_sources/widgets/gradientwidget.txt | 8 - .../_sources/widgets/graphicslayoutwidget.txt | 8 - .../html/_sources/widgets/graphicsview.txt | 8 - .../_sources/widgets/histogramlutwidget.txt | 8 - .../build/html/_sources/widgets/index.txt | 31 - .../html/_sources/widgets/joystickbutton.txt | 8 - .../html/_sources/widgets/multiplotwidget.txt | 8 - .../html/_sources/widgets/parametertree.txt | 5 - .../html/_sources/widgets/plotwidget.txt | 8 - .../html/_sources/widgets/progressdialog.txt | 8 - .../html/_sources/widgets/rawimagewidget.txt | 8 - .../build/html/_sources/widgets/spinbox.txt | 8 - .../html/_sources/widgets/tablewidget.txt | 8 - .../html/_sources/widgets/treewidget.txt | 8 - .../html/_sources/widgets/verticallabel.txt | 8 - documentation/build/html/_static/basic.css | 509 - documentation/build/html/_static/default.css | 255 - documentation/build/html/_static/doctools.js | 247 - documentation/build/html/_static/file.png | Bin 392 -> 0 bytes documentation/build/html/_static/jquery.js | 8176 ----------------- documentation/build/html/_static/minus.png | Bin 199 -> 0 bytes documentation/build/html/_static/plus.png | Bin 199 -> 0 bytes documentation/build/html/_static/pygments.css | 61 - .../build/html/_static/searchtools.js | 518 -- documentation/build/html/_static/sidebar.js | 147 - .../build/html/_static/underscore.js | 16 - documentation/build/html/apireference.html | 178 - documentation/build/html/functions.html | 341 - documentation/build/html/genindex.html | 457 - .../build/html/graphicsItems/arrowitem.html | 132 - .../build/html/graphicsItems/axisitem.html | 154 - .../build/html/graphicsItems/buttonitem.html | 131 - .../build/html/graphicsItems/curvearrow.html | 132 - .../build/html/graphicsItems/curvepoint.html | 136 - .../graphicsItems/gradienteditoritem.html | 136 - .../html/graphicsItems/gradientlegend.html | 138 - .../html/graphicsItems/graphicslayout.html | 144 - .../html/graphicsItems/graphicsobject.html | 189 - .../html/graphicsItems/graphicswidget.html | 132 - .../build/html/graphicsItems/griditem.html | 132 - .../html/graphicsItems/histogramlutitem.html | 130 - .../build/html/graphicsItems/imageitem.html | 184 - .../build/html/graphicsItems/index.html | 149 - .../html/graphicsItems/infiniteline.html | 155 - .../build/html/graphicsItems/labelitem.html | 154 - .../html/graphicsItems/linearregionitem.html | 138 - .../html/graphicsItems/plotcurveitem.html | 141 - .../html/graphicsItems/plotdataitem.html | 289 - .../build/html/graphicsItems/plotitem.html | 245 - .../build/html/graphicsItems/roi.html | 185 - .../build/html/graphicsItems/scalebar.html | 131 - .../html/graphicsItems/scatterplotitem.html | 171 - .../html/graphicsItems/uigraphicsitem.html | 179 - .../build/html/graphicsItems/viewbox.html | 227 - .../build/html/graphicsItems/vtickgroup.html | 132 - documentation/build/html/graphicswindow.html | 123 - documentation/build/html/how_to_use.html | 161 - documentation/build/html/images.html | 135 - documentation/build/html/index.html | 160 - documentation/build/html/introduction.html | 163 - documentation/build/html/objects.inv | Bin 2225 -> 0 bytes documentation/build/html/parametertree.html | 123 - documentation/build/html/plotting.html | 217 - documentation/build/html/py-modindex.html | 118 - .../build/html/region_of_interest.html | 140 - documentation/build/html/search.html | 102 - documentation/build/html/searchindex.js | 1 - documentation/build/html/style.html | 127 - .../build/html/widgets/checktable.html | 130 - .../build/html/widgets/colorbutton.html | 130 - .../build/html/widgets/datatreewidget.html | 138 - .../build/html/widgets/dockarea.html | 120 - .../build/html/widgets/filedialog.html | 130 - .../build/html/widgets/gradientwidget.html | 130 - .../html/widgets/graphicslayoutwidget.html | 130 - .../build/html/widgets/graphicsview.html | 162 - .../html/widgets/histogramlutwidget.html | 130 - documentation/build/html/widgets/index.html | 144 - .../build/html/widgets/joystickbutton.html | 130 - .../build/html/widgets/multiplotwidget.html | 131 - .../build/html/widgets/parametertree.html | 120 - .../build/html/widgets/plotwidget.html | 131 - .../build/html/widgets/progressdialog.html | 153 - .../build/html/widgets/rawimagewidget.html | 132 - documentation/build/html/widgets/spinbox.html | 164 - .../build/html/widgets/tablewidget.html | 168 - .../build/html/widgets/treewidget.html | 140 - .../build/html/widgets/verticallabel.html | 130 - documentation/source/index.rst | 1 + examples/Arrow.py | 2 +- examples/PlotSpeedTest.py | 5 +- examples/ScatterPlot.py | 2 +- examples/__main__.py | 2 +- flowchart/Flowchart.py | 2 +- flowchart/Node.py | 2 +- graphicsItems/AxisItem.py | 69 +- widgets/GraphicsView.py | 8 +- widgets/PlotWidget.py | 2 +- 159 files changed, 52 insertions(+), 20473 deletions(-) delete mode 100644 documentation/build/doctrees/environment.pickle delete mode 100644 documentation/build/doctrees/graphicsItems/index.doctree delete mode 100644 documentation/build/doctrees/graphicswindow.doctree delete mode 100644 documentation/build/doctrees/how_to_use.doctree delete mode 100644 documentation/build/doctrees/images.doctree delete mode 100644 documentation/build/doctrees/index.doctree delete mode 100644 documentation/build/doctrees/introduction.doctree delete mode 100644 documentation/build/doctrees/parametertree.doctree delete mode 100644 documentation/build/doctrees/plotting.doctree delete mode 100644 documentation/build/doctrees/region_of_interest.doctree delete mode 100644 documentation/build/doctrees/widgets/dockarea.doctree delete mode 100644 documentation/build/doctrees/widgets/index.doctree delete mode 100644 documentation/build/doctrees/widgets/parametertree.doctree delete mode 100644 documentation/build/html/.buildinfo delete mode 100644 documentation/build/html/_images/plottingClasses.png delete mode 100644 documentation/build/html/_modules/index.html delete mode 100644 documentation/build/html/_modules/pyqtgraph.html delete mode 100644 documentation/build/html/_sources/apireference.txt delete mode 100644 documentation/build/html/_sources/functions.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/arrowitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/axisitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/buttonitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/curvearrow.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/curvepoint.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/gradienteditoritem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/gradientlegend.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/graphicslayout.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/graphicsobject.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/graphicswidget.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/griditem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/histogramlutitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/imageitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/index.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/infiniteline.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/labelitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/linearregionitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/plotcurveitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/plotdataitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/plotitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/roi.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/scalebar.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/scatterplotitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/uigraphicsitem.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/viewbox.txt delete mode 100644 documentation/build/html/_sources/graphicsItems/vtickgroup.txt delete mode 100644 documentation/build/html/_sources/graphicswindow.txt delete mode 100644 documentation/build/html/_sources/how_to_use.txt delete mode 100644 documentation/build/html/_sources/images.txt delete mode 100644 documentation/build/html/_sources/index.txt delete mode 100644 documentation/build/html/_sources/introduction.txt delete mode 100644 documentation/build/html/_sources/parametertree.txt delete mode 100644 documentation/build/html/_sources/plotting.txt delete mode 100644 documentation/build/html/_sources/region_of_interest.txt delete mode 100644 documentation/build/html/_sources/style.txt delete mode 100644 documentation/build/html/_sources/widgets/checktable.txt delete mode 100644 documentation/build/html/_sources/widgets/colorbutton.txt delete mode 100644 documentation/build/html/_sources/widgets/datatreewidget.txt delete mode 100644 documentation/build/html/_sources/widgets/dockarea.txt delete mode 100644 documentation/build/html/_sources/widgets/filedialog.txt delete mode 100644 documentation/build/html/_sources/widgets/gradientwidget.txt delete mode 100644 documentation/build/html/_sources/widgets/graphicslayoutwidget.txt delete mode 100644 documentation/build/html/_sources/widgets/graphicsview.txt delete mode 100644 documentation/build/html/_sources/widgets/histogramlutwidget.txt delete mode 100644 documentation/build/html/_sources/widgets/index.txt delete mode 100644 documentation/build/html/_sources/widgets/joystickbutton.txt delete mode 100644 documentation/build/html/_sources/widgets/multiplotwidget.txt delete mode 100644 documentation/build/html/_sources/widgets/parametertree.txt delete mode 100644 documentation/build/html/_sources/widgets/plotwidget.txt delete mode 100644 documentation/build/html/_sources/widgets/progressdialog.txt delete mode 100644 documentation/build/html/_sources/widgets/rawimagewidget.txt delete mode 100644 documentation/build/html/_sources/widgets/spinbox.txt delete mode 100644 documentation/build/html/_sources/widgets/tablewidget.txt delete mode 100644 documentation/build/html/_sources/widgets/treewidget.txt delete mode 100644 documentation/build/html/_sources/widgets/verticallabel.txt delete mode 100644 documentation/build/html/_static/basic.css delete mode 100644 documentation/build/html/_static/default.css delete mode 100644 documentation/build/html/_static/doctools.js delete mode 100644 documentation/build/html/_static/file.png delete mode 100644 documentation/build/html/_static/jquery.js delete mode 100644 documentation/build/html/_static/minus.png delete mode 100644 documentation/build/html/_static/plus.png delete mode 100644 documentation/build/html/_static/pygments.css delete mode 100644 documentation/build/html/_static/searchtools.js delete mode 100644 documentation/build/html/_static/sidebar.js delete mode 100644 documentation/build/html/_static/underscore.js delete mode 100644 documentation/build/html/apireference.html delete mode 100644 documentation/build/html/functions.html delete mode 100644 documentation/build/html/genindex.html delete mode 100644 documentation/build/html/graphicsItems/arrowitem.html delete mode 100644 documentation/build/html/graphicsItems/axisitem.html delete mode 100644 documentation/build/html/graphicsItems/buttonitem.html delete mode 100644 documentation/build/html/graphicsItems/curvearrow.html delete mode 100644 documentation/build/html/graphicsItems/curvepoint.html delete mode 100644 documentation/build/html/graphicsItems/gradienteditoritem.html delete mode 100644 documentation/build/html/graphicsItems/gradientlegend.html delete mode 100644 documentation/build/html/graphicsItems/graphicslayout.html delete mode 100644 documentation/build/html/graphicsItems/graphicsobject.html delete mode 100644 documentation/build/html/graphicsItems/graphicswidget.html delete mode 100644 documentation/build/html/graphicsItems/griditem.html delete mode 100644 documentation/build/html/graphicsItems/histogramlutitem.html delete mode 100644 documentation/build/html/graphicsItems/imageitem.html delete mode 100644 documentation/build/html/graphicsItems/index.html delete mode 100644 documentation/build/html/graphicsItems/infiniteline.html delete mode 100644 documentation/build/html/graphicsItems/labelitem.html delete mode 100644 documentation/build/html/graphicsItems/linearregionitem.html delete mode 100644 documentation/build/html/graphicsItems/plotcurveitem.html delete mode 100644 documentation/build/html/graphicsItems/plotdataitem.html delete mode 100644 documentation/build/html/graphicsItems/plotitem.html delete mode 100644 documentation/build/html/graphicsItems/roi.html delete mode 100644 documentation/build/html/graphicsItems/scalebar.html delete mode 100644 documentation/build/html/graphicsItems/scatterplotitem.html delete mode 100644 documentation/build/html/graphicsItems/uigraphicsitem.html delete mode 100644 documentation/build/html/graphicsItems/viewbox.html delete mode 100644 documentation/build/html/graphicsItems/vtickgroup.html delete mode 100644 documentation/build/html/graphicswindow.html delete mode 100644 documentation/build/html/how_to_use.html delete mode 100644 documentation/build/html/images.html delete mode 100644 documentation/build/html/index.html delete mode 100644 documentation/build/html/introduction.html delete mode 100644 documentation/build/html/objects.inv delete mode 100644 documentation/build/html/parametertree.html delete mode 100644 documentation/build/html/plotting.html delete mode 100644 documentation/build/html/py-modindex.html delete mode 100644 documentation/build/html/region_of_interest.html delete mode 100644 documentation/build/html/search.html delete mode 100644 documentation/build/html/searchindex.js delete mode 100644 documentation/build/html/style.html delete mode 100644 documentation/build/html/widgets/checktable.html delete mode 100644 documentation/build/html/widgets/colorbutton.html delete mode 100644 documentation/build/html/widgets/datatreewidget.html delete mode 100644 documentation/build/html/widgets/dockarea.html delete mode 100644 documentation/build/html/widgets/filedialog.html delete mode 100644 documentation/build/html/widgets/gradientwidget.html delete mode 100644 documentation/build/html/widgets/graphicslayoutwidget.html delete mode 100644 documentation/build/html/widgets/graphicsview.html delete mode 100644 documentation/build/html/widgets/histogramlutwidget.html delete mode 100644 documentation/build/html/widgets/index.html delete mode 100644 documentation/build/html/widgets/joystickbutton.html delete mode 100644 documentation/build/html/widgets/multiplotwidget.html delete mode 100644 documentation/build/html/widgets/parametertree.html delete mode 100644 documentation/build/html/widgets/plotwidget.html delete mode 100644 documentation/build/html/widgets/progressdialog.html delete mode 100644 documentation/build/html/widgets/rawimagewidget.html delete mode 100644 documentation/build/html/widgets/spinbox.html delete mode 100644 documentation/build/html/widgets/tablewidget.html delete mode 100644 documentation/build/html/widgets/treewidget.html delete mode 100644 documentation/build/html/widgets/verticallabel.html diff --git a/canvas/Canvas.py b/canvas/Canvas.py index 8514b60e..5f7c1ac6 100644 --- a/canvas/Canvas.py +++ b/canvas/Canvas.py @@ -15,7 +15,7 @@ from pyqtgraph.graphicsItems.ViewBox import ViewBox from pyqtgraph.graphicsItems.GridItem import GridItem #import DataManager import numpy as np -import debug +from pyqtgraph import debug #import pyqtgraph as pg import weakref from CanvasManager import CanvasManager diff --git a/canvas/CanvasItem.py b/canvas/CanvasItem.py index 6298d364..d9b6f100 100644 --- a/canvas/CanvasItem.py +++ b/canvas/CanvasItem.py @@ -3,7 +3,7 @@ from pyqtgraph.Qt import QtGui, QtCore, QtSvg from pyqtgraph.graphicsItems.ROI import ROI import pyqtgraph as pg import TransformGuiTemplate -import debug +from pyqtgraph import debug class SelectBox(ROI): def __init__(self, scalable=False, rotatable=True): diff --git a/documentation/build/doctrees/environment.pickle b/documentation/build/doctrees/environment.pickle deleted file mode 100644 index eee3550a391102da9f44347003ad2e3b52f874e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77056 zcmc${37i~789p4Yge>(UG|GxaPb#&G9zHdEM zU0qdOQ+@Kv&0W=jQcwSq)?)vn9x3J?Q-8;Xh zw^*&L>VSiOPjAx{T((mkRf$=@v@UaQ(AhTJ@9|lH4TH)?&21Yj?&)4o z^s5v7LT7K$L@FD1b(gzXu4-$4xw}|xQb_x=s6F+v;5*(M!7ub6 zR+&&@(OZH_D-^U1E1`@Nn@d}^m$qszZQWkVwU=Oe2?p;_8ar=I*ftAww?EL7RoM~7 z?G#jYrg3$NyOi>!7FbmpU!oOjx%%ko?=CK>?5c`)3o2g;74M#@_^VLNMlfq>#n*z2 ztL6+h)yf{wzh_X{i~1Yo*&Al;7MZb0k_>H;%EUfu(7r+CYoS4tG7Z`f291P4)7ZY1 zhRHBv|DbXJ%}Az9f%1JLVq;>PlxIeP<FibqHX#`@ugU-9HGjO3@S&3%8$-e-VWs(LHX>$Ku>pW zYTL9Nis6_2r32_fD#yTvj-YZZZD^dU08@^POj+L%LQ71z(y8`z1(oj5o?@mw^I=aD z?3oL!X^52tFrpMxdT2x{$%E#u49)D*tmcKPxi_fvg_`>_HJ7bs-h)>205n&Eia5=& zq$)J`Mw+)swmlascY4Jxzv8P6gF$6cXv5-68*~B2Y6<3@RsuW}KX9#wjpk9q2ij0%C!yF}tu>`zG5&E`k-`4l3VqR-}?Ih85>UR*Z60 zQ|hVu=q(C;y@RfyUZR#<8dNR|ExA0?lJCNj^-((qIvF$C=2~m6fHmI>Dp%5)23f9x zMVCevxq-yhs{NXv^8HZz4>Gm?5ZX6~_WgUR1HFZ%=p}N>XEj&o@6Iji=`NNl*TRhJ zg36C*#CXr!*#!a4R(57-?=Ql^1vT<&ME>v2vSgyFIA6F%P=0Tud^IzAcu-Y66jXj2s(Lt6)gw^V3RQ;}d%MbgMKa)*iaCw~ zIUp|hJ>~w~{IW!xq) z$jY;7;B!Ic&!K_OXBzkd3|tEa9%kWMnUxn|(Mv()Wm;69`U*^VE;3=wo_=4--GjD@GW2V->am;1E|zpXaB z6I9*}ZFn!!hQGrGKI!*UthV9zVZjGMzZ2;XdwST>-GgRms-p6dYX3N> z{4>=4Nv8Hsq1`@Z6>t02f5C!(2bKTOf{f(PV8zFg6&tz+;Mm}BwDP&y@I_GhZ)n4p znKt~-*1}Xf5a5_phOIPg*y*RpO7H?%87@s>btH1Ff&|#|MP$o*&IU4BohaoVS(TYE zIg!;=*l;6ErzkQ)BlWWsS)HJ<^Hzbz*@sM%k%+>aCU}9Yp`cM=;+o8x_b5Izq_noG zTuT9K8-P){jz%(6u8Rby`j z)s}kZ6qfW<*@rW(nxcr${`BWMx?k#ZS@YcO(ic!6x8Nb@WT zb8Jae-lL+(t}T=GR_(%cxhNdasd-xwL-V#qAdno~(7bIll40Ixg3Rb{j*&;A)XFhP zf|X<81+uLQFvGlb#_cF0?@?`JY&h!gDiymH>au{0V@6uoj6fiHxS@sPHIiZB_5_*x ze=&M0OQ!IuH3OA+&H0 z1OnL;ZfN0N8p*J5Z-T-eOqV814<`E{1y=40FOaV(KJ3ck`6f|9-lNjU($%W|(%z!% z$26KW8G%6dhZ~x7fJQP*nnIAdi)Zxo7boPf?$tX%2aJ=;y=9T9$N7;b3lOpRoidI&*g9bgXRU>!ha;S!8H z6kZ^+6=)U&T!uNsq>z zSXm&*-oF$zNkw(yDlF`R7f82a?fo0&DH56YsBCEK2Fe`U%x895w*Y}aN^nE#dNh(@ zokvj2x>+h}A+Ex@UU-4@DK=)E$j|X70t5miK5tWNzw77d}dsFf;94ia;R8!42&_ zULzUyom2)o>A(odGY9GZkuPLs6Eqh|7Ca6`8ia6c=D}(NUh?fks!v z0-rBuGb63~1_FV66K-hLw=|Mr)j0&2x$|rl*;1b0;zZ`U${cU^R^?n|gPrHW3*>wi zW+u@?fi9rzyhn2)%Tp`UJ^k~0I1cYc4=fiFz!F@9Kp@|S8bzP^8IpadCwio2P%tp(uKp>Fs!3{0DQX?6b zT}6-?)i^=!uDKdXFzy<7fqY*Dm{CnS;}0k!?@?`J-&XF4%r;b=7+<#0_W47CXzjHK z1ackR(ApnqB*WV42^u?ZT?}gDYdG3WZ$KK%y%Am@H>reC**R~fl)Oj9k-h6VK6Ooo za}N12^U=hgAP~qca6=P+s*wy6ZzU+U60h@*+i(%K-3~91pD8r9B(GWSATIAwRb*5+ zBlF5js~mrsIidWVnQ7Ub2n2E$+|aULXe7h3y9qK6!Go+gGfk9xkOK4Wg%`*#6>lDb zseJcQLf)g&$i`Gf^Cm$rr1z;-^sfk_t@k4k$gkmswmzVd3|oIgkf~_iCDxrQi6$oS+vey zAdFVNh(I7O!40i^StA)%zCw^$pq*+(9-tz%)?Y;`tbGk$Ag`+ovr?Op_YF$QdsH4- zy+P7yU9gkCFeeRt6M;bf3O6+LEsbOt`Zt2c>YL;EG8hceD$UW}s=SR{F!&vKfxN4- zMlB2T9wp~J8WS_z^-NtH@!KBY@61WV-$x*j58#G||3f1ghJQ$qd6%oR2YdT}gv+q> zV|ao5Q^Dq?ZamW`#O6J!i%eS+@4oPA8PE3quA+R(EVSug2n6zPxS>t|(MX0(pAlr1 z>?X~cmYW?)`y2_d>I-;*{8!;-?Jk+^OXBk$RYrD&{U%>1M=#Ulf6Pn^hpkMhE5Qvd zTv;O-7OsK>Sjd$tzG05uugR)Nfr+cZ3uL(B&E1{KH-ZxK9+gHmZsDrcwM)|jnX9ro zA+&TP0)aHa4J}*iNst)_yT)kxp~Lm1y{v_F*t<5oK-N(yW*nTEdtFM)do&=j zdTnRzV%$KO=&Z*~v~zs~u#N;bv~xp^WZ1b8L1tFFU!mI5mFqUIL~@oA*%%pM=qB(2 z*;K`tS#5Uq%_uML(S*p-&7GxsJL>iVouFw4HHrwDy*UC{S%Mpyy`@Gn%-)J1(^NSq zpUnA}_Dx$O0p{l51+tC8O;b%~8%=!Pqsqw4)WiZyRlUVdJV3`VGc6p809KXYh8Aw8 zkqisR5o9J7SbFL$?gv55xCZ0$@B$gHNHehrb8Jsk-lL+>uEDmZ?s8uNm5e4Y6BOJ+ zaN8neR!cF^lP>h;wz`!c=kPKdXyF|f47U1=LuYg--Dd6T>| zl07=SJiLmheOF0#VcbS=U0*;ImlXO2u$4x3MPljIw$;N6yWv7@ux$ih8dNdokgp&< z9e8Wj?uZ8RRk+PwhB0nuQE!=~HFeCDLVvYaU&3H(!IGs-vIn54TzyNijmPKq7}*o4 zZEGH^lg->T?jw=CaH)2R>Hw(KzWItxbN)vOcc3wDL_UC0tF!XsOO?f@Q@_NP~Y6*KO z$^-gVNDe@TK&HTL8)06^S7oYRI}q+*+e%$nb3&-uGgl8n*fZ1*wyiv{RHkd@8L=z} zGpwu9;oH8}!hB42igS8<@C=!W^wQ&Mm3uoShu~Um-qz@UBaE9Bw_?1xaNgunp?^WK zTV^4rw*;O1yy3`=O&z6jcTHbGMaXd0+S-A6X^-F0A%_xnN*c!ZuMsR%h+Sp#Fw$Ho zvon?P?S6#P$%M>7&UB&X;#y5#HAlI_JF<hhQ62rbLuMAZ?O)R>$ML>lK2%Na?6^i9*&##hshAY?8fvGOG=c$%Gbb5P$3&L{OR zz)H@M1YVfd#XWoxi=0kj^|6(6xY^i5B1ZvKnwqd~lLR8H+W~azax`_+al!YPkq+I+ zz`mcRX++woWuJtWEgNHI6c-h-W^qhCXiNg+s+omq)rZ5}veDN;=;sN2xdb%v1~jU! zV~Kk`uqTcU(ApL(8+T9hnMs@6ZTQ0q&6>EF|iGwQTrJnXyX`5Qc+FG+t=}|?KbRa zh1g`9P4=?xwH|LP?G2?0h__L)+12JHTO4xo{hn(_;uEt~s&aA(s-1mieB9cP=lE<4 zX9tPo8bGXkHJ!CZwG_8;Dt6h(qD&L{<~0Jvr&5cVpA)z^^U2$uyf(>$ zKx~5QEzTsS2V+}6N%%R zUjmnGHiYomqI;%wn21d}eR2|@sV*+5Pp#CEN5s&{)Ym?=zVuUYzw%&zcU4XywC*vu zUZQ&3JGN7qSNrX--;uB5T5Vp^MAzekMhtqfp~qh;r-j(466JISCtHqLNzlnq4audq zagk5Mhv7taj#~IY6V}eLmZC;Z8WQB*_%9(`L)%eBn<88wDw>WdT@CW_V>ue zG_*5TG`R#<(&LiU;?$*7$Az(YR zle4D2g|mwNas~6I?!E76?t1I0g+;|VSn4avmB>+VU9zi-o^z39R}rTVy!ai%y@FAf zz`a{nhw{VjG~JW3N=dStAFwdj09Kk1D|i~DPY8Wq9@+GL>YC50iQCku{S^JV{GpNj zfT+4Vc>6?2+_T#bgYgj7CkqslxwL~XMr(Zk~bEc5hm(* z%$v?et|O+dCFxJA+*aV$aJnu(Vs4$K$28;<#Y#0hycO+r<8Fe% zCpI?3@J(){{<^VY^1gK4+`{70S^P!I0gJGWQf{K!WG|Ow#yxw(F6U8?athOd= zA=%5xPpIkK_?^smrBMs6_8hRN&?~nvTk6*OsY>VG_jvXx7~Wt7{niY?Z3;*ZI{Frx z3CZml`G2PQljdWjp}+jGucYoEY*u8x{9G|(6GqjIjnR6|>p{5_+3N?#*<53sb6BBw zuqbyCl;-*usyO+qPCsoqQsO3-rWQ;6*R* z%pgB{cK?d%`HnEruO(YX@>Z7nskIhs8NbGb+Pq}TNKvel`x(Xl1%62$AU@fhB}cGb ze7QDv5X;N*8|F>j9S^FyT%tzf(_{R)6uZ+{OY#umi3##=6`bt1bzi!BLQKN1IptyE z?nzLRY=7zJ%A8UGuVDCed4$m9(_0IqL%$v#gT!W`7c zlhl-I27gpblcNb%i@A-X8|*1!7bfa5S-fP&7XhCpa8?W`e^Qmn#u}-VXN;!UBQaU? z(Eu9VWH@k_X9-(@;WJu{S$2Gmz~t~bIXQ@?LLu#c2Bv-@lmXNGA%s0&Z{sqX=4`$Q zaC4a#C?L@^U&NL4s$Og#^h-2!96P2MpQAIIm)SqcMF+i;tMW3jb@SI`t*06_?TP!}uhpwcLX#bDYhM{Z%|cIu1%cNIP0;xU zuGGwL;llx2SpV`D+Nty3n4QV-g0t#Pik=#0y^grp!ew;3{FS(mVz*0ri4>1#Ga`A5 zIa7D@-*7EG_~RfIbWQCocNTi16`r?|wSK6Y9Q^UF)D~=}f%EjaEAJ3lHyqzK(Wp|K zqfyP|I+L#Tyi3CqtiPvb52^bIqx+BkXSfO>N2XN0J(Qv$D_AZdU$7L~y_1(qhzklV%npl|68@F25!^;}Xcthii?#-60NelF)Tg-xF^NtZ~ zIbDm=lbmjsXk`xK4xC9W&8%IIPAZIgEbz)>X=EKjk{wo~2LK?mmSBTUQPw3W`2a{# z%ZA3ukv7crn0rE;v80CBI&9dztxrs16?Ow$sm)71L;Du)Cxg|JY{=ZHXXr+nKdC8t z!xD8U8x#9>;_=d;H^Es+IJ;-kppSKTY;lrJfUF;sWVA^03!Y7xT^Ax_?IEekRc;UN z=S0PBMkABWBDumBKBi?9v+mQF?P#PPaj-c}(;<7jdrNW`y|<5WTM(x&@Z-3;QDR!$#i{csvRz`H84jXdDP*Ji;Y@6sQ;tz=Q)Oswz zmn0f}^6j^d_s#7RS-LGP)pzdk>P|Mqu!oWDsN$TkSjy3Zs*hf|^6}!!X z$T+G>uO~L+V)~_Fyr&7@UgxPgIXFr_t79uEeD@;->|L4CFXM?ztU7J4Dw9v%IJm2f z9eNY$U@Zzu-axJn@f4jRUTV*e#Jc;W|lRhfvyKz4xJoZ92&emo>QGST}k zCfhjAY}i@;yyVy{@zXrnNi&zj%!AQ3L$4}3tF~P<&r}Su0@)SeFv}`}cX7LEp0C96 z?4HQeH83Dw)jWH|^6VMQGuXD)0?Z0=(g~Vy5ry0fi8$)*^x3jELTw{E78j(SyB1|1 z#Ak8`t>*0R5!shH=1eSMAThCbaA9%c;<7B9*jGTq>F6zVPVB<7X=0(PvdhHYp3aFp z3T|S!3_lTaqGLfO@_@T;UNcsFO?7wmm_tLVG70g(lzm%lpp`DF-;bH5|9_(1oI)p) zDRDS{d^aD56YY9Egi;tG1$& z+D2-d>{(FqWg4#P8sbhn?6~8E+zfD*n~d$hI7zPDTWDJwj~SezgY)k?1`1f;JqX#` zWjaMQb%bd$0~ZQy>lT-E^$vCynH6^oc9$H?>}?~<*e{IfFY0n8E(C3B4=i24^H-`J z<~Q4iP~LEJV4=)nWF6{U(&1{PDu+^%%!b?CW=?@=TPv)&R^&~#>)ihFx|BJ<>48wG z^2uB(8;+kQE;2v)_12iF7aCn7)Agk zM^n%Su>?Nk*pQ_iDaE#RsT)jUBXyUlrDIfL2XP~8f*gxjp{+^t;tU)ZSkUzD;`{2ug?PJk!*zs9QL}_4lKF^xq_$laFi0{B(^vAP#6Vk9U!jU-z)F-J zrYn9mj1h6Rfrkq-aW3HM81Wanxt_leR|4sU+g#c>C`|}Ty9T9wg3{EWG%F|_5tKTE z(!!wR2c;k=)q>KwLFtm9baha=At>DzljgVM7>>D8e0c2N2#D19F294RQR z9q98j(6>i{4rBs-4ISuS!NA)OeO{oClR#G&1DzQJIvNW+_K1NFHh1g^pUEz^*UTb=y1=2e&w(XH+{gQ6bEFmzlY7HNd}N1*|4R8OZfSM zsDLWm&UtU(7@IMe_XBy(477>!2U9J&s>ks ztK1ul&Hmjv{!XHIDGzb#Vvmv=95AOj^~|?FjjB>uXVG6=EcWMSEj`rVMUQZpwR8?T zpVp@Nc&6qKg-m1kq5jmto?KH?)8s>DPMJ2f-t`u9TIfQqiVTS9 zd87k{t|FQ0F87ZM+2FZYePth^_+KRfX7753XyO*{$HQ5R%SCvW=o7CIe%twsnpc`bB zaW*Wjw0d~^+w3AZ)UC&F?&z40Us!f@XP+rJaraL;6 z#O+77j^5s4CgeDrD)d&H^ApVdr6INpQnL3^Av&&syk5JsP6MnVKVr2eU0?PDLjL zrp;WKGLQJM+(#|sy@wkvD%QfhYldui;}+N-m2x$rIxcTps3t8o2MOb%=|FmT@GE;> z*N==h*UbbtH~LAIDXevUW^XXbO<(eP7e+N z?!}AGB}=0e^m4p8(cxgH49*vmPuV*#R1q{$c@(n zkjLj>jE?EqIikZXWLZ)>i(S}f-FAd7n( z%aX9^v@*RppW7=OB()}z_Bqzv+m4sMg@;ZW7-%*Z(WJ+zX?Pa)=aj~%%`XPy6lM5o z)_i+wo}(-%b`N(KdksEJQ+|@SWlCab0}lLd)+*edAyb>VeR4a*Y%#@5)wrr)`Kzvb zTx?|G2Q^qrwuQ8nzgTnI?jQHl|3UNV&*_TRax@^#$H}z);cL{4Xc%r17~mQc+m~w$ zhvN{aoMzc+{~Y?Fo%fIBb(#LJH#s$cO+DG*9_33-OJIMk&B5Igrn%vs&|z9D?-TrTQGYCo zUrS-OgFXx;Mwh{kC~BOh)6cP!Qppr-d#rj%%}2H81MxB3ZEjnYZC3^nZd(Ptnk+(S z`lRV|&cOUoZ&O(urY&Jw=k)n!U@pqmBTK`i93Lj1z~oJ*Z+}MH zI;kO~1YxEVnQ5)*>mpNXlqe^KnNDV=b*FD~#$X_)Aly7}RZdk|h*J^8VmDV9jd@?! z3xj7=+t$IdN=N@-UkBfSVz@4+At{j4DWJ6R^p#2z@V6`e_QBs&{LRAO5%}xG-$MNP z_zUn?!{531y99q%}b!MfGJeO-N>N@&r+m8sT*Ook-T&}mc8{mi9ZX`6$uO!YA zmzyl;X8Nk^$3$@L+S!L;SnAww`H4l{5~6-e6c_l-6G3jZIc}q`)p0v>&=N1%@Nabr$yZrNBx2*uF{1ATDjZiyNCH)GryPl*kD~zF>%I< z8D=h&FTb>y`{*k-zaoIA*J1%8Jf~RhxA}feU(5Oc^YM*OvaFrqxx(@ri+K=!SlWjO zi&qxl`ZCJ4!{1ua!}QglM~L9dLstT)kT;z?YV-dt%>R4l=j%|H&r|}B*L}=pejI*i z(G$$i6Ucct@P&)~!Df4sz83b6%u`>n1$QLvQx@?w{7~?p2;!@bq#@PRd9lw}?6Yy~ zbHv7P?5^ne)<0Xo^YoRX7nr-gE@FqOzGyKo!4D05nXvkM&Y5d#U?7cYb??b=_^yJC+E$o9h>>q^1@1?}rkbGz{AJNy6eM~@ovtdKiN6bGh;uH8` znLZ_mzZQ(@K@Y?Em(Bcd`pWTtn6tj;DDY%YmH(NAd=5X9{{>NOTILwF#0iT3wSX__ ztB(IMH@iw_jk%k8;-?l4!w`&5@Ri_)YF0)>Psno(z%7@_DmMG7^p&U8n3wNalS<;f zvX*DKMUH?UDq5Xj-tSJ4cF)>5jI{Zi=&PnRm^t44c*(Qx*0hMVLd4oc)Z4}t0}vgZ zM7NGbt_wf3Z9RfBo3_6C?=|PRt#6?l#GxA!8t(x(xU%Q8ZDau()7Nrs!rZw!2EnJ^ zLwuENPfy#_VmE^y+Bu4FzOPPJfEhk*ZV_7$;hrm7I)r0gceb0}U$(M{t?8?MIcDcm zC2^+LHa6dA`r6#aFkifWI0cLiYQ|c`w)9o8R#?E)WDmC}ULuBClSQ%kc-ou!2(zl4`UrE^P{l&&t_pu_RfJ*5YA z{!n_h^lItt(nqDwb@EYKTPGem?a)bwPC0bKQF>fw8v1faXBj%j&>4o#FLbo8Qw)7a zq|*zXTFDE*Km>$W{C4N zJyYGV*32kpYkFe1@~0h&>Q&+oH8~BL2JEHNI&CTSoRo7S3$;f{$tkXm5hX7*O&qoQ1n^fAkN$C-I6w z_OaM~iQR|TuMwNe!cKD^G9z%31@1@SR01axxK$RAD{4HX0kHio>;S@M5jKUetqsf- zF!f^`;HFyKfy8wZH;uT>leoFzr?#l;Ad8t!%tB&jWMIh1;kt59%)u5jlNg_vLx|Zn zt5{PLpSB`;mPH>*bU^fMqAi=Qbg5lo1kSO*xdhe-Y$GsJZR)oPKp$q&hZB7+(Olt4 zlbZZxDzzSAaYqt&32{deXB&<)H)HP?ut!^LJFzzqdknF*B9o2I?RTZl4vRaMxO<2z z5SQ8DX1MSE5ZGyfT?9T(U^hb82w|!woYOIHks{YK39%PyG9TC3c`l%gXDOq^oVL-r z0v{ItQVqrRAQNvekFd81TS(Y8S&hQH26nesuSNC|`4N%*M5dZM8j=nJ+vNz%H!bE{#PFF(%sIp~vR7M}{gE65oNEP~M*&j{D>kr z`k{#HDZ-8{!uj7seaO2GrSim+A~S6iW_T?y=B&3FWH;s{AFPmalZ@Z+=}B zbwu}B0l%UEjyNgcehL_yRdC)KdULzB@@tEKfOx*9ApSSRTicUezy5rIXBnW_4_fF$ z@bPNH?#soYp1RqyD!;|`KpuwMTzarw9$~=aGO0HnL62*Q9#@Dxu8nzISn;@m<8cw+ z+Xh*RI@E5LM-|D%TW@nB`L?21+O=JNrwLq`_eL^dEFCWXd3yO^1j1H&6#XOJe!yexs zcwA!ixVB#$Y#WI_ZlHtHet8U8gSJ&ceN`T3Se}53S#WxrU*}+NZ_)4I#-1km1M*-` z@RJJoqXI^m&zU`?k&Kg#pGHDjCWUqMz?dfa6OtkE8F+y_t3s^A=QNU*nD?k0ACQ(m z!*^Sku{+Rwew$m(3SB;SoJv94Uz0qKe9-U$yg*)r8=l7el1Az^&=_iX8GhRI@X+)o zc?HQ3{3^UaUV|G7ew`6~>bt+IR2FVE!bbVwKn_X|mKpF3YofOioOQChmi&~HDTzQqA}F)8U6Zi znak(6j;i_s9*)6>%c{aR%srZ+Rpo{CZ#%P!i#hQ%OFrSSz*;d1RMAl?G4f9D^ zlW`j61G6U08s>AeCV36>@mZ7c8s<~9CfjS657U}V&@i8=HEBV(*fzY>(~b48v)b0Q zBM03h(Jrk>ZB{n^Z$I4eV}rsHtjqbO=_|AMX4^sQERAm)VXI#z;*x1_=6)`g#@FBD z`?$up19AtlBMVTnjbtZ{q#GY@ft``y?(tm|w`&}?TP7~=Q3dayufTV=`0fbfn0-Z9 zwR>nJtv2sb6XAQpcenUn2nVvaA}o9#MjG7W`=ncP@)1a5s9|6Fu@5qF;3;3jMcmkv z;03ZDT;AAIKs{~9zOf5gj8AopPgY6$8%gGNKR_etqQjyoNN|i#RosDb+_X#_KH7-u zEYLyl9plpx4rGQREM*64B&{~@Q4`@a;XB3;K{$|Eim>oQ8EL@y)S(%tG1M@dzWEZQ zU5b@CxCFlE!V9DgF8MwLeA9vz;Cq(i`!JPsxRGS|=F9HPr`QolaC{%BxTE5@qcd@N zk1AN4cKD9(V-UvY92H>+J60q0d{YzQ1^ABdPJ{#LQiO$fGtz+XSwr(pW2m7>-+Z7Z zxsomOkpkKmz{6)A;ga@wpq+-S0PROO+Iv)zXCxWg7iuJ3ewfvZ1V?+H;`-ybawabC zQ3Xpi0N>GGK{yamgypTOk+j;pM@@wL@Ez@g2;;+#im>p-j5MJAh@okxG1RbxzWIvH zY`in=&P{Ebmczy|zw9r?PKpJx6dAz&aqt2;9xmB0fPLDt0_=A>_D@hrfstg`KT#uT z8erW?NO0_*thiI+xKlH6d5S_+ z5f0?Lim>o27->NH!l5arG1Txq`iAo8EtXt~tDyWUc!69Emy}mQIgMBW%6&)qH7e=* zMv|fY2O3G29VYz{36An>6?a`6_oGZ)-lGbZ=z92$@*5BiK^!x_InY=M<*3wdAm;| zX|;Kenh5_Dd`J8J2;-xYim>np7->LzFf{Enh8liD-+UD{_4ZUAL=t#^2p+yZ375Q| z4&G_W3h-WYyg#gx9x;*(?~iIEO#tlr9TFVxzgOI2aoppXxV%RdEY=h79q)fYIFKh5 zVTt>rM$&5Y9yJmE6nw|~(+CIhCq-EJGmJFgy*4!OG=>_UrEi*h^0l)(hZNBMXLx}; z50|u`1KMfG3ebM8qx}Vy^rDeuXn#o~>GH#@myzITe?@Vx#&NG@;;;yZ>nzpl@Ez@M zARNeF6k&OLQzL1$d5@Y1|0{e)`&$SH@;60T_}h#$p#9vTX{RyN@D6=Ld-AQlyo(gj z{vNzQ{tlP4Ukuu5$O_PYiKG2}mGps;WN80~M$+YnSsx<7(f*O*K91x5nTf+EDsi2q z`UJkC{ZoVk`IjOrZ~xXvT5aB=Cc^&%-_iaV!hw9Q2n+v$kp{G1GBoWph8q4$-+b^i z{ua%=Taho33hMs{FOXr_)K2QJ0`)Xz1*pH;QNI#o1hTS`WT;<7BWVs`+Nwx!)UT$v z;c?uEOkCch3YKhj_>TIK2;&2mim=?Rp^>!OyhlxhuL<8#zZSxQtgQ$OUx$$f)L%U` z^)!YW)}?PgDZan?_9O=E=2v0npnygAxm{V-LpIdG`ta~=OSr6q>rn@^Y6a@x23H3g zs-%sKBvS_)Ya~quY}^D1t`0U;+-7mys7zemqY4&rbNH?fwm>+LEfrx&+)5*9wRw-4 z2;UmMtAiZE_^_oSEPOO04eH>Aq3eLgP{SDd=3|v^$BvA}H4wfnyg;^tOTupj;WS|d z2*1q{K29Yy8%c)nyhhR`he6|!;0WJdaTDUWmP}mUqY9R%mFbT0iHP6;B!yVYcGO5( zZ{DLS;&+1Y7~dJ;Kz31th40Ep1IBL~nsFLK4ZG1d--kUAzc0nlhVk>^T=)u{4w5JG%En zIFQMTuzc;Wk$UyiMEC*l9og-Hf|aG(>ki0M&EqZ zd~)h%?Q#&(!2NW1SkQwD?sap1Iwz`w=4Mw%v`FcQJy-?KGy+Y<9HNnWW*o7zknV^* zRN=GZ@Hq|O)WlNGh3|-MLpYGb6k&-yoRI{vtz+!>f6)iGn4i5@TX+lvB0NB(rGWp@ zuWXl>b(&6l_=7O8-hwUSvA*8wF~^V?Jvam)7traQ+kalrW*S+lCDOSxgQDcb}cJzAdahK z;_@CR4w7F#(U7g1#=zzZaR%V*7RPz$tSS+&4!&c4@v&@q0ZN;=6%GK`Wdidab+AOIGu?4sLnM$h6ki_rv5Fm5iHUVmKE}cS^2xe;;`R`2m%>SHVBj zNPV5S)_pC~UE{t^;XjJQuWtaSCN{kr;Je0sBf^2)qzK!%Z)POXxHlMsU$Nn6|0*8z z-7P*(2aBE?%V_Vkk4k?`-^`__8DV;)j}az6K`PqxE${;QDO^4u_{+WeylCh&I=i7q z!>i~yfZt}>?M%0-_}h$l)BJDONSbQc`!giCD!M~)KabUu#NP|w)zL2z4&**XSop6PNz_r)=|4Vn2Sj72;ePsK^=E&x+_yo- zRdWZLbN8mWGoIwv$cL(V0A3)!fy?gZ8Po#J%B}^+_k)Ug$Y2cLztu?NazD&;$NM9S ze>9H&T?0H-u~ff@?`VGv;Xoc&gyroCjnu2BCc^&!-_iag!h!rz5f=UwBMI8K8dJq# zExpAS?zU{P!{nCm$0yp)H`eXGM?{idcjyr;Lc37N?YUNMJh+26D9`;kh zi_=Zy+ei7#lRo<&y)FCyJl!b=K&ISzlN0i2ph z!mIFICA@}kAg?RJR>B*Mq$)vwIopEY&1nzS$%EThyb@?C_5X$b*m?X?hVJ8@ki#du zCTMr1N5aRd;Z0;kHT)G`AaB8CH7pPCZ`6_ZSiI$}h__YAJ4T7Ah<7#8xFX&|x~qu4 zEByU9{DTH?Y9bN;fbS~eLxcnQND;OoK4v6Q5o?Xn+n|c`mN55N-o{VMsP3Qi%??X# z_5893#KiPUQMG(Rq({E-$11QHh@!iH6b7HPV>TFOcpS{jb8m zjKlxe08ULT;;<1BqbnhdXeQ(W_Zi0f#~U+xlis{_XHWK z7Bm?P3`c8dBwa%&+?q&moz_~4TRVp8`36UU9t#N|DzV4=n^-O)Z4 zkwCUph~;fNjimKrxf$1q9|zyj-i&Y{c|};lp1$dorp1r*nSkqH zy#-z%t#HZu-@rP}$Y$N~J5ez^7>wa}M~yTt=T1y_JnyXdUE=s%8{nymrPvL=qxmZc z2eP{&EKgt6NWFS$B76_{j^;fP4rDJySoq$IBxqiL%;HkPZ|SM(ij)3CBYYezgLs-p zo%_%?Gls(^i9*@a(4|CmWHbcJ|ju zJw1-y1CZ|6oucrmarl7^;MByDPJ{2*JqY1IrYpj-JA;t~yXzQsalUd3o($dP<)xOk zQQ^V#&GreU3xas|b9Z@PnTaIudI&shJcUbMc?eM|BS@vG%S-B^DsQ%tXGoo+k;bIX zMYd;fFVXQxnTL556Py2!sPUQW2KaqZmn$I&w^L3BF$tpW7~@rP3;@I-0(D zQFee8A?>&Z9*=>C{itxsBag^(IY1!IT3!N=RY3(K$Pn16k;VjeA>9$!t?*(TKEDB+ znpmm@@Ew6AgaheOgeA~pBthUNV{i@(S|ip8bi9I}Xi2ffU+&gP8>x69{ju}-=^VZV zqd>M!tdUV5mdRRMTcsD7z;7SCK>Fd5-ytC&oW)oc;ma7ByzM&ivT7bMnoWgNG?K0m zlt_@^I`69D{5Wnf6NeRGTqhxm;Jb=kj4)P!6=7>)sYcRj^By%3ejI#Pk;fw($O($D z@PLs-MMguDw}*a4(imzuk-ph@7@c<@C*dlp=wx^}XcR80sPrMKfkrH=8e-#;j~wNv zs-&+QNrv*%G?FemOgbG2j`Et~&WPjA%*0^<7}r^%v*0_*&qf#vz>2V(eN!W8wRw-4 z2>%v*NBKDj2Xd|=Ec`r18c_bx(3H~{YB-;MxbZM{T!dVJ6wrPlyg)93OWHpJ?KETs zX#d>N{%w`?9V5xmez8W<<%e09Ai>dosp2k+<1Wv{!9HeAdEPtf(t~c@w^*3lF%>hii5ebg^n-q6*9QWf)T;8J! zmh30+9rd>$jCEK=Snh7sNLp>)qb9;{gYT%n9pONJrU(nagOP0N&B-J~?zW{pRPuBB zW9MN(e;5uckvkCu>36{k!ey>K-MTa@RM1o`d zKE?eij=Mh-m-ncGrTI0}9pw)og7sB}Sk4~QNLp{+qblMbg6}B*EyDOIy&^395k|5p z$FVL$@=ar?;ZgeL7Zlu3&XYZP!hARkm){`+YT)f7HS5HlZ{~5lc{CR`} zc|j2t{vsm@%C{L)mIe5T*l~LPcuRSH%YY@arK`74t;SxYETc|nElqfd{#ZSz@Q|2j zew84sqsd0Jyo|i4o>$-n@+w?Z&vGHrYq5CQ)mJ}ddQBC)ZWNe`d_yDkOuCBr3({Rh zys7ZN#^G-@fKwA$_#1pz5pN?L$UBO#74a@3iHgV=-7kZ1T18dw(NB%;|Bh=Q_I-GP zd;pikrbhQPYgq&iJ=ioy=-H0xAFAs^qswsokw(%S!{m>V;BJzCD(;gw?$bni$OYUbHNvq9!)I|8_@Lk<}fiQj+uLukOl95E+gyT(fwA7GY6YZgr z|Is(K;!c~AVWSonI3 zG+IF;sl&Qt10)qb9<)f$u6{ zG{S+5QG|t$Wu!p`=)q<~Rsijxl5Odm*;aJonQVuv;C>vuK$_u_`-z5onz91i>)D*P z@#j_Ecq7knzr9A%B*3r>2fRS`giG#sH{8>d z72uv{&AE2Jm&)7Q$TQsUqmeWTFl=8WIPSluxJhx`ewjG@oF3O%uF3En_xmFp$N`G5 zw}cibO{a3IqZVc`cc(tvwCTyIG3X%Cf5ryovvb?#+n1t5%>Ic@V0{(08u+bDQ`9Iq5Pjgm)e;&Q*8vdCo z?<^zF@PD>O(nP?xZy>?(|4qexD~>xS6NjJO<2uWBE_}!Tc?bt`z9KAv7ic7{Ht$gr z;TOVp{9lA{Am3Jmg@1>U2K?(;qeJpfd#L1M`gYvIuT$tzoN@`Sg8NJ11#%f&a$hvu z)07q9Ue6V`ZU1tW_gy2;aDRnH(j>sJ?;*i)f2HECisP=%#Nqe%xXyB21K)A~eT1#+#sQg%u<+LzX~4W5wLB#Aw1-OGpl^Pj5}v*+f5Ami|0X<~kO`O6 zpJk}0B`ZL^ekQ^;`nOcx-;6v%{o5Kza{#;EL4u?HUB$f@$NfDMm-ncG#d@FVj{6S~ z3FIFNvE+TIk+fc%n2GDee+1ug|1rWiF;fv1{s|)uxMzun9`n;4D*2SY`5pFb{ZJ2= zu=OO0yef5XFKAzaqR1*SG=TXt=@_Wqf|J~uE!`WG5$T=@So z-I4#L;{O-N55sre>dzX(Z}X9fYO%N2<#Qt~f6mfZkcD`!wE{Ij zV`;&r^vy3Q%t`0DIZm=}7C+cXmd%h46)*}O_GiLn1#ma*P$=-vQY|n4Td43Yjc~*N zRvKx{|JF!%{O1(DO&mVD0i2px`Z4ew|6>ux#!W?7{eYcf%eh;>v7kLZFp16*h+6x|z7lq5G$K~efL1UIx zBeC~3`gte&EZ;}v?Q7&2_P?f)Gz~Cq5)vHy`zda69JhZaF7HtVOLhQ!$Nm(AaaMsM zEPV%RB&{~@Q4`_Q;5+sYLKquB6=C5s7-_)1el}uA_Gu5598BN5w{hQood{+Yl2nSM8goO(u4QfC? zU@~M4&>kwO(vKZAtXsEyTmF|!hu|<2ut8a8cD0geoR~^ z{M+yy|KCA4kc$;z;g>MdfPei4&yf7n9xAz%zUh3-u+vQEwDY+PX{dqA;o(qHxU7MP zO%2eX6{rDz(Z}5ZSE#)28F{7#uGC1H3Yd2l5?l>jt+;FAxbJ7;@*Y*NbU%RaYT$K)L zj1=(yV|X~B6fXIH((q4nR)Bx~M5Vn0Zc%wZHS!Gqw`wF!1dO{436B5U75B3^?v6}c z-lGbZ?dR|v|92uB$X$xC1pY!JX|;Kenh3uezT^KMgaf%(5f=VSMjG(1Uw|5tf7(MO z_t7^?*wLq<^M+&`$1Gzl>5AtX5N zf2+8M}P54~bUd#L11`eXG=2e(%2kePo_ z)J*+4S4}=dWTt+kt0o_5nBVHE$;TSzC%bC$PYv_ST{Zbc!~B3(O+M8yzvESte`%PX z^Qy_eHO#Mi)#N`K=EuEi@|lMDjjx(~u3>)at0rG)m|y&=$$vG>4}aC+418K)3SU$aKidh;Gt5ub}Zv!>-{ zho5Hz3n}1W5qL%iBlr zQ=N{#y#WnmA8K)Z#J(Cy^M@+@8WLPHous(^;<(9~xV%RdYytbjcdhgQgaetP2;0o2 zY9y^T?@<%s2f}x)bQ;2e9Ha;fpUy~wR;u479kP$0JybG-zM0MN7?#*4J`P3-n(0h< zfgA#t&2$7m9GdrNPIfcR>?ZiVQ^)@-m3OF-XWH*TX12nTY6A}oPNY9y^T?@<%sN5OaeAB}Jz?TWDQV;E_`zkc9#Nd9RLm2}WI zn?l?GF8<|LITq=tg#x@lI^nVw)-|<2qgJ36){E6bm&)rl@=PrhHIk+SCeBBKtAz!M zE5&g=nYg@16)d3#-_^oGgaheSgr%`hBWbmHkD3VYhwo~kjBp?Wim>nsBMoYS1s=Mu zrae?5^v#yo{R-8duADsq%$9R1%?+&OXFxtX}UM-?pEdGHT(f@6P1Nn|3Ec{|d8qlxbdLEK~+CwFm&^KG7+~=R=Qd|S`m%$6QLaQ(f8sco$5fi%2jrUd zx0Z>YuI9bRSfO-$xr#D8h34^s*nMKYIiG6|f9u*B7l=K0rE+)P8=wq2zFZy2*d8)) zPIa-rn~%n2OL>S===gF?BxPGj=_(H_T{eCKhDRP9U%nsVX$9V}O2IG7l>TK)8BZy6 zeE9*S6iXBE`6i09%xnXByt`N?E)`_6x*ny_@#Wgcmhr%5vzn0TX#I?Y(DCKENC>N@ zT2%eX(qziW(-1no{3w#qY-Hd-zvU2d4b7nA%k_~69@z^s=9CKE<;BY<=n)E{Y z#7~o#_Q5aqDb%vN6{JqE=$BQ6hX(ApGG2hemsnrQ7$hW9MF4yS#mC*96G+-8p&}jZOOsq?q%|^ zE~}P~FSkKNu{5<^Zl^5UI=EZNeyTJ3wtJH{(DCJGkqzuZvfG9e=32%@#MANRjtI|| z(N*f{?Vc-z{_6a)^d-4SOF4;B==k#UNXm{*3ch)~UwIIx|MoAKjU8A+%UYY1(DCKY zNET}com$`W%h`-_==gG1BxgM2EG#aaCBfJJz;3~kI!EEpYMeh~>_755berr=8m zWMXI$t+a!VFLy^G$S_3A=_>XY=a!c(WjLkK@#UUK%63M|@(5Uq0_ga1ZzRB)LH%>f zYRu4uIiGyc@#UA1l(8_Q+EwT+?w5LKrQ1n2;dFetFM>}Xyy#D=4xn+&D0eL^b`NdE zeH25-mtRF<*ytfb^^x@gL&p_;P=QXD{Rz1U=%i8|fIbNynF8M-phkB2c+( z{86-kjxP@YUo6eSUU~|$_x%`1N`5jn`}V(sa_IQ-U}Ogg&A$CpGF>n5G{w;I>c=PedY+AP=Cv<#yA~L0MC8uOMbDp3%bbR?kBxYwA(Oq1GQ{Ef&^$nzb zL}_$Ha|a=r8{{oH-(f!sjg4=Iq2FMo^#a?k~V_6|wRQ4k$po{9wV zVPplFYF{o{_fQrcU!IO+O@u5R^2{jqFYrstCFuc5qT|b-B1tZid3dPXz<=IFKd7_vVYYkhmiPG`qImjxO0=!zKPXLei9 zQ4k$po{x;;(?7clNegw2{Sy|BjxR5$LA7>yF=o&hNKAgoGTWZ}DTj_PFGcn=zV}ix zojI3N3>{xyj>NEW_m%OzD$FScQqQagf?lT}I=;LT3EBmMq*&;6_g(!1sDZfaDUOaW zuSVk7D)A(pSzN3FQ?0UrsCOxfjxVo8qG%bL)8rB#%iS46t_I?MM{#s~dA(koKIE;; zxZ_Ropt0ER@g1t-1rmAeOfV_zV| zo<|x%$CrObMz9)txO&kLH-|VnzI+nlI0EZis4tltB5x;>jxV1^NNVg`XqR6yExm#` zI==iX!WoVF9fyV*HzZGVeED~T%t2$Rx@c%)`>2$TFaJqG@uRt+p>GjN$Cu9{=xAu| zD)*LU?jVjPZfNpv2&Uu9=Mk7DD|n7+U70*RNhlp(zKEb>04>bNk2H$>3`1kZGl{0- z%YP#@6<2$vV8Ni!xGCLAC>>wEjG(NbYLBiXH!ST3MAGr)e-V;*akb}wMTOpmz*`8U z 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 IpKq=EABBxf{Qv*} diff --git a/documentation/build/doctrees/graphicsItems/index.doctree b/documentation/build/doctrees/graphicsItems/index.doctree deleted file mode 100644 index ce964372e07ccdc124c94c7e289cb261eb6a2963..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4809 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 bCi<;b(?9C;Pc~{Z25%<*)un%DjoJSIS$77U diff --git a/documentation/build/doctrees/graphicswindow.doctree b/documentation/build/doctrees/graphicswindow.doctree deleted file mode 100644 index 24ef9fc734c2fc6fc99a89f8c9dd9bb3de18b0de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3501 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 LrcdE-+OB^Y2)B-A diff --git a/documentation/build/doctrees/images.doctree b/documentation/build/doctrees/images.doctree deleted file mode 100644 index 8f127e33bd00e7463177e169e98899dbc4bcd1fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11808 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 diff --git a/documentation/build/doctrees/index.doctree b/documentation/build/doctrees/index.doctree deleted file mode 100644 index e1879d0ce48a54022363186d745193dbdb778e42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6042 zcmds5X_OpQ6;39ZttYc&NPv*7NPsjUJ)KDcgkj$TVX#Hag9d4Os=MCwE2g@t@2jfm z8MG7>Mh16r#SIr+P*HKk1rLt}*UOqcKfEO_~j13=i$4jzVz|yKL98tK2tC*D?aL;(%pmA!Mul zu;K@H07*QzZ1HiQx+3akeGOCkyjB+zzmwvz#OYHS^h=#Z=cy%-Q}tRvaKX5k*m8!~k5DvXw)wM7Qq z@&fkAJK+S+H$6F#ubZyzmmRZ`S5$ekN_+B-UCAqR=Udjv&$C=UJ}(bD@u?x{2XyH5 z*V17T9bRFbfmu^X&^p%X$0E}aEMo?Nv@2l%^Q@0(LzZ<}VMEdRw6VlGtB&dWc-~ZE zT@|xBA(e4Am)Hv1^2sRCk!&b|G8cF`K$)8-P!3S$TEgarb9-rv$=YN7TT675=rBXJ z(y?7mM~`;m@Wg@W7?`iCDs0Ds)oELave|`31mX_NcPz}lJ)+}acI{f1{%5YW14s>u zHR3R_jT`*MuK5rv@4?B zP(UbYwucontSmeL?q}+N$j}~We0)SFD2=<}I%j)s0N3f!^Sf&N1g&yO7FuZq;uH+npLREi2A5$;J%r!=dLzbbfp&*zbo2yJ5Y; zCAa8;Suw!cbu`c@%V>0c&vmBh!dbSm=6Dr))T}}m7{_35h0nv3(uU6$O7!S#p}0yX zoxwt}HDaJVKs+&_5Zv=w)Xj=&UPlw+lMmiJQ)428r9#)dz>6@QQsix2t z3T@Wz@Ja5Bw#BAS(;`9+;RTE9_#f?ID^-tkW>V)e$`&A{OscPS7fpY3_`RXx&6%lZHZh zhf$T>47g=frZaIU@KEM5FEpyA zYnYbV2vDnSU$Zn+P&@EFXIju_xg6I}B74!ww8zc%fq%W?(oRvSg~rQl{syico!-P{mk##i?C0tH&deVXb zR(ZNTO&!<36aN3KMAyPkpB&LsQa`;~J1FXy>OroH==$_SVoY&_vf<3UfT5>~rit*Q z8$_q6jJ7^C+H6*+qIzRQPg7o|IX` z*%3VlhY7{?My<`l0$2$hI9%DW;Y;*f$ahmj&r9`81YtdMsOb4h=@(>KN4!WcEG~yC z+n4Z{jj8G{S`>Kdyt0iRF+(rLp*KhL5@nQra2i2g85yf_S67%ZdMRYTDe>O!);@Zf z6657d7^juoUZIoQEfKv^nLx3*IWerR+^6C`Of|Y0=-b5MN$z2-COy480jr-yYFhVyPkDTN4A& z+p=t>(v0p9w-q}fDGtx6_qX#oox^~*RT<|U@HkbFQ^Dz-@UwSC^ltFg9m?%GyR)wJ zWO`4W=i^{fXx|ypdqF)1?Yal^qPrfW_Z>uAXo%mx1WYZ6KcFGLE20l7t<>H-=R*9U zCH9VPQd)diC=lBEd+X{V}1SxHqDY$7chI zPoz+MGAr&7_sj#~Q}aQ%TZ#Ya1cX!~`V2t0FQU(aqXyw#4T4x?`uijL9B6b06bG8m zO<5IVnT2-${1Ucn+5HRJ?hi!tMa4{YP4k%1m%#1*xk!9@32vjCl`3D+NIV$PSCuu? z#s^X)zP2P1s`ZP7UJ8eY{w*Bz$zRtvJRH$C;*){HH&YzGrEqv?9uD80kHdpX`0oIR zgYwXKfx`D9`abw+6dqoT0y;bOQv|Fhkf!Tnn5&~p|3QKEwpttnpHDxG=tryzkMUsl zlbapQbvLa-0x-`df+q&boZA8-1VtF{f&bOFfNQnM+J3#FdC#hN9!i<@k?e z8~(FwFttfpjnvR!!j~)X1tJ}SV?1f$5<_KaH1HTid=Sc@fl|yzEdE-7rg)>&d+{KB2F{8%no$Jn&_gX5Qyx=4q6Z zmdlsjdbJlF+8h}(ietMPkEUghdhT^hw-#cy!A6VARcft`yRuh3bX#aS(P7pbvJdY{ z0gXgi4+F9vYoh=Xs^ad()d0%@O_&j;gF>%n2Dw_1wX@ztzW6#q4k|j4E3ge*N>7$y zD3zCM<@i;gp?2_i2*}ug(%#2Lf%6?$L#|QF?G~?uHMv%CVgno#qjC+5Wxn7%kcVP@ zmTmOHpb-X-#Q!kGs6&Z{Ireb%(yOjkFn^Y(RItc(SOcp4R#*X0IHyH>%bnt6qO8SMyEfm3h9NA$wHkvn?l9!qXgudDf# zjS&Sg6XF6%TXC~u-Of7DgOA%aX=s)jb2#;+?c0GT<&k=IZPoG2i84ohcgh&J(PrZU zNN&N$8`wsuT5pB)K{qPvwp?xn(JWiR-AQ?rW)>^n@OfxK%eo_v*2@Fv5j;3@Yx0uE z==DKmJzVDLK|Qg`HqEmqp19Ecxf3$0iThMM%yHyn^=khD#N>7?v-U6;&+V1Rspm|> zN=4C*n3L_!1zF}4S-0Mj^I@(b_1<5$Y+v0u!q>GC^LlSLG73hKaeGScRAegT%u2aw zTY-?nSQ+hNee<1E?ox#9?7%*Vv|BxQCIhA1qu$v6%&n?sH1KzOdsH5Q-DgV@i@lN8m4`KDYDk0T(tO2B9!^Un(#dQqlq{MUAR17Z^Av4zK~jYOA@l~ ev~2itKS)dR0{k=8TjOFuj^ZVf7vevx4E+I3UQC5C(=|QZNc*TU}kJ{d-k4 zodHV;ih_uE;EBrNfd`&=;E4yGsOz5Yu6r+g!0x*1-tMlu`+cvfdb)c$yB|~{em?o9 zyX#lKihsTuo|zp&v$fY~G+iv5n_d~|8-l1DgiZ*8 zeXmrps)1S)tF`(lRtl_A!wLeUhR!2%YP6TJU@-`-M%E{%)`7kCvDyIk$Y=$AM5d+2 z^cDIblkJ6SYXo{pMI5Dev*TN*_$; zKdN_5vYHfmjtT5@>gd_)z;e;{8&=Vc=B;AOr}<*T@SLFJ8s#GQDjKFbv*V!G{5T_ET6X(=P`r6JlRo>7ClF{RH z>STM+i1b5U$FtNa)2lFbs%&Zk7!8@W<5mEqnw(RGd@oWEy9~` z;pqT-vwf6(ti44akOArp8*H4Jw~w2%kDs$on6pouv&ZKm8|&Lx)y7(9^)9u?+6(>P zwtW6)0ll+h^=vF|^IE4yWg{w!yr*JyPA2b^1Y6^^&+S@UJxAZbMlk6FlWp0l^9<0W z&bNmQCTw*^*mLtXvwK0TE@XDufUa3?Ko^0Ksf1};xV^Y*#gyCjq?g#+_BYdZu(u;t zmoj@i>jlDIFFG&}Lu@yvc7nRgVl~awwN|cJu`4rcwsAv~UXF>MUPX%ni!|tlMODMJ zG-xmxw5U~m(snI|w(n^n^VTGP!GR;vs*xw=X1Gqc$lj#kd+wq}mWmv*Dws;Myc$%= zBFC%A5`pQk8EQte={lwuLQv3lhVVD!GhpXjzrPSX|Fl9}PzUnH(EXB(bkHUtvJ63zxuo_*4)iCCv4z^`1 zEV43aeie3OY0!rq`c4g68bXa4T7JPXU{`1s``Qgf9T!`3U=6lYvnQcl+|yu#=`vI= zYBUqOOjf~j8K!GupMr<>YOp9zgOY`g8My{QRohk%f&!3oQlRbN%^jwHwPUrkDq;WN zy-aJN%}$&H69IcRjiym{J4i_OX$}5>N%dRaIa}9#J!mZ_w6%ug!9BQ|RRtnpLFr58WQz@5 z-Oq;BWHxjSZ0Mkn4z!SFVL8u_)eG2iSYW%CEa%!-UB{L)i4Ak7#GCUT6p1}Cl$cfP za>tAIPH5LSUd3<0k+V8oxqDX4^-BJQ{UkqzAm6>v0@LHkVN&^Wq zRu#O_SKv)oAYpZpqN@6+1QLjuC!KJz;;WiO5w<-nQN)IfNGFLR>am(>a8nwKM?x;N@6N3LsyCD#cM z8eR|gd_$~Wj3FYY6gz!QM=urg?)#FwKA1GBm(IeGuQ-_lP=ujLJeowJn=&%MQ@7L%NWLsFw=!b;F$e<+LAMz(snlZKs14H%R^kmP7`qvVleC!h+x#q z!S4-ez&zYuOWn+*zoHM)Ga03?w0VoVC04gGY&^|Pss4zRUe#yp^d=6rZ-az>QPjU0 zlDj=tugOTbMI^@&Ib(H4tX|7loer$FBfdkp=tnhMc0sZX7h8#?MNMo9vWrSAS7cZO zQ}wzfXl%jb^?i71s< z#r|EfdONd!F4)H*$)Yr{g#thsYQ?7EdH9C!TfjR6()hW3`yS}Z?yGkccg^fTEZW3& zsNT^F#5?;i(+1*h0pgxm-OFV0*to7Q&&lJ~^wz1iCLbU|`| ztlr0voD4{=aVj+n0R{)%I2z0|B!T7fK^RfKr~z3u9Hj&G{$2n+(1(dO03Q?p9*EV4 zmG`1&?&W^JuKT!|>bycrNx$Hajhi_~rPIe3*;$K!98q=l%F#oSrC< zE1GY2%_8~+hYv~Dy;`2NRa%-z5IAPY2a9oJ6O&~OBEzdCQ>yRw;`+Tl*tBu|HNo}! zvHEr9ipM_MiR*9l89RM6)PN3y?;~0iBAxGMLD(y*PAZ62$ zw-D!o^XH=Gqc3&lMS}5Mu2(?6*9YXbfPP;D^ut*F0rPU((~B4Nhv4N0oeJ|ueR!EZ zh85#h%5r91wtG{4zj6fWPf>z3M z`BRu9i;6M>fckN){*l??i;he8 zd(rVvVD5=j){>3==dRV$qf~qDe}TOJ?+O1E=>J=+{+-e1$$u>9+k6E1Nv!@O<2YMW zP96W5xA%YAC}w>9S6<@epT_FHnJ*K^6YTRG`XD(wrESNa#San?iEA~)T{ok0mR+4x zoJv8GGD{#$AePEz;51JazCb)OnsLJ86fbf|S-z;C0Y_v&kwfYph9s?z2m`620C^ks z4e;z9BJ&_}#H))}>PVypXyidX-MMSHy(N~}ul*#|4>?x+Paplr{2Hx*OxU&2 zO8kgv6+T&1<%6O+0HUHp82yd*Pdi*U9V#MNew$Q;_oe|Ju|H~yQKVq*#UNyv__=2%=X&eaxLDAw;aKbm`0`h z*zCRdJ}B&`xv*&+3vXkc?X=V1t`{LLllk;%>~A-4=WPcC{HtKiGzMY8U2nu!OoxeB z$Lgun*6^opd#D%|H^UK{Gj*hqmO{xhTTp@HiO?YL_ z?pDr$OS24g!)*g0!>7T?IaO_^W;Eb}aT$U8RItbw*1N1bmDjieU6387wm_Efpx0%8Ml~$Dw0oj%VQ0 z&e@Lz_7*8tp)E4$adV{MM^^zBJrgZPyM~6gO3%l+@aHr-4i6TNHgr6G*f}{qZDoM; zBl?oyROkfsjOj$~|2Pu>Yd#4Na@3-{yd0P3F(KoUv<{3;<`EllMSF%@Nov3fFlo!3u}S*(bsN|)1;F0)s0av?jxVDB{Dh|5*5 zsiJC#lc6Xc36M9zks&*?^05a6ap}HmBE7{`9Ay-sq7+$m!q@M5At}n z&@`3O#Fr;11VCscA&;Cy3mNjccDU7UDggBS_!8FoNY^TK4(4UUKbPm%hxazzsi29J zBs~WW`Vdl!C}hH!^*rvk{4VdrOgQKB`{Zut$&*LV#c05P0ls3o5TEHezs}4rspduU z{^I2Q?A?H357$1l9E5WS>VS!XppXkUaEKOviDz6XvUVB^26A;=dy%14(00oW2rDz+ zIiiNbZ%86f#voK&;BpSG%Sh@A;ZQ5dwPM9bR=}DxaR8og4^QvgITM&JQJ_me>k{}< zUP0T1{^g23o?*Fz2W|V6;O|n93c>BfS4@`)V`KG%={id*^U;a_(nq_?QfV4pSoBObOqY$>~4*$rWD1FC~O>`OX@18x9W#-rLKPH&YA63(hPcXzO3yD zuSA3FuDkFfrmOIoXcpFdo;>u5VWYjCUFdEem|U)393|y-*hIMi$HY-3$>M3ewM=PS zGkw}xpgkA{ICOl)bhRuX8M->J4=k|pTp^Gc`luOUclN?kqFZzgdd%p#iiKTUigk!D z&}Z~9zL2W=euyNERT3?y=i|-vN_{mGfJ#()0iI*J7N2>0(9YYd?H}1g_IdWOUBiBd z6e2jf4sY$vhScz`32`?N;FbnkBhBH(j6Tv{f@@sqg)(CapZX9Qc%gt^B#j0)t`8O) zW#27jqXOb2%^aEhazhu%GG9gBTKx8~o}s*72UL{~HSRDBhy{UwY-x4yDckGX3^Tsp zr#hN}Pn(yl*g2qi8RqgZ)IC!JKVtIm*%rPp56cv8KU%gW(Xx7?UjVboQ}br2WNPZ1 zI!LE(2M>}2-B@QTSP&t9Z<{T^dtD0ECGawzY1EP~ab(A7Ie?Q^Tf>uMB(!S*Uoo}t znR!X3VoAH~Ep@glG_dS=tALdEqGd)Oaw@3`E#et9$s?Vj3g~*Y&e09{)JHNYB=UJN z+G2|N&3c@-QIm>DE3t!sULwO@$}fiy3~&lHpd0bnQQ!gRifehVlDpb)BUIkgKD_to zCVa;9GVZI7W!hBYT5HB7pqJwfRDC1X^QnR&_$KB+JdEI%ZpOPAeJxc@3_wAAL@vDo zZTiu67&fPhMM+*394o9Ae5w`gu;CWzmE2<(T4P#L(tS&^!f4ZKE`^Q(-71rh2B;5j zmbf05@8;-L(mE3Qykth>Z9L-u-qRwzntzYDxO$6l$&7&&i*Dz(RhCDu;UB{sq{EII zO}Ycm%zy}~B(9HMi#7;mEm%eEzn8dz*Gb2mjWUk3+v~a8fIgDam(;1#8@Qi+#*BUp zMD9}wK>2(LTS^|0j;^@)c_+H*>zRQ71MTz{lHSP80~M%iT@w?z1Z0q-+(4^KUy51PJLb!v34Oer#lcB6FC32}tFPg*ybu5Zkj ztOmk}l2JkBKL0a--i4R9=*NQ9M%x)KsZlA3rQVHBx9W#j-U7WxCQBG^23Ax7mkpQR zE6wY<1_g>NNp=(&1a!Z&k1=}qGlMM7=3JBP9w^lE=IQ;?d6ey;?KT4XfV8gb zkr;gt&H6wTRwvJ<2l#g`orUiuh!W0Nz=CET)xHhml|(?j}bx02F>+;KqPvmGRTf`6~hHpfr$({)ef zUq$Fsyu@&l6lJM@8qYKOsA%YtJRXwPwXAe)m-87Oxg~WutyZg$lZ|-w<`#dw>`OQ4f%-%ie`H7e7AXF=H z;tSO=AntLk zt>~c2J4y!{R+HHAMOX23!80_Ux|tGAB$RS{Fjcp?LGxaoWpL-HB+op-vu$el!b>vO z?B@M#I^^a$=_ZMe6D=I2-AwabYY$BusK8e|U+@80qs5YF8AQv&L<@>$JQ;*OyzoJr z4~+(_glAke_&mgWxZp*^o4M;1&k>rS8o+_bN982ds{hX)HIs=n%j3XmE#Ww9VnqFe_rEPU)huw+VUb> zUTn)tY$xx>x-VM~{bYCkuW8aHMg1Mc#4q&ZwPEn0TuW=cu^` zKM@gJUGO#7LKA(?HZW(07V(n|?6nUA+a9n!dCW&1hC(}Gpn&`o__(g%r}llo*3+6) z>y#__`m=jbPRk~KsL+?g2ys+iZIpldnDWM?6KD}N_y*X$vEXMI_2*z^S0V9DhWh4l z>Z9^HWBHa*WV5@h(gr^hhEEmzETiL1K)JC>UVVEFw~X~?k6UlH_8IHXkykwoQXBbs zZo$tR*gaL|$A#YY?XdXyc}JSaP8a+F6WI(RLqV?Mpt^j6msC7@TZZw%i7z}trb#rVc%t7ZVZ0Nm}V2SQnJFfine5#Cwwsv)d_&~xK8S8-_Y zqH!-3UR>fO1*gVp2)sLo@pyb=vo&q-GT;rlQDD;r=L0LrFq5^${BoNMI=HL9l)GPH z^Idd!WVus5y4&V+RQCd1-p-0&Y4bhifYbg#BP(ynSD|lh6YjLUdu_hYV5LchtDIlm zrkTL6dfNTyX>-FYinnZj4b2Ul^#NX6@at$Hi2Wc#<8sgrdqOLIz0Gf+86~tv8&>?r zp(~-AC9xK<-?-Hcb98uqQ^Id9_$^I3G?2nE4FKS)ir)&1^I$s3eDv-V`$X&o=o4>i z)BddMVJVt;o`w8&IwrMFueB_lB$yupp}Q+d*0ZFJLW|!)btvEoW18OytMjQyO$-9f z)PuoywP{|VMWv3HL}uhr{B9&tC&tGB=RGuqC)p;ym*zq@?&TOAu-+H^KAJ>Fe?QfI z7cEY^QOX}Mh^4qrDAq*CA4H?tk1#Nf#QlNdLzpk58{*b27ks!iMROHI)ek??mR6gN zi!4hr2c(G)MVBfGD#C?<&45Gpo35OxV}DvG6hiYS zwrDBIb(-t{X7iJnEue;{`2ipyw{adh8>YhfQ&8`KpGT>)T~mrb4XtqH1#vg2mVL&E zyzXYP0fsH%M*P_}EgIdsL0D$ZK~n&)7W_Gy!Gq;XuRQ7V=jou>2rz`9taFSgr8ZxH zHZ?DayFrh?Smg>8w-%^CCmDYUmc1}>R~-?#LFl+XMyzt8`11XB!~_6H<% z(Rh`=2~1O?Lsj-5T#N|M57AVvyUW+`x2REVHwFD|><;4L6!CYcUankL5aGLY!0`j+ zb|7V?%lFz;#|cj1FxcSl<6^37x8tk_zLxxh7KYy4b;LiUDLQjAT>prs2Q%-F$^QS2 zia()xPs9QXF9?`pF|%Swqx#^IRs0W| T|4B7dD;Tl=Rq(&_&dPrPIG^{q diff --git a/documentation/build/doctrees/plotting.doctree b/documentation/build/doctrees/plotting.doctree deleted file mode 100644 index 5a3604feb0645eba883be551c3808b755a7e5d8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26167 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 DnwcKC diff --git a/documentation/build/doctrees/widgets/index.doctree b/documentation/build/doctrees/widgets/index.doctree deleted file mode 100644 index 4f113d6def817a500583698574297206c413200d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4236 zcmd^C=YJeW8CGPg+}X0_9%7qpnxjCZ3&D_JS|AVzf+Yq|a)fL)bGvglW9{wkyE7~4 zLN3@qj)>lS@4c7Md+)vXUcT`U@XX$xbf=&5kxzWl?{qiwmgjxm_kHHncn~H*Zd9Zf z;v^KhYX5DE*i`APOHR{xm$uBYO3d5BbW)*dYnLYKd&b7b(hiwc##4E*c2-o_5=PUc ztJyA{H?&a(Hj~&7MN`vMm!{c{lxJG_ZJ{-9!BEAcD%%m{Sq5jmPVy`ew9RGJPy|WF z?QPobvfVs4Ni#`IoEYJ2!!tuOi)XPZ8)tNyL#`QLDWloX#@HmnMu2ocjRvZY#^$4dJVZ`ACm8V~4bEmDn|g}$~Pdu@Pzk$sG zKA-Qg=>`v$GFzq#JhsImdZ9-b$yv@O`sa^$bg`V|IolekSkTzgsu=1!hOPbU%b5t;#$|DdGnu{&v&EGsmNqs6kwc;p=K=i-3-CA7|pRDpCr9w*nLnMY1D$(%+GGOD;` zzsnpI8<916AoPNBLJBPgjpPJ8c3L8~wxf>IC~Sl{@9?^ z`6ps7#d*6SG7-{=HFtXd2Noy1Xxu3uwYTT#-W8`kVVz*y+dha;G1z%f&p zXmyXCH0RcL1KxqDG)heWGu_yuC%g4sLqDoEw$ZW@D|AzjJh#4o=(ms?Zc{EV-`pdA z)N&w2uw-~65_C(CxLeGgf!pHy@@k_;!KiJ_m=Fy(2rTT87`+}OQIcVT8|=Ymk6Ld1 z&`@Z_h*E)$cqhpRMr4naTb~^|hB>{25ya_akCxo}zM-FNGh?ZX%w|YYkJ@hioT1;P z(k6kMcC>Ck?ol$D1qNX&!$uDkkCGOpJtDWhbI6KLRoqBcDeI9Q#jdF$QC>4W%H8_j zp=*Qmioz`SXl2yx$)p2`5iFJPtoEqm)(;HbZ|9LwLv_EkM^71bKUCM-dej}o216a@ zF?)GKM09(Po;vCu5~1KtS>01TIxX0wRAC66@w7FzUBy9^L;D~{O;7je8FTVRd6T?Z z-Xa?^luaq+Ng2slrZSU8F3VMUtGrF#E>8(I-R7&INR6aB7RPh+=C#Qp}4^C=&jz`aB zyGFEDar=25J)iB}blfj~dVxnTWR*anM^ypP=X5g$Jz^l9T8a4}2q%z1jAJVW8p|ADm zb!=J-W3bLNy?#iVG_zvjhSlqNltb&%8xneBm)3pw+mQzPChm*t4?t^69+~pM2Nx&UOc_L} z4nO3|n#&G}EK4#UktSBU`2Bw+WJiR;eVEPKv@nRYgu@7F(nr`tC>nW-KI*brf$?q& z+d(TxAq;(NjUC3!N^^5}Ha`xf4KxBncOfLy7CXXUwG5|E0KJ`I-cJ2>PHFliu)?HM zaWg58eaeEo!n4>03|oS?>C-OTXK}YpX{+WN(MG&R&}Y~*ewe@LvZEn=mhBR&3J(&{ zI)`8{!22AqnGBM+sao{;vQ}W+veL>V8GQkkgDBxkzG!n5`Ph7RD59`myptV9Chhf3 z+o-y)Q;XG?*4P#iFVmOZGOtvNJVYMth`wUsTTK(NtdNQ^k2HN1{bVtL)GStGneo@w z*tY(2!ME(CuOm#$(IJ+711>fJPv2w{xoOT{N#A1C^13DTw{cs=!zH5cuu5@qUIT>h zvYmdYwB3B5V1di`TvkB`r*NoN>H8Q=6>LBsBNCJB2a9+Bj`E0p$R^l{YvB4vY-;fS z`!VzWw-2YEuu5N7+q(S}Sr%N{K=)_p8%vV)*8Vv=w`}c|m6e6GV`LH+tsRQx8cymO z{em4D#cETF{8HY5gZ(BHktiks5H!*sJlK8< zs+WitMAq~>kL@ogtzx_>ibuDIZDB(CJ-T2Ub`|=p|HU8h#Y~a$`gr`Kn>X0L%>%yw z&i>QlSpP-%=Pvz4&dR+S)Ej=?_gBhayYx4C7^5GG?(cvhpe!$k diff --git a/documentation/build/doctrees/widgets/parametertree.doctree b/documentation/build/doctrees/widgets/parametertree.doctree deleted file mode 100644 index 56b8107738508625a28d18ead11cbd1317e7ffc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2912 zcmcgu`Fk8k5tZfW*tI3gj%){PCC+890ci{v2qAhd}kx9E>O2ZG-Jh!zvGc!{P?S7QnYALT}MO>wl=ZA*Qn<_Lmv2a@CK`cjx z7iwOlWxOyl$fPl13{O3u`?MTYS|eIuib_Y4&kU&_%c#)8z2>t+x*#fBjEchMg_VJ^ zLR((iJVgr*QxY3quKAprr_~dH);mBeh8820q%nf4PMP#eR zZU^TQ&Com?10i2lvozoR{?N1=ElXYHNdyY}{F*HZf;?4Z(o3st>FpMJ+sj0rm>?CQ z=d3&t@og_nLeG)!mHTYRT9nG0@ZS*Nweh^&Bpyp^yc0QYXk+=cFW==WHD3*B!HTit zgx6@H%ZeYSzOYs&VP(PiBQ;+e&|+Mb&d%_oJ}pG4Fb1C=^Jy^@(YAK=dfcbej@l=D zesZw&Fp_EMTt9@laRLcnS1Xd{TCX>J9;kUy(F19cOTM`|i;$hD_;qSlp$xAdOcMyf zq@eB%sKR>98v}Le{wnj-WwojvQdiU(%{34CmO^rG9H>Wq^_Z_7_tg`=y5Uy}(N!1o zP17Oo$UUh1&G(OU8{EF7=C=ZjaxvGdP*h=LN|oe$8zPR|j!E)Z?)CX?fc{j?Z-+I_ zvj3@hXeb}z+nW^7V-_$0xHvqG(Nv`0Hr1Q7#;QO)-~5`6M3BE z{o0_d%gz6xMq8Kf?}y)I`Q5^?b*f{l3MGdieEQUYn^Br261Zx3M z%`eglN|tC0i2#$w^&o%B=Pg<^(ppr=@TZSSd8CWn$~^ABDAEeEls{AOXKVi4fG$j^ z2wal^__kpmFdl^KLdO`BCEm$ALjC#kLprNR5lk^JE1hycSCqBo)1GIG0+TwCcC=CG zv8Qa7di(|Ifq_Um*DT<5xs;{LL1J)4a2O70*+5%m5EYpl?uH{2(iSep1WrtIPJsI? zX(<)?xWbf%eJpuIvzVS^>cs+M#EPtBX>v|;u?(v*7eiW+ zStwm$_fu(cmG`1)T`;~jNhD@@J$8yJmrSbU3boC@> zQ?2q7rD@rXwcv=SpVDj+^Un~!z@38?+m-X@2Xv)vMw!ZHB7jM&g2_4X3rN0Wz7e$H zU;1>Zk#>?B9DVM#lesHU%)df6oWap__`|R9!%~y+;h6l*unOto^q_Cgx!-QiwD-pE zYX1E|U7udm?d0wAAFyHnSo5EbZCK^j>U?X!e|CHIFU=8v%jK`?y1;(L(lywU_uO63 zUIa%wPOYfb3UWVwu(oULZ#Dm2tzq53<3Bo&&RKT=;`C_UQExKX4gb^Uf6=_tCr+n- K*ZiL<-1sj_1vL=> diff --git a/documentation/build/html/.buildinfo b/documentation/build/html/.buildinfo deleted file mode 100644 index 1fd6f9ff..00000000 --- a/documentation/build/html/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: ba0221080f2c4e6cd48402ae7f991103 -tags: fbb0d17656682115ca4d033fb2f83ba1 diff --git a/documentation/build/html/_images/plottingClasses.png b/documentation/build/html/_images/plottingClasses.png deleted file mode 100644 index 7c8325a5bbe0809aa3b40ac3a8f76972b4d802a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68667 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 diff --git a/documentation/build/html/_modules/index.html b/documentation/build/html/_modules/index.html deleted file mode 100644 index 4438a15a..00000000 --- a/documentation/build/html/_modules/index.html +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - Overview: module code — pyqtgraph v1.8 documentation - - - - - - - - - - - -
-
-
-
- -

All modules for which code is available

- - -
-
-
-
-
- - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/_modules/pyqtgraph.html b/documentation/build/html/_modules/pyqtgraph.html deleted file mode 100644 index 7779bd3a..00000000 --- a/documentation/build/html/_modules/pyqtgraph.html +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - - - - pyqtgraph — pyqtgraph v1.8 documentation - - - - - - - - - - - - -
-
-
-
- -

Source code for pyqtgraph

-# -*- coding: utf-8 -*-
-### import all the goodies and add some helper functions for easy CLI use
-
-## 'Qt' is a local module; it is intended mainly to cover up the differences
-## between PyQt4 and PySide.
-from Qt import QtGui 
-
-
-CONFIG_OPTIONS = {
-    'leftButtonPan': True
-}
-
-def setConfigOption(opt, value):
-    CONFIG_OPTIONS[opt] = value
-
-def getConfigOption(opt):
-    return CONFIG_OPTIONS[opt]
-
-## Import almost everything to make it available from a single namespace
-## don't import the more complex systems--canvas, parametertree, flowchart, dockarea
-## these must be imported separately.
-
-import os
-def importAll(path):
-    d = os.path.join(os.path.split(__file__)[0], path)
-    files = []
-    for f in os.listdir(d):
-        if os.path.isdir(os.path.join(d, f)):
-            files.append(f)
-        elif f[-3:] == '.py' and f != '__init__.py':
-            files.append(f[:-3])
-        
-    for modName in files:
-        mod = __import__(path+"."+modName, globals(), locals(), fromlist=['*'])
-        if hasattr(mod, '__all__'):
-            names = mod.__all__
-        else:
-            names = [n for n in dir(mod) if n[0] != '_']
-        for k in names:
-            if hasattr(mod, k):
-                globals()[k] = getattr(mod, k)
-
-importAll('graphicsItems')
-importAll('widgets')
-
-from imageview import *
-from WidgetGroup import *
-from Point import Point
-from Transform import Transform
-from functions import *
-from graphicsWindows import *
-from SignalProxy import *
-
-
-
-
-## Convenience functions for command-line use
-
-
-
-plots = []
-images = []
-QAPP = None
-
-
[docs]def plot(*args, **kargs): - """ - | Create and return a PlotWindow (this is just a window with PlotWidget inside), plot data in it. - | Accepts a *title* argument to set the title of the window. - | All other arguments are used to plot data. (see :func:`PlotItem.plot() <pyqtgraph.PlotItem.plot>`) - """ - mkQApp() - if 'title' in kargs: - w = PlotWindow(title=kargs['title']) - del kargs['title'] - else: - w = PlotWindow() - if len(args)+len(kargs) > 0: - w.plot(*args, **kargs) - plots.append(w) - w.show() - return w -
-
[docs]def image(*args, **kargs): - """ - | Create and return an ImageWindow (this is just a window with ImageView widget inside), show image data inside. - | Will show 2D or 3D image data. - | Accepts a *title* argument to set the title of the window. - | All other arguments are used to show data. (see :func:`ImageView.setImage() <pyqtgraph.ImageView.setImage>`) - """ - mkQApp() - w = ImageWindow(*args, **kargs) - images.append(w) - w.show() - return w
-show = image ## for backward compatibility - - -def mkQApp(): - if QtGui.QApplication.instance() is None: - global QAPP - QAPP = QtGui.QApplication([]) -
- -
-
-
-
-
- - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/_sources/apireference.txt b/documentation/build/html/_sources/apireference.txt deleted file mode 100644 index ab4ec666..00000000 --- a/documentation/build/html/_sources/apireference.txt +++ /dev/null @@ -1,11 +0,0 @@ -API Reference -============= - -Contents: - -.. toctree:: - :maxdepth: 2 - - functions - graphicsItems/index - widgets/index diff --git a/documentation/build/html/_sources/functions.txt b/documentation/build/html/_sources/functions.txt deleted file mode 100644 index 3d56a4d9..00000000 --- a/documentation/build/html/_sources/functions.txt +++ /dev/null @@ -1,53 +0,0 @@ -Pyqtgraph's Helper Functions -============================ - -Simple Data Display Functions ------------------------------ - -.. autofunction:: pyqtgraph.plot - -.. autofunction:: pyqtgraph.image - - - -Color, Pen, and Brush Functions -------------------------------- - -Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and fill shapes. These classes are highly capable but somewhat awkward to use. Pyqtgraph offers the functions :func:`~pyqtgraph.mkColor`, :func:`~pyqtgraph.mkPen`, and :func:`~pyqtgraph.mkBrush` to simplify the process of creating these classes. In most cases, however, it will be unnecessary to call these functions directly--any function or method that accepts *pen* or *brush* arguments will make use of these functions for you. For example, the following three lines all have the same effect:: - - pg.plot(xdata, ydata, pen='r') - pg.plot(xdata, ydata, pen=pg.mkPen('r')) - pg.plot(xdata, ydata, pen=QPen(QColor(255, 0, 0))) - - -.. autofunction:: pyqtgraph.mkColor - -.. autofunction:: pyqtgraph.mkPen - -.. autofunction:: pyqtgraph.mkBrush - -.. autofunction:: pyqtgraph.hsvColor - -.. autofunction:: pyqtgraph.intColor - -.. autofunction:: pyqtgraph.colorTuple - -.. autofunction:: pyqtgraph.colorStr - - -Data Slicing ------------- - -.. autofunction:: pyqtgraph.affineSlice - - - -SI Unit Conversion Functions ----------------------------- - -.. autofunction:: pyqtgraph.siFormat - -.. autofunction:: pyqtgraph.siScale - -.. autofunction:: pyqtgraph.siEval - diff --git a/documentation/build/html/_sources/graphicsItems/arrowitem.txt b/documentation/build/html/_sources/graphicsItems/arrowitem.txt deleted file mode 100644 index 250957a5..00000000 --- a/documentation/build/html/_sources/graphicsItems/arrowitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -ArrowItem -========= - -.. autoclass:: pyqtgraph.ArrowItem - :members: - - .. automethod:: pyqtgraph.ArrowItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/axisitem.txt b/documentation/build/html/_sources/graphicsItems/axisitem.txt deleted file mode 100644 index 8f76d130..00000000 --- a/documentation/build/html/_sources/graphicsItems/axisitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -AxisItem -======== - -.. autoclass:: pyqtgraph.AxisItem - :members: - - .. automethod:: pyqtgraph.AxisItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/buttonitem.txt b/documentation/build/html/_sources/graphicsItems/buttonitem.txt deleted file mode 100644 index 44469db6..00000000 --- a/documentation/build/html/_sources/graphicsItems/buttonitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -ButtonItem -========== - -.. autoclass:: pyqtgraph.ButtonItem - :members: - - .. automethod:: pyqtgraph.ButtonItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/curvearrow.txt b/documentation/build/html/_sources/graphicsItems/curvearrow.txt deleted file mode 100644 index 4c7f11ab..00000000 --- a/documentation/build/html/_sources/graphicsItems/curvearrow.txt +++ /dev/null @@ -1,8 +0,0 @@ -CurveArrow -========== - -.. autoclass:: pyqtgraph.CurveArrow - :members: - - .. automethod:: pyqtgraph.CurveArrow.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/curvepoint.txt b/documentation/build/html/_sources/graphicsItems/curvepoint.txt deleted file mode 100644 index f19791f7..00000000 --- a/documentation/build/html/_sources/graphicsItems/curvepoint.txt +++ /dev/null @@ -1,8 +0,0 @@ -CurvePoint -========== - -.. autoclass:: pyqtgraph.CurvePoint - :members: - - .. automethod:: pyqtgraph.CurvePoint.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/gradienteditoritem.txt b/documentation/build/html/_sources/graphicsItems/gradienteditoritem.txt deleted file mode 100644 index 02d40956..00000000 --- a/documentation/build/html/_sources/graphicsItems/gradienteditoritem.txt +++ /dev/null @@ -1,8 +0,0 @@ -GradientEditorItem -================== - -.. autoclass:: pyqtgraph.GradientEditorItem - :members: - - .. automethod:: pyqtgraph.GradientEditorItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/gradientlegend.txt b/documentation/build/html/_sources/graphicsItems/gradientlegend.txt deleted file mode 100644 index f47031c0..00000000 --- a/documentation/build/html/_sources/graphicsItems/gradientlegend.txt +++ /dev/null @@ -1,8 +0,0 @@ -GradientLegend -============== - -.. autoclass:: pyqtgraph.GradientLegend - :members: - - .. automethod:: pyqtgraph.GradientLegend.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/graphicslayout.txt b/documentation/build/html/_sources/graphicsItems/graphicslayout.txt deleted file mode 100644 index f45dfd87..00000000 --- a/documentation/build/html/_sources/graphicsItems/graphicslayout.txt +++ /dev/null @@ -1,8 +0,0 @@ -GraphicsLayout -============== - -.. autoclass:: pyqtgraph.GraphicsLayout - :members: - - .. automethod:: pyqtgraph.GraphicsLayout.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/graphicsobject.txt b/documentation/build/html/_sources/graphicsItems/graphicsobject.txt deleted file mode 100644 index 736d941e..00000000 --- a/documentation/build/html/_sources/graphicsItems/graphicsobject.txt +++ /dev/null @@ -1,8 +0,0 @@ -GraphicsObject -============== - -.. autoclass:: pyqtgraph.GraphicsObject - :members: - - .. automethod:: pyqtgraph.GraphicsObject.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/graphicswidget.txt b/documentation/build/html/_sources/graphicsItems/graphicswidget.txt deleted file mode 100644 index 7cf23bbe..00000000 --- a/documentation/build/html/_sources/graphicsItems/graphicswidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -GraphicsWidget -============== - -.. autoclass:: pyqtgraph.GraphicsWidget - :members: - - .. automethod:: pyqtgraph.GraphicsWidget.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/griditem.txt b/documentation/build/html/_sources/graphicsItems/griditem.txt deleted file mode 100644 index aa932766..00000000 --- a/documentation/build/html/_sources/graphicsItems/griditem.txt +++ /dev/null @@ -1,8 +0,0 @@ -GridItem -======== - -.. autoclass:: pyqtgraph.GridItem - :members: - - .. automethod:: pyqtgraph.GridItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/histogramlutitem.txt b/documentation/build/html/_sources/graphicsItems/histogramlutitem.txt deleted file mode 100644 index db0e18cb..00000000 --- a/documentation/build/html/_sources/graphicsItems/histogramlutitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -HistogramLUTItem -================ - -.. autoclass:: pyqtgraph.HistogramLUTItem - :members: - - .. automethod:: pyqtgraph.HistogramLUTItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/imageitem.txt b/documentation/build/html/_sources/graphicsItems/imageitem.txt deleted file mode 100644 index 49a981dc..00000000 --- a/documentation/build/html/_sources/graphicsItems/imageitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -ImageItem -========= - -.. autoclass:: pyqtgraph.ImageItem - :members: - - .. automethod:: pyqtgraph.ImageItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/index.txt b/documentation/build/html/_sources/graphicsItems/index.txt deleted file mode 100644 index 46f5a938..00000000 --- a/documentation/build/html/_sources/graphicsItems/index.txt +++ /dev/null @@ -1,37 +0,0 @@ -Pyqtgraph's Graphics Items -========================== - -Since pyqtgraph relies on Qt's GraphicsView framework, most of its graphics functionality is implemented as QGraphicsItem subclasses. This has two important consequences: 1) virtually anything you want to draw can be easily accomplished using the functionality provided by Qt. 2) Many of pyqtgraph's GraphicsItem classes can be used in any normal QGraphicsScene. - - -Contents: - -.. toctree:: - :maxdepth: 2 - - plotdataitem - plotcurveitem - scatterplotitem - plotitem - imageitem - viewbox - linearregionitem - infiniteline - roi - graphicslayout - axisitem - arrowitem - curvepoint - curvearrow - griditem - scalebar - labelitem - vtickgroup - gradienteditoritem - histogramlutitem - gradientlegend - buttonitem - graphicsobject - graphicswidget - uigraphicsitem - diff --git a/documentation/build/html/_sources/graphicsItems/infiniteline.txt b/documentation/build/html/_sources/graphicsItems/infiniteline.txt deleted file mode 100644 index e95987bc..00000000 --- a/documentation/build/html/_sources/graphicsItems/infiniteline.txt +++ /dev/null @@ -1,8 +0,0 @@ -InfiniteLine -============ - -.. autoclass:: pyqtgraph.InfiniteLine - :members: - - .. automethod:: pyqtgraph.InfiniteLine.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/labelitem.txt b/documentation/build/html/_sources/graphicsItems/labelitem.txt deleted file mode 100644 index ca420d76..00000000 --- a/documentation/build/html/_sources/graphicsItems/labelitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -LabelItem -========= - -.. autoclass:: pyqtgraph.LabelItem - :members: - - .. automethod:: pyqtgraph.LabelItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/linearregionitem.txt b/documentation/build/html/_sources/graphicsItems/linearregionitem.txt deleted file mode 100644 index 9bcb534c..00000000 --- a/documentation/build/html/_sources/graphicsItems/linearregionitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -LinearRegionItem -================ - -.. autoclass:: pyqtgraph.LinearRegionItem - :members: - - .. automethod:: pyqtgraph.LinearRegionItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/plotcurveitem.txt b/documentation/build/html/_sources/graphicsItems/plotcurveitem.txt deleted file mode 100644 index f0b2171d..00000000 --- a/documentation/build/html/_sources/graphicsItems/plotcurveitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -PlotCurveItem -============= - -.. autoclass:: pyqtgraph.PlotCurveItem - :members: - - .. automethod:: pyqtgraph.PlotCurveItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/plotdataitem.txt b/documentation/build/html/_sources/graphicsItems/plotdataitem.txt deleted file mode 100644 index 275084e9..00000000 --- a/documentation/build/html/_sources/graphicsItems/plotdataitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -PlotDataItem -============ - -.. autoclass:: pyqtgraph.PlotDataItem - :members: - - .. automethod:: pyqtgraph.PlotDataItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/plotitem.txt b/documentation/build/html/_sources/graphicsItems/plotitem.txt deleted file mode 100644 index cbf5f9f4..00000000 --- a/documentation/build/html/_sources/graphicsItems/plotitem.txt +++ /dev/null @@ -1,7 +0,0 @@ -PlotItem -======== - -.. autoclass:: pyqtgraph.PlotItem - :members: - - .. automethod:: pyqtgraph.PlotItem.__init__ diff --git a/documentation/build/html/_sources/graphicsItems/roi.txt b/documentation/build/html/_sources/graphicsItems/roi.txt deleted file mode 100644 index 22945ade..00000000 --- a/documentation/build/html/_sources/graphicsItems/roi.txt +++ /dev/null @@ -1,8 +0,0 @@ -ROI -=== - -.. autoclass:: pyqtgraph.ROI - :members: - - .. automethod:: pyqtgraph.ROI.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/scalebar.txt b/documentation/build/html/_sources/graphicsItems/scalebar.txt deleted file mode 100644 index 2ab33967..00000000 --- a/documentation/build/html/_sources/graphicsItems/scalebar.txt +++ /dev/null @@ -1,8 +0,0 @@ -ScaleBar -======== - -.. autoclass:: pyqtgraph.ScaleBar - :members: - - .. automethod:: pyqtgraph.ScaleBar.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/scatterplotitem.txt b/documentation/build/html/_sources/graphicsItems/scatterplotitem.txt deleted file mode 100644 index be2c874b..00000000 --- a/documentation/build/html/_sources/graphicsItems/scatterplotitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -ScatterPlotItem -=============== - -.. autoclass:: pyqtgraph.ScatterPlotItem - :members: - - .. automethod:: pyqtgraph.ScatterPlotItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/uigraphicsitem.txt b/documentation/build/html/_sources/graphicsItems/uigraphicsitem.txt deleted file mode 100644 index 4f0b9933..00000000 --- a/documentation/build/html/_sources/graphicsItems/uigraphicsitem.txt +++ /dev/null @@ -1,8 +0,0 @@ -UIGraphicsItem -============== - -.. autoclass:: pyqtgraph.UIGraphicsItem - :members: - - .. automethod:: pyqtgraph.UIGraphicsItem.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/viewbox.txt b/documentation/build/html/_sources/graphicsItems/viewbox.txt deleted file mode 100644 index 3593d295..00000000 --- a/documentation/build/html/_sources/graphicsItems/viewbox.txt +++ /dev/null @@ -1,8 +0,0 @@ -ViewBox -======= - -.. autoclass:: pyqtgraph.ViewBox - :members: - - .. automethod:: pyqtgraph.ViewBox.__init__ - diff --git a/documentation/build/html/_sources/graphicsItems/vtickgroup.txt b/documentation/build/html/_sources/graphicsItems/vtickgroup.txt deleted file mode 100644 index 342705de..00000000 --- a/documentation/build/html/_sources/graphicsItems/vtickgroup.txt +++ /dev/null @@ -1,8 +0,0 @@ -VTickGroup -========== - -.. autoclass:: pyqtgraph.VTickGroup - :members: - - .. automethod:: pyqtgraph.VTickGroup.__init__ - diff --git a/documentation/build/html/_sources/graphicswindow.txt b/documentation/build/html/_sources/graphicswindow.txt deleted file mode 100644 index 3d5641c3..00000000 --- a/documentation/build/html/_sources/graphicswindow.txt +++ /dev/null @@ -1,8 +0,0 @@ -Basic display widgets -===================== - - - GraphicsWindow - - GraphicsView - - GraphicsLayoutItem - - ViewBox - diff --git a/documentation/build/html/_sources/how_to_use.txt b/documentation/build/html/_sources/how_to_use.txt deleted file mode 100644 index 74e901d0..00000000 --- a/documentation/build/html/_sources/how_to_use.txt +++ /dev/null @@ -1,47 +0,0 @@ -How to use pyqtgraph -==================== - -There are a few suggested ways to use pyqtgraph: - -* From the interactive shell (python -i, ipython, etc) -* Displaying pop-up windows from an application -* Embedding widgets in a PyQt application - - - -Command-line use ----------------- - -Pyqtgraph makes it very easy to visualize data from the command line. Observe:: - - import pyqtgraph as pg - pg.plot(data) # data can be a list of values or a numpy array - -The example above would open a window displaying a line plot of the data given. I don't think it could reasonably be any simpler than that. The call to pg.plot returns a handle to the plot widget that is created, allowing more data to be added to the same window. - -Further examples:: - - pw = pg.plot(xVals, yVals, pen='r') # plot x vs y in red - pw.plot(xVals, yVals2, pen='b') - - win = pg.GraphicsWindow() # Automatically generates grids with multiple items - win.addPlot(data1, row=0, col=0) - win.addPlot(data2, row=0, col=1) - win.addPlot(data3, row=1, col=0, colspan=2) - - pg.show(imageData) # imageData must be a numpy array with 2 to 4 dimensions - -We're only scratching the surface here--these functions accept many different data formats and options for customizing the appearance of your data. - - -Displaying windows from within an application ---------------------------------------------- - -While I consider this approach somewhat lazy, it is often the case that 'lazy' is indistinguishable from 'highly efficient'. The approach here is simply to use the very same functions that would be used on the command line, but from within an existing application. I often use this when I simply want to get a immediate feedback about the state of data in my application without taking the time to build a user interface for it. - - -Embedding widgets inside PyQt applications ------------------------------------------- - -For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: PlotWidget, ImageView, GraphicsView, GraphicsLayoutWidget. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality. - diff --git a/documentation/build/html/_sources/images.txt b/documentation/build/html/_sources/images.txt deleted file mode 100644 index 461a9cb7..00000000 --- a/documentation/build/html/_sources/images.txt +++ /dev/null @@ -1,26 +0,0 @@ -Displaying images and video -=========================== - -Pyqtgraph displays 2D numpy arrays as images and provides tools for determining how to translate between the numpy data type and RGB values on the screen. If you want to display data from common image and video file formats, you will need to load the data first using another library (PIL works well for images and built-in numpy conversion). - -The easiest way to display 2D or 3D data is using the :func:`pyqtgraph.image` function:: - - import pyqtgraph as pg - pg.image(imageData) - -This function will accept any floating-point or integer data types and displays a single :class:`~pyqtgraph.ImageView` widget containing your data. This widget includes controls for determining how the image data will be converted to 32-bit RGBa values. Conversion happens in two steps (both are optional): - -1. Scale and offset the data (by selecting the dark/light levels on the displayed histogram) -2. Convert the data to color using a lookup table (determined by the colors shown in the gradient editor) - -If the data is 3D (time, x, y), then a time axis will be shown with a slider that can set the currently displayed frame. (if the axes in your data are ordered differently, use numpy.transpose to rearrange them) - -There are a few other methods for displaying images as well: - -* The :class:`~pyqtgraph.ImageView` class can also be instantiated directly and embedded in Qt applications. -* Instances of :class:`~pyqtgraph.ImageItem` can be used inside a GraphicsView. -* For higher performance, use :class:`~pyqtgraph.RawImageWidget`. - -Any of these classes are acceptable for displaying video by calling setImage() to display a new frame. To increase performance, the image processing system uses scipy.weave to produce compiled libraries. If your computer has a compiler available, weave will automatically attempt to build the libraries it needs on demand. If this fails, then the slower pure-python methods will be used instead. - -For more information, see the classes listed above and the 'VideoSpeedTest', 'ImageItem', 'ImageView', and 'HistogramLUT' :ref:`examples`. \ No newline at end of file diff --git a/documentation/build/html/_sources/index.txt b/documentation/build/html/_sources/index.txt deleted file mode 100644 index aa6753ef..00000000 --- a/documentation/build/html/_sources/index.txt +++ /dev/null @@ -1,32 +0,0 @@ -.. pyqtgraph documentation master file, created by - sphinx-quickstart on Fri Nov 18 19:33:12 2011. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to the documentation for pyqtgraph 1.8 -============================================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - introduction - how_to_use - plotting - images - style - region_of_interest - graphicswindow - parametertree - internals - apireference - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/documentation/build/html/_sources/introduction.txt b/documentation/build/html/_sources/introduction.txt deleted file mode 100644 index c5c1dfab..00000000 --- a/documentation/build/html/_sources/introduction.txt +++ /dev/null @@ -1,51 +0,0 @@ -Introduction -============ - - - -What is pyqtgraph? ------------------- - -Pyqtgraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer). - -Pyqtgraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt's GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort. - -It is known to run on Linux, Windows, and OSX - - -What can it do? ---------------- - -Amongst the core features of pyqtgraph are: - -* Basic data visualization primitives: Images, line and scatter plots -* Fast enough for realtime update of video/plot data -* Interactive scaling/panning, averaging, FFTs, SVG/PNG export -* Widgets for marking/selecting plot regions -* Widgets for marking/selecting image region-of-interest and automatically slicing multi-dimensional image data -* Framework for building customized image region-of-interest widgets -* Docking system that replaces/complements Qt's dock system to allow more complex (and more predictable) docking arrangements -* ParameterTree widget for rapid prototyping of dynamic interfaces (Similar to the property trees in Qt Designer and many other applications) - - -.. _examples: - -Examples --------- - -Pyqtgraph includes an extensive set of examples that can be accessed by running:: - - import pyqtgraph.examples - pyqtgraph.examples.run() - -This will start a launcher with a list of available examples. Select an item from the list to view its source code and double-click an item to run the example. - - -How does it compare to... -------------------------- - -* matplotlib: For plotting and making publication-quality graphics, matplotlib is far more mature than pyqtgraph. However, matplotlib is also much slower and not suitable for applications requiring realtime update of plots/video or rapid interactivity. It also does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. - -* pyqwt5: pyqwt is generally more mature than pyqtgraph for plotting and is about as fast. The major differences are 1) pyqtgraph is written in pure python, so it is somewhat more portable than pyqwt, which often lags behind pyqt in development (and can be a pain to install on some platforms) and 2) like matplotlib, pyqwt does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. - -(My experience with these libraries is somewhat outdated; please correct me if I am wrong here) diff --git a/documentation/build/html/_sources/parametertree.txt b/documentation/build/html/_sources/parametertree.txt deleted file mode 100644 index de699492..00000000 --- a/documentation/build/html/_sources/parametertree.txt +++ /dev/null @@ -1,7 +0,0 @@ -Rapid GUI prototyping -===================== - - - parametertree - - dockarea - - flowchart - - canvas diff --git a/documentation/build/html/_sources/plotting.txt b/documentation/build/html/_sources/plotting.txt deleted file mode 100644 index ee9ed6dc..00000000 --- a/documentation/build/html/_sources/plotting.txt +++ /dev/null @@ -1,73 +0,0 @@ -Plotting in pyqtgraph -===================== - -There are a few basic ways to plot data in pyqtgraph: - -================================================================ ================================================== -:func:`pyqtgraph.plot` Create a new plot window showing your data -:func:`PlotWidget.plot() ` Add a new set of data to an existing plot widget -:func:`PlotItem.plot() ` Add a new set of data to an existing plot widget -:func:`GraphicsWindow.addPlot() ` Add a new plot to a grid of plots -================================================================ ================================================== - -All of these will accept the same basic arguments which control how the plot data is interpreted and displayed: - -* x - Optional X data; if not specified, then a range of integers will be generated automatically. -* y - Y data. -* pen - The pen to use when drawing plot lines, or None to disable lines. -* symbol - A string describing the shape of symbols to use for each point. Optionally, this may also be a sequence of strings with a different symbol for each point. -* symbolPen - The pen (or sequence of pens) to use when drawing the symbol outline. -* symbolBrush - The brush (or sequence of brushes) to use when filling the symbol. -* fillLevel - Fills the area under the plot curve to this Y-value. -* brush - The brush to use when filling under the curve. - -See the 'plotting' :ref:`example ` for a demonstration of these arguments. - -All of the above functions also return handles to the objects that are created, allowing the plots and data to be further modified. - -Organization of Plotting Classes --------------------------------- - -There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. Pyqtgraph is based heavily on Qt's GraphicsView framework--if you are not already familiar with this, it's worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView. - -* Data Classes (all subclasses of QGraphicsItem) - * PlotCurveItem - Displays a plot line given x,y data - * ScatterPlotItem - Displays points given x,y data - * :class:`PlotDataItem ` - Combines PlotCurveItem and ScatterPlotItem. The plotting functions discussed above create objects of this type. -* Container Classes (subclasses of QGraphicsItem; contain other QGraphicsItem objects and must be viewed from within a GraphicsView) - * PlotItem - Contains a ViewBox for displaying data as well as AxisItems and labels for displaying the axes and title. This is a QGraphicsItem subclass and thus may only be used from within a GraphicsView - * GraphicsLayoutItem - QGraphicsItem subclass which displays a grid of items. This is used to display multiple PlotItems together. - * ViewBox - A QGraphicsItem subclass for displaying data. The user may scale/pan the contents of a ViewBox using the mouse. Typically all PlotData/PlotCurve/ScatterPlotItems are displayed from within a ViewBox. - * AxisItem - Displays axis values, ticks, and labels. Most commonly used with PlotItem. -* Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs) - * PlotWidget - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. - * GraphicsLayoutWidget - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget. - -.. image:: images/plottingClasses.png - - -Examples --------- - -See the 'plotting' and 'PlotWidget' :ref:`examples included with pyqtgraph ` for more information. - -Show x,y data as scatter plot:: - - import pyqtgraph as pg - import numpy as np - x = np.random.normal(size=1000) - y = np.random.normal(size=1000) - pg.plot(x, y, pen=None, symbol='o') ## setting pen=None disables line drawing - -Create/show a plot widget, display three data curves:: - - import pyqtgraph as pg - import numpy as np - x = np.arange(1000) - y = np.random.normal(size=(3, 1000)) - plotWidget = pg.plot(title="Three plot curves") - for i in range(3): - plotWidget.plot(x, y[i], pen=(i,3)) ## setting pen=(i,3) automaticaly creates three different-colored pens - - - diff --git a/documentation/build/html/_sources/region_of_interest.txt b/documentation/build/html/_sources/region_of_interest.txt deleted file mode 100644 index 24799cb7..00000000 --- a/documentation/build/html/_sources/region_of_interest.txt +++ /dev/null @@ -1,19 +0,0 @@ -Region-of-interest controls -=========================== - -Slicing Multidimensional Data ------------------------------ - -Linear Selection and Marking ----------------------------- - -2D Selection and Marking ------------------------- - - - - -- translate / rotate / scale -- highly configurable control handles -- automated data slicing -- linearregion, infiniteline diff --git a/documentation/build/html/_sources/style.txt b/documentation/build/html/_sources/style.txt deleted file mode 100644 index fc172420..00000000 --- a/documentation/build/html/_sources/style.txt +++ /dev/null @@ -1,17 +0,0 @@ -Line, Fill, and Color -===================== - -Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color. - -For these function arguments, the following values may be used: - -* single-character string representing color (b, g, r, c, m, y, k, w) -* (r, g, b) or (r, g, b, a) tuple -* single greyscale value (0.0 - 1.0) -* (index, maximum) tuple for automatically iterating through colors (see functions.intColor) -* QColor -* QPen / QBrush where appropriate - -Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt's QPen and QBrush classes. - -Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt's QColor class diff --git a/documentation/build/html/_sources/widgets/checktable.txt b/documentation/build/html/_sources/widgets/checktable.txt deleted file mode 100644 index 5301a4e9..00000000 --- a/documentation/build/html/_sources/widgets/checktable.txt +++ /dev/null @@ -1,8 +0,0 @@ -CheckTable -========== - -.. autoclass:: pyqtgraph.CheckTable - :members: - - .. automethod:: pyqtgraph.CheckTable.__init__ - diff --git a/documentation/build/html/_sources/widgets/colorbutton.txt b/documentation/build/html/_sources/widgets/colorbutton.txt deleted file mode 100644 index 690239d8..00000000 --- a/documentation/build/html/_sources/widgets/colorbutton.txt +++ /dev/null @@ -1,8 +0,0 @@ -ColorButton -=========== - -.. autoclass:: pyqtgraph.ColorButton - :members: - - .. automethod:: pyqtgraph.ColorButton.__init__ - diff --git a/documentation/build/html/_sources/widgets/datatreewidget.txt b/documentation/build/html/_sources/widgets/datatreewidget.txt deleted file mode 100644 index f6bbdbaf..00000000 --- a/documentation/build/html/_sources/widgets/datatreewidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -DataTreeWidget -============== - -.. autoclass:: pyqtgraph.DataTreeWidget - :members: - - .. automethod:: pyqtgraph.DataTreeWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/dockarea.txt b/documentation/build/html/_sources/widgets/dockarea.txt deleted file mode 100644 index 09a6acca..00000000 --- a/documentation/build/html/_sources/widgets/dockarea.txt +++ /dev/null @@ -1,5 +0,0 @@ -dockarea module -=============== - -.. automodule:: pyqtgraph.dockarea - :members: diff --git a/documentation/build/html/_sources/widgets/filedialog.txt b/documentation/build/html/_sources/widgets/filedialog.txt deleted file mode 100644 index bf2f9c07..00000000 --- a/documentation/build/html/_sources/widgets/filedialog.txt +++ /dev/null @@ -1,8 +0,0 @@ -FileDialog -========== - -.. autoclass:: pyqtgraph.FileDialog - :members: - - .. automethod:: pyqtgraph.FileDialog.__init__ - diff --git a/documentation/build/html/_sources/widgets/gradientwidget.txt b/documentation/build/html/_sources/widgets/gradientwidget.txt deleted file mode 100644 index a2587503..00000000 --- a/documentation/build/html/_sources/widgets/gradientwidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -GradientWidget -============== - -.. autoclass:: pyqtgraph.GradientWidget - :members: - - .. automethod:: pyqtgraph.GradientWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/graphicslayoutwidget.txt b/documentation/build/html/_sources/widgets/graphicslayoutwidget.txt deleted file mode 100644 index 5f885f07..00000000 --- a/documentation/build/html/_sources/widgets/graphicslayoutwidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -GraphicsLayoutWidget -==================== - -.. autoclass:: pyqtgraph.GraphicsLayoutWidget - :members: - - .. automethod:: pyqtgraph.GraphicsLayoutWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/graphicsview.txt b/documentation/build/html/_sources/widgets/graphicsview.txt deleted file mode 100644 index ac7ae3bf..00000000 --- a/documentation/build/html/_sources/widgets/graphicsview.txt +++ /dev/null @@ -1,8 +0,0 @@ -GraphicsView -============ - -.. autoclass:: pyqtgraph.GraphicsView - :members: - - .. automethod:: pyqtgraph.GraphicsView.__init__ - diff --git a/documentation/build/html/_sources/widgets/histogramlutwidget.txt b/documentation/build/html/_sources/widgets/histogramlutwidget.txt deleted file mode 100644 index 9d8f3b20..00000000 --- a/documentation/build/html/_sources/widgets/histogramlutwidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -HistogramLUTWidget -================== - -.. autoclass:: pyqtgraph.HistogramLUTWidget - :members: - - .. automethod:: pyqtgraph.HistogramLUTWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/index.txt b/documentation/build/html/_sources/widgets/index.txt deleted file mode 100644 index bce5b070..00000000 --- a/documentation/build/html/_sources/widgets/index.txt +++ /dev/null @@ -1,31 +0,0 @@ -Pyqtgraph's Widgets -=================== - -Pyqtgraph provides several QWidget subclasses which are useful for building user interfaces. These widgets can generally be used in any Qt application and provide functionality that is frequently useful in science and engineering applications. - -Contents: - -.. toctree:: - :maxdepth: 2 - - plotwidget - imageview - datatreewidget - checktable - tablewidget - gradientwidget - colorbutton - graphicslayoutwidget - dockarea - parametertree - histogramlutwidget - progressdialog - spinbox - filedialog - graphicsview - joystickbutton - multiplotwidget - treewidget - verticallabel - rawimagewidget - diff --git a/documentation/build/html/_sources/widgets/joystickbutton.txt b/documentation/build/html/_sources/widgets/joystickbutton.txt deleted file mode 100644 index 4d21e16f..00000000 --- a/documentation/build/html/_sources/widgets/joystickbutton.txt +++ /dev/null @@ -1,8 +0,0 @@ -JoystickButton -============== - -.. autoclass:: pyqtgraph.JoystickButton - :members: - - .. automethod:: pyqtgraph.JoystickButton.__init__ - diff --git a/documentation/build/html/_sources/widgets/multiplotwidget.txt b/documentation/build/html/_sources/widgets/multiplotwidget.txt deleted file mode 100644 index 46986db0..00000000 --- a/documentation/build/html/_sources/widgets/multiplotwidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -MultiPlotWidget -=============== - -.. autoclass:: pyqtgraph.MultiPlotWidget - :members: - - .. automethod:: pyqtgraph.MultiPlotWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/parametertree.txt b/documentation/build/html/_sources/widgets/parametertree.txt deleted file mode 100644 index 565b930b..00000000 --- a/documentation/build/html/_sources/widgets/parametertree.txt +++ /dev/null @@ -1,5 +0,0 @@ -parametertree module -==================== - -.. automodule:: pyqtgraph.parametertree - :members: diff --git a/documentation/build/html/_sources/widgets/plotwidget.txt b/documentation/build/html/_sources/widgets/plotwidget.txt deleted file mode 100644 index cbded80d..00000000 --- a/documentation/build/html/_sources/widgets/plotwidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -PlotWidget -========== - -.. autoclass:: pyqtgraph.PlotWidget - :members: - - .. automethod:: pyqtgraph.PlotWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/progressdialog.txt b/documentation/build/html/_sources/widgets/progressdialog.txt deleted file mode 100644 index fff04cb3..00000000 --- a/documentation/build/html/_sources/widgets/progressdialog.txt +++ /dev/null @@ -1,8 +0,0 @@ -ProgressDialog -============== - -.. autoclass:: pyqtgraph.ProgressDialog - :members: - - .. automethod:: pyqtgraph.ProgressDialog.__init__ - diff --git a/documentation/build/html/_sources/widgets/rawimagewidget.txt b/documentation/build/html/_sources/widgets/rawimagewidget.txt deleted file mode 100644 index 29fda791..00000000 --- a/documentation/build/html/_sources/widgets/rawimagewidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -RawImageWidget -============== - -.. autoclass:: pyqtgraph.RawImageWidget - :members: - - .. automethod:: pyqtgraph.RawImageWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/spinbox.txt b/documentation/build/html/_sources/widgets/spinbox.txt deleted file mode 100644 index 33da1f4c..00000000 --- a/documentation/build/html/_sources/widgets/spinbox.txt +++ /dev/null @@ -1,8 +0,0 @@ -SpinBox -======= - -.. autoclass:: pyqtgraph.SpinBox - :members: - - .. automethod:: pyqtgraph.SpinBox.__init__ - diff --git a/documentation/build/html/_sources/widgets/tablewidget.txt b/documentation/build/html/_sources/widgets/tablewidget.txt deleted file mode 100644 index 283b540b..00000000 --- a/documentation/build/html/_sources/widgets/tablewidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -TableWidget -=========== - -.. autoclass:: pyqtgraph.TableWidget - :members: - - .. automethod:: pyqtgraph.TableWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/treewidget.txt b/documentation/build/html/_sources/widgets/treewidget.txt deleted file mode 100644 index 00f9fa28..00000000 --- a/documentation/build/html/_sources/widgets/treewidget.txt +++ /dev/null @@ -1,8 +0,0 @@ -TreeWidget -========== - -.. autoclass:: pyqtgraph.TreeWidget - :members: - - .. automethod:: pyqtgraph.TreeWidget.__init__ - diff --git a/documentation/build/html/_sources/widgets/verticallabel.txt b/documentation/build/html/_sources/widgets/verticallabel.txt deleted file mode 100644 index 4f627437..00000000 --- a/documentation/build/html/_sources/widgets/verticallabel.txt +++ /dev/null @@ -1,8 +0,0 @@ -VerticalLabel -============= - -.. autoclass:: pyqtgraph.VerticalLabel - :members: - - .. automethod:: pyqtgraph.VerticalLabel.__init__ - diff --git a/documentation/build/html/_static/basic.css b/documentation/build/html/_static/basic.css deleted file mode 100644 index 69f30d4f..00000000 --- a/documentation/build/html/_static/basic.css +++ /dev/null @@ -1,509 +0,0 @@ -/* - * basic.css - * ~~~~~~~~~ - * - * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -img { - border: 0; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li div.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable dl, table.indextable dd { - margin-top: 0; - margin-bottom: 0; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- general body styles --------------------------------------------------- */ - -a.headerlink { - visibility: hidden; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.field-list ul { - padding-left: 1em; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -.align-left { - text-align: left; -} - -.align-center { - clear: both; - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px 7px 0 7px; - background-color: #ffe; - width: 40%; - float: right; -} - -p.sidebar-title { - font-weight: bold; -} - -/* -- topics ---------------------------------------------------------------- */ - -div.topic { - border: 1px solid #ccc; - padding: 7px 7px 0 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -div.admonition dl { - margin-bottom: 0; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - border: 0; - border-collapse: collapse; -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -table.field-list td, table.field-list th { - border: 0 !important; -} - -table.footnote td, table.footnote th { - border: 0 !important; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -dl { - margin-bottom: 15px; -} - -dd p { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dt:target, .highlighted { - background-color: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.refcount { - color: #060; -} - -.optional { - font-size: 1.3em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; -} - -td.linenos pre { - padding: 5px 0px; - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - margin-left: 0.5em; -} - -table.highlighttable td { - padding: 0 0.5em 0 0.5em; -} - -tt.descname { - background-color: transparent; - font-weight: bold; - font-size: 1.2em; -} - -tt.descclassname { - background-color: transparent; -} - -tt.xref, a tt { - background-color: transparent; - font-weight: bold; -} - -h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} diff --git a/documentation/build/html/_static/default.css b/documentation/build/html/_static/default.css deleted file mode 100644 index b30cb790..00000000 --- a/documentation/build/html/_static/default.css +++ /dev/null @@ -1,255 +0,0 @@ -/* - * default.css_t - * ~~~~~~~~~~~~~ - * - * Sphinx stylesheet -- default theme. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -@import url("basic.css"); - -/* -- page layout ----------------------------------------------------------- */ - -body { - font-family: sans-serif; - font-size: 100%; - background-color: #11303d; - color: #000; - margin: 0; - padding: 0; -} - -div.document { - background-color: #1c4e63; -} - -div.documentwrapper { - float: left; - width: 100%; -} - -div.bodywrapper { - margin: 0 0 0 230px; -} - -div.body { - background-color: #ffffff; - color: #000000; - padding: 0 20px 30px 20px; -} - -div.footer { - color: #ffffff; - width: 100%; - padding: 9px 0 9px 0; - text-align: center; - font-size: 75%; -} - -div.footer a { - color: #ffffff; - text-decoration: underline; -} - -div.related { - background-color: #133f52; - line-height: 30px; - color: #ffffff; -} - -div.related a { - color: #ffffff; -} - -div.sphinxsidebar { -} - -div.sphinxsidebar h3 { - font-family: 'Trebuchet MS', sans-serif; - color: #ffffff; - font-size: 1.4em; - font-weight: normal; - margin: 0; - padding: 0; -} - -div.sphinxsidebar h3 a { - color: #ffffff; -} - -div.sphinxsidebar h4 { - font-family: 'Trebuchet MS', sans-serif; - color: #ffffff; - font-size: 1.3em; - font-weight: normal; - margin: 5px 0 0 0; - padding: 0; -} - -div.sphinxsidebar p { - color: #ffffff; -} - -div.sphinxsidebar p.topless { - margin: 5px 10px 10px 10px; -} - -div.sphinxsidebar ul { - margin: 10px; - padding: 0; - color: #ffffff; -} - -div.sphinxsidebar a { - color: #98dbcc; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - - -/* -- hyperlink styles ------------------------------------------------------ */ - -a { - color: #355f7c; - text-decoration: none; -} - -a:visited { - color: #355f7c; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - - - -/* -- body styles ----------------------------------------------------------- */ - -div.body h1, -div.body h2, -div.body h3, -div.body h4, -div.body h5, -div.body h6 { - font-family: 'Trebuchet MS', sans-serif; - background-color: #f2f2f2; - font-weight: normal; - color: #20435c; - border-bottom: 1px solid #ccc; - margin: 20px -20px 10px -20px; - padding: 3px 0 3px 10px; -} - -div.body h1 { margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 160%; } -div.body h3 { font-size: 140%; } -div.body h4 { font-size: 120%; } -div.body h5 { font-size: 110%; } -div.body h6 { font-size: 100%; } - -a.headerlink { - color: #c60f0f; - font-size: 0.8em; - padding: 0 4px 0 4px; - text-decoration: none; -} - -a.headerlink:hover { - background-color: #c60f0f; - color: white; -} - -div.body p, div.body dd, div.body li { - text-align: justify; - line-height: 130%; -} - -div.admonition p.admonition-title + p { - display: inline; -} - -div.admonition p { - margin-bottom: 5px; -} - -div.admonition pre { - margin-bottom: 5px; -} - -div.admonition ul, div.admonition ol { - margin-bottom: 5px; -} - -div.note { - background-color: #eee; - border: 1px solid #ccc; -} - -div.seealso { - background-color: #ffc; - border: 1px solid #ff6; -} - -div.topic { - background-color: #eee; -} - -div.warning { - background-color: #ffe4e4; - border: 1px solid #f66; -} - -p.admonition-title { - display: inline; -} - -p.admonition-title:after { - content: ":"; -} - -pre { - padding: 5px; - background-color: #eeffcc; - color: #333333; - line-height: 120%; - border: 1px solid #ac9; - border-left: none; - border-right: none; -} - -tt { - background-color: #ecf0f3; - padding: 0 1px 0 1px; - font-size: 0.95em; -} - -th { - background-color: #ede; -} - -.warning tt { - background: #efc2c2; -} - -.note tt { - background: #d6d6d6; -} - -.viewcode-back { - font-family: sans-serif; -} - -div.viewcode-block:target { - background-color: #f4debf; - border-top: 1px solid #ac9; - border-bottom: 1px solid #ac9; -} \ No newline at end of file diff --git a/documentation/build/html/_static/doctools.js b/documentation/build/html/_static/doctools.js deleted file mode 100644 index eeea95ea..00000000 --- a/documentation/build/html/_static/doctools.js +++ /dev/null @@ -1,247 +0,0 @@ -/* - * doctools.js - * ~~~~~~~~~~~ - * - * Sphinx JavaScript utilties for all documentation. - * - * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/** - * select a different prefix for underscore - */ -$u = _.noConflict(); - -/** - * make the code below compatible with browsers without - * an installed firebug like debugger -if (!window.console || !console.firebug) { - var names = ["log", "debug", "info", "warn", "error", "assert", "dir", - "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", - "profile", "profileEnd"]; - window.console = {}; - for (var i = 0; i < names.length; ++i) - window.console[names[i]] = function() {}; -} - */ - -/** - * small helper function to urldecode strings - */ -jQuery.urldecode = function(x) { - return decodeURIComponent(x).replace(/\+/g, ' '); -} - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s == 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * small function to check if an array contains - * a given item. - */ -jQuery.contains = function(arr, item) { - for (var i = 0; i < arr.length; i++) { - if (arr[i] == item) - return true; - } - return false; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node) { - if (node.nodeType == 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { - var span = document.createElement("span"); - span.className = className; - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this); - }); - } - } - return this.each(function() { - highlight(this); - }); -}; - -/** - * Small JavaScript module for the documentation. - */ -var Documentation = { - - init : function() { - this.fixFirefoxAnchorBug(); - this.highlightSearchWords(); - this.initIndexTable(); - }, - - /** - * i18n support - */ - TRANSLATIONS : {}, - PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, - LOCALE : 'unknown', - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext : function(string) { - var translated = Documentation.TRANSLATIONS[string]; - if (typeof translated == 'undefined') - return string; - return (typeof translated == 'string') ? translated : translated[0]; - }, - - ngettext : function(singular, plural, n) { - var translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated == 'undefined') - return (n == 1) ? singular : plural; - return translated[Documentation.PLURALEXPR(n)]; - }, - - addTranslations : function(catalog) { - for (var key in catalog.messages) - this.TRANSLATIONS[key] = catalog.messages[key]; - this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); - this.LOCALE = catalog.locale; - }, - - /** - * add context elements like header anchor links - */ - addContextElements : function() { - $('div[id] > :header:first').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this headline')). - appendTo(this); - }); - $('dt[id]').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this definition')). - appendTo(this); - }); - }, - - /** - * workaround a firefox stupidity - */ - fixFirefoxAnchorBug : function() { - if (document.location.hash && $.browser.mozilla) - window.setTimeout(function() { - document.location.href += ''; - }, 10); - }, - - /** - * highlight the search words provided in the url in the text - */ - highlightSearchWords : function() { - var params = $.getQueryParameters(); - var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; - if (terms.length) { - var body = $('div.body'); - window.setTimeout(function() { - $.each(terms, function() { - body.highlightText(this.toLowerCase(), 'highlighted'); - }); - }, 10); - $('') - .appendTo($('.sidebar .this-page-menu')); - } - }, - - /** - * init the domain index toggle buttons - */ - initIndexTable : function() { - var togglers = $('img.toggler').click(function() { - var src = $(this).attr('src'); - var idnum = $(this).attr('id').substr(7); - $('tr.cg-' + idnum).toggle(); - if (src.substr(-9) == 'minus.png') - $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); - else - $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); - }).css('display', ''); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { - togglers.click(); - } - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords : function() { - $('.sidebar .this-page-menu li.highlight-link').fadeOut(300); - $('span.highlighted').removeClass('highlighted'); - }, - - /** - * make the url absolute - */ - makeURL : function(relativeURL) { - return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; - }, - - /** - * get the current relative url - */ - getCurrentURL : function() { - var path = document.location.pathname; - var parts = path.split(/\//); - $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { - if (this == '..') - parts.pop(); - }); - var url = parts.join('/'); - return path.substring(url.lastIndexOf('/') + 1, path.length - 1); - } -}; - -// quick alias for translations -_ = Documentation.gettext; - -$(document).ready(function() { - Documentation.init(); -}); diff --git a/documentation/build/html/_static/file.png b/documentation/build/html/_static/file.png deleted file mode 100644 index d18082e397e7e54f20721af768c4c2983258f1b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 392 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP$HyOL$D9)yc9|lc|nKf<9@eUiWd>3GuTC!a5vdfWYEazjncPj5ZQX%+1 zt8B*4=d)!cdDz4wr^#OMYfqGz$1LDFF>|#>*O?AGil(WEs?wLLy{Gj2J_@opDm%`dlax3yA*@*N$G&*ukFv>P8+2CBWO(qz zD0k1@kN>hhb1_6`&wrCswzINE(evt-5C1B^STi2@PmdKI;Vst0PQB6!2kdN diff --git a/documentation/build/html/_static/jquery.js b/documentation/build/html/_static/jquery.js deleted file mode 100644 index 5c99a8d4..00000000 --- a/documentation/build/html/_static/jquery.js +++ /dev/null @@ -1,8176 +0,0 @@ -/*! - * jQuery JavaScript Library v1.5 - * http://jquery.com/ - * - * Copyright 2011, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Mon Jan 31 08:31:29 2011 -0500 - */ -(function( window, undefined ) { - -// Use the correct document accordingly with window argument (sandbox) -var document = window.document; -var jQuery = (function() { - -// Define a local copy of jQuery -var jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); - }, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // A central reference to the root jQuery(document) - rootjQuery, - - // A simple way to check for HTML strings or ID strings - // (both of which we optimize for) - quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/, - - // Check if a string has a non-whitespace character in it - rnotwhite = /\S/, - - // Used for trimming whitespace - trimLeft = /^\s+/, - trimRight = /\s+$/, - - // Check for digits - rdigit = /\d/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, - rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - - // Useragent RegExp - rwebkit = /(webkit)[ \/]([\w.]+)/, - ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, - rmsie = /(msie) ([\w.]+)/, - rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, - - // Keep a UserAgent string for use with jQuery.browser - userAgent = navigator.userAgent, - - // For matching the engine and version of the browser - browserMatch, - - // Has the ready events already been bound? - readyBound = false, - - // The deferred used on DOM ready - readyList, - - // Promise methods - promiseMethods = "then done fail isResolved isRejected promise".split( " " ), - - // The ready event handler - DOMContentLoaded, - - // Save a reference to some core methods - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - push = Array.prototype.push, - slice = Array.prototype.slice, - trim = String.prototype.trim, - indexOf = Array.prototype.indexOf, - - // [[Class]] -> type pairs - class2type = {}; - -jQuery.fn = jQuery.prototype = { - constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem, ret, doc; - - // Handle $(""), $(null), or $(undefined) - if ( !selector ) { - return this; - } - - // Handle $(DOMElement) - if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - } - - // The body element only exists once, optimize finding it - if ( selector === "body" && !context && document.body ) { - this.context = document; - this[0] = document.body; - this.selector = "body"; - this.length = 1; - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - // Are we dealing with HTML string or an ID? - match = quickExpr.exec( selector ); - - // Verify a match, and that no context was specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - doc = (context ? context.ownerDocument || context : document); - - // If a single string is passed in and it's a single tag - // just do a createElement and skip the rest - ret = rsingleTag.exec( selector ); - - if ( ret ) { - if ( jQuery.isPlainObject( context ) ) { - selector = [ document.createElement( ret[1] ) ]; - jQuery.fn.attr.call( selector, context, true ); - - } else { - selector = [ doc.createElement( ret[1] ) ]; - } - - } else { - ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); - selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes; - } - - return jQuery.merge( this, selector ); - - // HANDLE: $("#id") - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return (context || rootjQuery).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if (selector.selector !== undefined) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, - - // Start with an empty selector - selector: "", - - // The current version of jQuery being used - jquery: "1.5", - - // The default length of a jQuery object is 0 - length: 0, - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - toArray: function() { - return slice.call( this, 0 ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems, name, selector ) { - // Build a new jQuery matched element set - var ret = this.constructor(); - - if ( jQuery.isArray( elems ) ) { - push.apply( ret, elems ); - - } else { - jQuery.merge( ret, elems ); - } - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - ret.context = this.context; - - if ( name === "find" ) { - ret.selector = this.selector + (this.selector ? " " : "") + selector; - } else if ( name ) { - ret.selector = this.selector + "." + name + "(" + selector + ")"; - } - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - ready: function( fn ) { - // Attach the listeners - jQuery.bindReady(); - - // Add the callback - readyList.done( fn ); - - return this; - }, - - eq: function( i ) { - return i === -1 ? - this.slice( i ) : - this.slice( i, +i + 1 ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ), - "slice", slice.call(arguments).join(",") ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: [].sort, - splice: [].splice -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( length === i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - noConflict: function( deep ) { - window.$ = _$; - - if ( deep ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Handle when the DOM is ready - ready: function( wait ) { - // A third-party is pushing the ready event forwards - if ( wait === true ) { - jQuery.readyWait--; - } - - // Make sure that the DOM is not already loaded - if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready, 1 ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger( "ready" ).unbind( "ready" ); - } - } - }, - - bindReady: function() { - if ( readyBound ) { - return; - } - - readyBound = true; - - // Catch cases where $(document).ready() is called after the - // browser event has already occurred. - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - return setTimeout( jQuery.ready, 1 ); - } - - // Mozilla, Opera and webkit nightlies currently support this event - if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", jQuery.ready, false ); - - // If IE event model is used - } else if ( document.attachEvent ) { - // ensure firing before onload, - // maybe late but safe also for iframes - document.attachEvent("onreadystatechange", DOMContentLoaded); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", jQuery.ready ); - - // If IE and not a frame - // continually check to see if the document is ready - var toplevel = false; - - try { - toplevel = window.frameElement == null; - } catch(e) {} - - if ( document.documentElement.doScroll && toplevel ) { - doScrollCheck(); - } - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - // A crude way of determining if an object is a window - isWindow: function( obj ) { - return obj && typeof obj === "object" && "setInterval" in obj; - }, - - isNaN: function( obj ) { - return obj == null || !rdigit.test( obj ) || isNaN( obj ); - }, - - type: function( obj ) { - return obj == null ? - String( obj ) : - class2type[ toString.call(obj) ] || "object"; - }, - - isPlainObject: function( obj ) { - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - // Not own constructor property must be Object - if ( obj.constructor && - !hasOwn.call(obj, "constructor") && - !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - - var key; - for ( key in obj ) {} - - return key === undefined || hasOwn.call( obj, key ); - }, - - isEmptyObject: function( obj ) { - for ( var name in obj ) { - return false; - } - return true; - }, - - error: function( msg ) { - throw msg; - }, - - parseJSON: function( data ) { - if ( typeof data !== "string" || !data ) { - return null; - } - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test(data.replace(rvalidescape, "@") - .replace(rvalidtokens, "]") - .replace(rvalidbraces, "")) ) { - - // Try to use the native JSON parser first - return window.JSON && window.JSON.parse ? - window.JSON.parse( data ) : - (new Function("return " + data))(); - - } else { - jQuery.error( "Invalid JSON: " + data ); - } - }, - - // Cross-browser xml parsing - // (xml & tmp used internally) - parseXML: function( data , xml , tmp ) { - - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - - tmp = xml.documentElement; - - if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) { - jQuery.error( "Invalid XML: " + data ); - } - - return xml; - }, - - noop: function() {}, - - // Evalulates a script in a global context - globalEval: function( data ) { - if ( data && rnotwhite.test(data) ) { - // Inspired by code by Andrea Giammarchi - // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html - var head = document.getElementsByTagName("head")[0] || document.documentElement, - script = document.createElement("script"); - - script.type = "text/javascript"; - - if ( jQuery.support.scriptEval() ) { - script.appendChild( document.createTextNode( data ) ); - } else { - script.text = data; - } - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709). - head.insertBefore( script, head.firstChild ); - head.removeChild( script ); - } - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); - }, - - // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, - length = object.length, - isObj = length === undefined || jQuery.isFunction(object); - - if ( args ) { - if ( isObj ) { - for ( name in object ) { - if ( callback.apply( object[ name ], args ) === false ) { - break; - } - } - } else { - for ( ; i < length; ) { - if ( callback.apply( object[ i++ ], args ) === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isObj ) { - for ( name in object ) { - if ( callback.call( object[ name ], name, object[ name ] ) === false ) { - break; - } - } - } else { - for ( var value = object[0]; - i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} - } - } - - return object; - }, - - // Use native String.trim function wherever possible - trim: trim ? - function( text ) { - return text == null ? - "" : - trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); - }, - - // results is for internal usage only - makeArray: function( array, results ) { - var ret = results || []; - - if ( array != null ) { - // The window, strings (and functions) also have 'length' - // The extra typeof function check is to prevent crashes - // in Safari 2 (See: #3039) - // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 - var type = jQuery.type(array); - - if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { - push.call( ret, array ); - } else { - jQuery.merge( ret, array ); - } - } - - return ret; - }, - - inArray: function( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); - } - - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; - } - } - - return -1; - }, - - merge: function( first, second ) { - var i = first.length, - j = 0; - - if ( typeof second.length === "number" ) { - for ( var l = second.length; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, inv ) { - var ret = [], retVal; - inv = !!inv; - - // Go through the array, only saving the items - // that pass the validator function - for ( var i = 0, length = elems.length; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var ret = [], value; - - // Go through the array, translating each of the items to their - // new value (or values). - for ( var i = 0, length = elems.length; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - // Flatten any nested arrays - return ret.concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - proxy: function( fn, proxy, thisObject ) { - if ( arguments.length === 2 ) { - if ( typeof proxy === "string" ) { - thisObject = fn; - fn = thisObject[ proxy ]; - proxy = undefined; - - } else if ( proxy && !jQuery.isFunction( proxy ) ) { - thisObject = proxy; - proxy = undefined; - } - } - - if ( !proxy && fn ) { - proxy = function() { - return fn.apply( thisObject || this, arguments ); - }; - } - - // Set the guid of unique handler to the same of original handler, so it can be removed - if ( fn ) { - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - } - - // So proxy can be declared as an argument - return proxy; - }, - - // Mutifunctional method to get and set values to a collection - // The value/s can be optionally by executed if its a function - access: function( elems, key, value, exec, fn, pass ) { - var length = elems.length; - - // Setting many attributes - if ( typeof key === "object" ) { - for ( var k in key ) { - jQuery.access( elems, k, key[k], exec, fn, value ); - } - return elems; - } - - // Setting one attribute - if ( value !== undefined ) { - // Optionally, function values get executed if exec is true - exec = !pass && exec && jQuery.isFunction(value); - - for ( var i = 0; i < length; i++ ) { - fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); - } - - return elems; - } - - // Getting an attribute - return length ? fn( elems[0], key ) : undefined; - }, - - now: function() { - return (new Date()).getTime(); - }, - - // Create a simple deferred (one callbacks list) - _Deferred: function() { - var // callbacks list - callbacks = [], - // stored [ context , args ] - fired, - // to avoid firing when already doing so - firing, - // flag to know if the deferred has been cancelled - cancelled, - // the deferred itself - deferred = { - - // done( f1, f2, ...) - done: function() { - if ( !cancelled ) { - var args = arguments, - i, - length, - elem, - type, - _fired; - if ( fired ) { - _fired = fired; - fired = 0; - } - for ( i = 0, length = args.length; i < length; i++ ) { - elem = args[ i ]; - type = jQuery.type( elem ); - if ( type === "array" ) { - deferred.done.apply( deferred, elem ); - } else if ( type === "function" ) { - callbacks.push( elem ); - } - } - if ( _fired ) { - deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); - } - } - return this; - }, - - // resolve with given context and args - resolveWith: function( context, args ) { - if ( !cancelled && !fired && !firing ) { - firing = 1; - try { - while( callbacks[ 0 ] ) { - callbacks.shift().apply( context, args ); - } - } - finally { - fired = [ context, args ]; - firing = 0; - } - } - return this; - }, - - // resolve with this as context and given arguments - resolve: function() { - deferred.resolveWith( jQuery.isFunction( this.promise ) ? this.promise() : this, arguments ); - return this; - }, - - // Has this deferred been resolved? - isResolved: function() { - return !!( firing || fired ); - }, - - // Cancel - cancel: function() { - cancelled = 1; - callbacks = []; - return this; - } - }; - - return deferred; - }, - - // Full fledged deferred (two callbacks list) - Deferred: function( func ) { - var deferred = jQuery._Deferred(), - failDeferred = jQuery._Deferred(), - promise; - // Add errorDeferred methods, then and promise - jQuery.extend( deferred, { - then: function( doneCallbacks, failCallbacks ) { - deferred.done( doneCallbacks ).fail( failCallbacks ); - return this; - }, - fail: failDeferred.done, - rejectWith: failDeferred.resolveWith, - reject: failDeferred.resolve, - isRejected: failDeferred.isResolved, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj , i /* internal */ ) { - if ( obj == null ) { - if ( promise ) { - return promise; - } - promise = obj = {}; - } - i = promiseMethods.length; - while( i-- ) { - obj[ promiseMethods[ i ] ] = deferred[ promiseMethods[ i ] ]; - } - return obj; - } - } ); - // Make sure only one callback list will be used - deferred.then( failDeferred.cancel, deferred.cancel ); - // Unexpose cancel - delete deferred.cancel; - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - return deferred; - }, - - // Deferred helper - when: function( object ) { - var args = arguments, - length = args.length, - deferred = length <= 1 && object && jQuery.isFunction( object.promise ) ? - object : - jQuery.Deferred(), - promise = deferred.promise(), - resolveArray; - - if ( length > 1 ) { - resolveArray = new Array( length ); - jQuery.each( args, function( index, element ) { - jQuery.when( element ).then( function( value ) { - resolveArray[ index ] = arguments.length > 1 ? slice.call( arguments, 0 ) : value; - if( ! --length ) { - deferred.resolveWith( promise, resolveArray ); - } - }, deferred.reject ); - } ); - } else if ( deferred !== object ) { - deferred.resolve( object ); - } - return promise; - }, - - // Use of jQuery.browser is frowned upon. - // More details: http://docs.jquery.com/Utilities/jQuery.browser - uaMatch: function( ua ) { - ua = ua.toLowerCase(); - - var match = rwebkit.exec( ua ) || - ropera.exec( ua ) || - rmsie.exec( ua ) || - ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || - []; - - return { browser: match[1] || "", version: match[2] || "0" }; - }, - - sub: function() { - function jQuerySubclass( selector, context ) { - return new jQuerySubclass.fn.init( selector, context ); - } - jQuery.extend( true, jQuerySubclass, this ); - jQuerySubclass.superclass = this; - jQuerySubclass.fn = jQuerySubclass.prototype = this(); - jQuerySubclass.fn.constructor = jQuerySubclass; - jQuerySubclass.subclass = this.subclass; - jQuerySubclass.fn.init = function init( selector, context ) { - if ( context && context instanceof jQuery && !(context instanceof jQuerySubclass) ) { - context = jQuerySubclass(context); - } - - return jQuery.fn.init.call( this, selector, context, rootjQuerySubclass ); - }; - jQuerySubclass.fn.init.prototype = jQuerySubclass.fn; - var rootjQuerySubclass = jQuerySubclass(document); - return jQuerySubclass; - }, - - browser: {} -}); - -// Create readyList deferred -readyList = jQuery._Deferred(); - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -browserMatch = jQuery.uaMatch( userAgent ); -if ( browserMatch.browser ) { - jQuery.browser[ browserMatch.browser ] = true; - jQuery.browser.version = browserMatch.version; -} - -// Deprecated, use jQuery.browser.webkit instead -if ( jQuery.browser.webkit ) { - jQuery.browser.safari = true; -} - -if ( indexOf ) { - jQuery.inArray = function( elem, array ) { - return indexOf.call( array, elem ); - }; -} - -// IE doesn't match non-breaking spaces with \s -if ( rnotwhite.test( "\xA0" ) ) { - trimLeft = /^[\s\xA0]+/; - trimRight = /[\s\xA0]+$/; -} - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); - -// Cleanup functions for the document ready method -if ( document.addEventListener ) { - DOMContentLoaded = function() { - document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - jQuery.ready(); - }; - -} else if ( document.attachEvent ) { - DOMContentLoaded = function() { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( document.readyState === "complete" ) { - document.detachEvent( "onreadystatechange", DOMContentLoaded ); - jQuery.ready(); - } - }; -} - -// The DOM ready check for Internet Explorer -function doScrollCheck() { - if ( jQuery.isReady ) { - return; - } - - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - document.documentElement.doScroll("left"); - } catch(e) { - setTimeout( doScrollCheck, 1 ); - return; - } - - // and execute any waiting functions - jQuery.ready(); -} - -// Expose jQuery to the global object -return (window.jQuery = window.$ = jQuery); - -})(); - - -(function() { - - jQuery.support = {}; - - var div = document.createElement("div"); - - div.style.display = "none"; - div.innerHTML = "
a"; - - var all = div.getElementsByTagName("*"), - a = div.getElementsByTagName("a")[0], - select = document.createElement("select"), - opt = select.appendChild( document.createElement("option") ); - - // Can't get basic test support - if ( !all || !all.length || !a ) { - return; - } - - jQuery.support = { - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: div.firstChild.nodeType === 3, - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName("tbody").length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName("link").length, - - // Get the style information from getAttribute - // (IE uses .cssText insted) - style: /red/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: a.getAttribute("href") === "/a", - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55$/.test( a.style.opacity ), - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, - - // Make sure that if no value is specified for a checkbox - // that it defaults to "on". - // (WebKit defaults to "" instead) - checkOn: div.getElementsByTagName("input")[0].value === "on", - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: opt.selected, - - // Will be defined later - deleteExpando: true, - optDisabled: false, - checkClone: false, - _scriptEval: null, - noCloneEvent: true, - boxModel: null, - inlineBlockNeedsLayout: false, - shrinkWrapBlocks: false, - reliableHiddenOffsets: true - }; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as diabled) - select.disabled = true; - jQuery.support.optDisabled = !opt.disabled; - - jQuery.support.scriptEval = function() { - if ( jQuery.support._scriptEval === null ) { - var root = document.documentElement, - script = document.createElement("script"), - id = "script" + jQuery.now(); - - script.type = "text/javascript"; - try { - script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); - } catch(e) {} - - root.insertBefore( script, root.firstChild ); - - // Make sure that the execution of code works by injecting a script - // tag with appendChild/createTextNode - // (IE doesn't support this, fails, and uses .text instead) - if ( window[ id ] ) { - jQuery.support._scriptEval = true; - delete window[ id ]; - } else { - jQuery.support._scriptEval = false; - } - - root.removeChild( script ); - // release memory in IE - root = script = id = null; - } - - return jQuery.support._scriptEval; - }; - - // Test to see if it's possible to delete an expando from an element - // Fails in Internet Explorer - try { - delete div.test; - - } catch(e) { - jQuery.support.deleteExpando = false; - } - - if ( div.attachEvent && div.fireEvent ) { - div.attachEvent("onclick", function click() { - // Cloning a node shouldn't copy over any - // bound event handlers (IE does this) - jQuery.support.noCloneEvent = false; - div.detachEvent("onclick", click); - }); - div.cloneNode(true).fireEvent("onclick"); - } - - div = document.createElement("div"); - div.innerHTML = ""; - - var fragment = document.createDocumentFragment(); - fragment.appendChild( div.firstChild ); - - // WebKit doesn't clone checked state correctly in fragments - jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; - - // Figure out if the W3C box model works as expected - // document.body must exist before we can do this - jQuery(function() { - var div = document.createElement("div"), - body = document.getElementsByTagName("body")[0]; - - // Frameset documents with no body should not run this code - if ( !body ) { - return; - } - - div.style.width = div.style.paddingLeft = "1px"; - body.appendChild( div ); - jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; - - if ( "zoom" in div.style ) { - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - // (IE < 8 does this) - div.style.display = "inline"; - div.style.zoom = 1; - jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2; - - // Check if elements with layout shrink-wrap their children - // (IE 6 does this) - div.style.display = ""; - div.innerHTML = "
"; - jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2; - } - - div.innerHTML = "
t
"; - var tds = div.getElementsByTagName("td"); - - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - // (only IE 8 fails this test) - jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0; - - tds[0].style.display = ""; - tds[1].style.display = "none"; - - // Check if empty table cells still have offsetWidth/Height - // (IE < 8 fail this test) - jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0; - div.innerHTML = ""; - - body.removeChild( div ).style.display = "none"; - div = tds = null; - }); - - // Technique from Juriy Zaytsev - // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ - var eventSupported = function( eventName ) { - var el = document.createElement("div"); - eventName = "on" + eventName; - - // We only care about the case where non-standard event systems - // are used, namely in IE. Short-circuiting here helps us to - // avoid an eval call (in setAttribute) which can cause CSP - // to go haywire. See: https://developer.mozilla.org/en/Security/CSP - if ( !el.attachEvent ) { - return true; - } - - var isSupported = (eventName in el); - if ( !isSupported ) { - el.setAttribute(eventName, "return;"); - isSupported = typeof el[eventName] === "function"; - } - el = null; - - return isSupported; - }; - - jQuery.support.submitBubbles = eventSupported("submit"); - jQuery.support.changeBubbles = eventSupported("change"); - - // release memory in IE - div = all = a = null; -})(); - - - -var rbrace = /^(?:\{.*\}|\[.*\])$/; - -jQuery.extend({ - cache: {}, - - // Please use with caution - uuid: 0, - - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", - "applet": true - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - - return !!elem && !jQuery.isEmptyObject(elem); - }, - - data: function( elem, name, data, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache, - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - elem[ jQuery.expando ] = id = ++jQuery.uuid; - } else { - id = jQuery.expando; - } - } - - if ( !cache[ id ] ) { - cache[ id ] = {}; - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" ) { - if ( pvt ) { - cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name); - } else { - cache[ id ] = jQuery.extend(cache[ id ], name); - } - } - - thisCache = cache[ id ]; - - // Internal jQuery data is stored in a separate object inside the object's data - // cache in order to avoid key collisions between internal data and user-defined - // data - if ( pvt ) { - if ( !thisCache[ internalKey ] ) { - thisCache[ internalKey ] = {}; - } - - thisCache = thisCache[ internalKey ]; - } - - if ( data !== undefined ) { - thisCache[ name ] = data; - } - - // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should - // not attempt to inspect the internal events object using jQuery.data, as this - // internal data object is undocumented and subject to change. - if ( name === "events" && !thisCache[name] ) { - return thisCache[ internalKey ] && thisCache[ internalKey ].events; - } - - return getByName ? thisCache[ name ] : thisCache; - }, - - removeData: function( elem, name, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var internalKey = jQuery.expando, isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - - // See jQuery.data for more information - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ]; - - if ( thisCache ) { - delete thisCache[ name ]; - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( !jQuery.isEmptyObject(thisCache) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( pvt ) { - delete cache[ id ][ internalKey ]; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !jQuery.isEmptyObject(cache[ id ]) ) { - return; - } - } - - var internalCache = cache[ id ][ internalKey ]; - - // Browsers that fail expando deletion also refuse to delete expandos on - // the window, but it will allow it on all other JS objects; other browsers - // don't care - if ( jQuery.support.deleteExpando || cache != window ) { - delete cache[ id ]; - } else { - cache[ id ] = null; - } - - // We destroyed the entire user cache at once because it's faster than - // iterating through each key, but we need to continue to persist internal - // data if it existed - if ( internalCache ) { - cache[ id ] = {}; - cache[ id ][ internalKey ] = internalCache; - - // Otherwise, we need to eliminate the expando on the node to avoid - // false lookups in the cache for entries that no longer exist - } else if ( isNode ) { - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( jQuery.support.deleteExpando ) { - delete elem[ jQuery.expando ]; - } else if ( elem.removeAttribute ) { - elem.removeAttribute( jQuery.expando ); - } else { - elem[ jQuery.expando ] = null; - } - } - }, - - // For internal use only. - _data: function( elem, name, data ) { - return jQuery.data( elem, name, data, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - if ( elem.nodeName ) { - var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; - - if ( match ) { - return !(match === true || elem.getAttribute("classid") !== match); - } - } - - return true; - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var data = null; - - if ( typeof key === "undefined" ) { - if ( this.length ) { - data = jQuery.data( this[0] ); - - if ( this[0].nodeType === 1 ) { - var attr = this[0].attributes, name; - for ( var i = 0, l = attr.length; i < l; i++ ) { - name = attr[i].name; - - if ( name.indexOf( "data-" ) === 0 ) { - name = name.substr( 5 ); - dataAttr( this[0], name, data[ name ] ); - } - } - } - } - - return data; - - } else if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - var parts = key.split("."); - parts[1] = parts[1] ? "." + parts[1] : ""; - - if ( value === undefined ) { - data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); - - // Try to fetch any internally stored data first - if ( data === undefined && this.length ) { - data = jQuery.data( this[0], key ); - data = dataAttr( this[0], key, data ); - } - - return data === undefined && parts[1] ? - this.data( parts[0] ) : - data; - - } else { - return this.each(function() { - var $this = jQuery( this ), - args = [ parts[0], value ]; - - $this.triggerHandler( "setData" + parts[1] + "!", args ); - jQuery.data( this, key, value ); - $this.triggerHandler( "changeData" + parts[1] + "!", args ); - }); - } - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - data = elem.getAttribute( "data-" + key ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - !jQuery.isNaN( data ) ? parseFloat( data ) : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - - - - -jQuery.extend({ - queue: function( elem, type, data ) { - if ( !elem ) { - return; - } - - type = (type || "fx") + "queue"; - var q = jQuery._data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( !data ) { - return q || []; - } - - if ( !q || jQuery.isArray(data) ) { - q = jQuery._data( elem, type, jQuery.makeArray(data) ); - - } else { - q.push( data ); - } - - return q; - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - fn = queue.shift(); - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - } - - if ( fn ) { - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift("inprogress"); - } - - fn.call(elem, function() { - jQuery.dequeue(elem, type); - }); - } - - if ( !queue.length ) { - jQuery.removeData( elem, type + "queue", true ); - } - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - } - - if ( data === undefined ) { - return jQuery.queue( this[0], type ); - } - return this.each(function( i ) { - var queue = jQuery.queue( this, type, data ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; - type = type || "fx"; - - return this.queue( type, function() { - var elem = this; - setTimeout(function() { - jQuery.dequeue( elem, type ); - }, time ); - }); - }, - - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - } -}); - - - - -var rclass = /[\n\t\r]/g, - rspaces = /\s+/, - rreturn = /\r/g, - rspecialurl = /^(?:href|src|style)$/, - rtype = /^(?:button|input)$/i, - rfocusable = /^(?:button|input|object|select|textarea)$/i, - rclickable = /^a(?:rea)?$/i, - rradiocheck = /^(?:radio|checkbox)$/i; - -jQuery.props = { - "for": "htmlFor", - "class": "className", - readonly: "readOnly", - maxlength: "maxLength", - cellspacing: "cellSpacing", - rowspan: "rowSpan", - colspan: "colSpan", - tabindex: "tabIndex", - usemap: "useMap", - frameborder: "frameBorder" -}; - -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, name, value, true, jQuery.attr ); - }, - - removeAttr: function( name, fn ) { - return this.each(function(){ - jQuery.attr( this, name, "" ); - if ( this.nodeType === 1 ) { - this.removeAttribute( name ); - } - }); - }, - - addClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.addClass( value.call(this, i, self.attr("class")) ); - }); - } - - if ( value && typeof value === "string" ) { - var classNames = (value || "").split( rspaces ); - - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; - - if ( elem.nodeType === 1 ) { - if ( !elem.className ) { - elem.className = value; - - } else { - var className = " " + elem.className + " ", - setClass = elem.className; - - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { - setClass += " " + classNames[c]; - } - } - elem.className = jQuery.trim( setClass ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - if ( jQuery.isFunction(value) ) { - return this.each(function(i) { - var self = jQuery(this); - self.removeClass( value.call(this, i, self.attr("class")) ); - }); - } - - if ( (value && typeof value === "string") || value === undefined ) { - var classNames = (value || "").split( rspaces ); - - for ( var i = 0, l = this.length; i < l; i++ ) { - var elem = this[i]; - - if ( elem.nodeType === 1 && elem.className ) { - if ( value ) { - var className = (" " + elem.className + " ").replace(rclass, " "); - for ( var c = 0, cl = classNames.length; c < cl; c++ ) { - className = className.replace(" " + classNames[c] + " ", " "); - } - elem.className = jQuery.trim( className ); - - } else { - elem.className = ""; - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isBool = typeof stateVal === "boolean"; - - if ( jQuery.isFunction( value ) ) { - return this.each(function(i) { - var self = jQuery(this); - self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - state = stateVal, - classNames = value.split( rspaces ); - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space seperated list - state = isBool ? state : !self.hasClass( className ); - self[ state ? "addClass" : "removeClass" ]( className ); - } - - } else if ( type === "undefined" || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // toggle whole className - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " "; - for ( var i = 0, l = this.length; i < l; i++ ) { - if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - if ( !arguments.length ) { - var elem = this[0]; - - if ( elem ) { - if ( jQuery.nodeName( elem, "option" ) ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } - - // We need to handle select boxes special - if ( jQuery.nodeName( elem, "select" ) ) { - var index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } - - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; - - // Don't return options that are disabled or in a disabled optgroup - if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && - (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { - - // Get the specific value for the option - value = jQuery(option).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - } - - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { - return elem.getAttribute("value") === null ? "on" : elem.value; - } - - // Everything else, we just grab the value - return (elem.value || "").replace(rreturn, ""); - - } - - return undefined; - } - - var isFunction = jQuery.isFunction(value); - - return this.each(function(i) { - var self = jQuery(this), val = value; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call(this, i, self.val()); - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray(val) ) { - val = jQuery.map(val, function (value) { - return value == null ? "" : value + ""; - }); - } - - if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { - this.checked = jQuery.inArray( self.val(), val ) >= 0; - - } else if ( jQuery.nodeName( this, "select" ) ) { - var values = jQuery.makeArray(val); - - jQuery( "option", this ).each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - this.selectedIndex = -1; - } - - } else { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - attrFn: { - val: true, - css: true, - html: true, - text: true, - data: true, - width: true, - height: true, - offset: true - }, - - attr: function( elem, name, value, pass ) { - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || elem.nodeType === 2 ) { - return undefined; - } - - if ( pass && name in jQuery.attrFn ) { - return jQuery(elem)[name](value); - } - - var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), - // Whether we are setting (or getting) - set = value !== undefined; - - // Try to normalize/fix the name - name = notxml && jQuery.props[ name ] || name; - - // Only do all the following if this is a node (faster for style) - if ( elem.nodeType === 1 ) { - // These attributes require special treatment - var special = rspecialurl.test( name ); - - // Safari mis-reports the default selected property of an option - // Accessing the parent's selectedIndex property fixes it - if ( name === "selected" && !jQuery.support.optSelected ) { - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - - // If applicable, access the attribute via the DOM 0 way - // 'in' checks fail in Blackberry 4.7 #6931 - if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { - if ( set ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } - - if ( value === null ) { - if ( elem.nodeType === 1 ) { - elem.removeAttribute( name ); - } - - } else { - elem[ name ] = value; - } - } - - // browsers index elements by id/name on forms, give priority to attributes. - if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { - return elem.getAttributeNode( name ).nodeValue; - } - - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - if ( name === "tabIndex" ) { - var attributeNode = elem.getAttributeNode( "tabIndex" ); - - return attributeNode && attributeNode.specified ? - attributeNode.value : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - - return elem[ name ]; - } - - if ( !jQuery.support.style && notxml && name === "style" ) { - if ( set ) { - elem.style.cssText = "" + value; - } - - return elem.style.cssText; - } - - if ( set ) { - // convert the value to a string (all browsers do this but IE) see #1070 - elem.setAttribute( name, "" + value ); - } - - // Ensure that missing attributes return undefined - // Blackberry 4.7 returns "" from getAttribute #6938 - if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { - return undefined; - } - - var attr = !jQuery.support.hrefNormalized && notxml && special ? - // Some attributes require a special call on IE - elem.getAttribute( name, 2 ) : - elem.getAttribute( name ); - - // Non-existent attributes return null, we normalize to undefined - return attr === null ? undefined : attr; - } - // Handle everything which isn't a DOM element node - if ( set ) { - elem[ name ] = value; - } - return elem[ name ]; - } -}); - - - - -var rnamespaces = /\.(.*)$/, - rformElems = /^(?:textarea|input|select)$/i, - rperiod = /\./g, - rspace = / /g, - rescape = /[^\w\s.|`]/g, - fcleanup = function( nm ) { - return nm.replace(rescape, "\\$&"); - }, - eventKey = "events"; - -/* - * A number of helper functions used for managing events. - * Many of the ideas behind this code originated from - * Dean Edwards' addEvent library. - */ -jQuery.event = { - - // Bind an event to an element - // Original by Dean Edwards - add: function( elem, types, handler, data ) { - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // For whatever reason, IE has trouble passing the window object - // around, causing it to be cloned in the process - if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) { - elem = window; - } - - if ( handler === false ) { - handler = returnFalse; - } else if ( !handler ) { - // Fixes bug #7229. Fix recommended by jdalton - return; - } - - var handleObjIn, handleObj; - - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - } - - // Make sure that the function being executed has a unique ID - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure - var elemData = jQuery._data( elem ); - - // If no elemData is found then we must be trying to bind to one of the - // banned noData elements - if ( !elemData ) { - return; - } - - var events = elemData[ eventKey ], - eventHandle = elemData.handle; - - if ( typeof events === "function" ) { - // On plain objects events is a fn that holds the the data - // which prevents this data from being JSON serialized - // the function does not need to be called, it just contains the data - eventHandle = events.handle; - events = events.events; - - } else if ( !events ) { - if ( !elem.nodeType ) { - // On plain objects, create a fn that acts as the holder - // of the values to avoid JSON serialization of event data - elemData[ eventKey ] = elemData = function(){}; - } - - elemData.events = events = {}; - } - - if ( !eventHandle ) { - elemData.handle = eventHandle = function() { - // Handle the second event of a trigger and when - // an event is called after a page has unloaded - return typeof jQuery !== "undefined" && !jQuery.event.triggered ? - jQuery.event.handle.apply( eventHandle.elem, arguments ) : - undefined; - }; - } - - // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native events in IE. - eventHandle.elem = elem; - - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - types = types.split(" "); - - var type, i = 0, namespaces; - - while ( (type = types[ i++ ]) ) { - handleObj = handleObjIn ? - jQuery.extend({}, handleObjIn) : - { handler: handler, data: data }; - - // Namespaced event handlers - if ( type.indexOf(".") > -1 ) { - namespaces = type.split("."); - type = namespaces.shift(); - handleObj.namespace = namespaces.slice(0).sort().join("."); - - } else { - namespaces = []; - handleObj.namespace = ""; - } - - handleObj.type = type; - if ( !handleObj.guid ) { - handleObj.guid = handler.guid; - } - - // Get the current list of functions bound to this event - var handlers = events[ type ], - special = jQuery.event.special[ type ] || {}; - - // Init the event handler queue - if ( !handlers ) { - handlers = events[ type ] = []; - - // Check for a special event handler - // Only use addEventListener/attachEvent if the special - // events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add the function to the element's handler list - handlers.push( handleObj ); - - // Keep track of which events have been used, for global triggering - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - global: {}, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, pos ) { - // don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - if ( handler === false ) { - handler = returnFalse; - } - - var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ), - events = elemData && elemData[ eventKey ]; - - if ( !elemData || !events ) { - return; - } - - if ( typeof events === "function" ) { - elemData = events; - events = events.events; - } - - // types is actually an event object here - if ( types && types.type ) { - handler = types.handler; - types = types.type; - } - - // Unbind all events for the element - if ( !types || typeof types === "string" && types.charAt(0) === "." ) { - types = types || ""; - - for ( type in events ) { - jQuery.event.remove( elem, type + types ); - } - - return; - } - - // Handle multiple events separated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - types = types.split(" "); - - while ( (type = types[ i++ ]) ) { - origType = type; - handleObj = null; - all = type.indexOf(".") < 0; - namespaces = []; - - if ( !all ) { - // Namespaced event handlers - namespaces = type.split("."); - type = namespaces.shift(); - - namespace = new RegExp("(^|\\.)" + - jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - eventType = events[ type ]; - - if ( !eventType ) { - continue; - } - - if ( !handler ) { - for ( j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( all || namespace.test( handleObj.namespace ) ) { - jQuery.event.remove( elem, origType, handleObj.handler, j ); - eventType.splice( j--, 1 ); - } - } - - continue; - } - - special = jQuery.event.special[ type ] || {}; - - for ( j = pos || 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( handler.guid === handleObj.guid ) { - // remove the given handler for the given type - if ( all || namespace.test( handleObj.namespace ) ) { - if ( pos == null ) { - eventType.splice( j--, 1 ); - } - - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - - if ( pos != null ) { - break; - } - } - } - - // remove generic event handler if no more handlers exist - if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { - if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - ret = null; - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - var handle = elemData.handle; - if ( handle ) { - handle.elem = null; - } - - delete elemData.events; - delete elemData.handle; - - if ( typeof elemData === "function" ) { - jQuery.removeData( elem, eventKey, true ); - - } else if ( jQuery.isEmptyObject( elemData ) ) { - jQuery.removeData( elem, undefined, true ); - } - } - }, - - // bubbling is internal - trigger: function( event, data, elem /*, bubbling */ ) { - // Event object or event type - var type = event.type || event, - bubbling = arguments[3]; - - if ( !bubbling ) { - event = typeof event === "object" ? - // jQuery.Event object - event[ jQuery.expando ] ? event : - // Object literal - jQuery.extend( jQuery.Event(type), event ) : - // Just the event type (string) - jQuery.Event(type); - - if ( type.indexOf("!") >= 0 ) { - event.type = type = type.slice(0, -1); - event.exclusive = true; - } - - // Handle a global trigger - if ( !elem ) { - // Don't bubble custom events when global (to avoid too much overhead) - event.stopPropagation(); - - // Only trigger if we've ever bound an event for it - if ( jQuery.event.global[ type ] ) { - // XXX This code smells terrible. event.js should not be directly - // inspecting the data cache - jQuery.each( jQuery.cache, function() { - // internalKey variable is just used to make it easier to find - // and potentially change this stuff later; currently it just - // points to jQuery.expando - var internalKey = jQuery.expando, - internalCache = this[ internalKey ]; - if ( internalCache && internalCache.events && internalCache.events[type] ) { - jQuery.event.trigger( event, data, internalCache.handle.elem ); - } - }); - } - } - - // Handle triggering a single element - - // don't do events on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { - return undefined; - } - - // Clean up in case it is reused - event.result = undefined; - event.target = elem; - - // Clone the incoming data, if any - data = jQuery.makeArray( data ); - data.unshift( event ); - } - - event.currentTarget = elem; - - // Trigger the event, it is assumed that "handle" is a function - var handle = elem.nodeType ? - jQuery._data( elem, "handle" ) : - (jQuery._data( elem, eventKey ) || {}).handle; - - if ( handle ) { - handle.apply( elem, data ); - } - - var parent = elem.parentNode || elem.ownerDocument; - - // Trigger an inline bound script - try { - if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { - if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { - event.result = false; - event.preventDefault(); - } - } - - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (inlineError) {} - - if ( !event.isPropagationStopped() && parent ) { - jQuery.event.trigger( event, data, parent, true ); - - } else if ( !event.isDefaultPrevented() ) { - var old, - target = event.target, - targetType = type.replace( rnamespaces, "" ), - isClick = jQuery.nodeName( target, "a" ) && targetType === "click", - special = jQuery.event.special[ targetType ] || {}; - - if ( (!special._default || special._default.call( elem, event ) === false) && - !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { - - try { - if ( target[ targetType ] ) { - // Make sure that we don't accidentally re-trigger the onFOO events - old = target[ "on" + targetType ]; - - if ( old ) { - target[ "on" + targetType ] = null; - } - - jQuery.event.triggered = true; - target[ targetType ](); - } - - // prevent IE from throwing an error for some elements with some event types, see #3533 - } catch (triggerError) {} - - if ( old ) { - target[ "on" + targetType ] = old; - } - - jQuery.event.triggered = false; - } - } - }, - - handle: function( event ) { - var all, handlers, namespaces, namespace_re, events, - namespace_sort = [], - args = jQuery.makeArray( arguments ); - - event = args[0] = jQuery.event.fix( event || window.event ); - event.currentTarget = this; - - // Namespaced event handlers - all = event.type.indexOf(".") < 0 && !event.exclusive; - - if ( !all ) { - namespaces = event.type.split("."); - event.type = namespaces.shift(); - namespace_sort = namespaces.slice(0).sort(); - namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - event.namespace = event.namespace || namespace_sort.join("."); - - events = jQuery._data(this, eventKey); - - if ( typeof events === "function" ) { - events = events.events; - } - - handlers = (events || {})[ event.type ]; - - if ( events && handlers ) { - // Clone the handlers to prevent manipulation - handlers = handlers.slice(0); - - for ( var j = 0, l = handlers.length; j < l; j++ ) { - var handleObj = handlers[ j ]; - - // Filter the functions by class - if ( all || namespace_re.test( handleObj.namespace ) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handleObj.handler; - event.data = handleObj.data; - event.handleObj = handleObj; - - var ret = handleObj.handler.apply( this, args ); - - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - - if ( event.isImmediatePropagationStopped() ) { - break; - } - } - } - } - - return event.result; - }, - - props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // store a copy of the original event object - // and "clone" to set read-only properties - var originalEvent = event; - event = jQuery.Event( originalEvent ); - - for ( var i = this.props.length, prop; i; ) { - prop = this.props[ --i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Fix target property, if necessary - if ( !event.target ) { - // Fixes #1925 where srcElement might not be defined either - event.target = event.srcElement || document; - } - - // check if target is a textnode (safari) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && event.fromElement ) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; - } - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && event.clientX != null ) { - var doc = document.documentElement, - body = document.body; - - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Add which for key events - if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { - event.which = event.charCode != null ? event.charCode : event.keyCode; - } - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if ( !event.metaKey && event.ctrlKey ) { - event.metaKey = event.ctrlKey; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && event.button !== undefined ) { - event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); - } - - return event; - }, - - // Deprecated, use jQuery.guid instead - guid: 1E8, - - // Deprecated, use jQuery.proxy instead - proxy: jQuery.proxy, - - special: { - ready: { - // Make sure the ready event is setup - setup: jQuery.bindReady, - teardown: jQuery.noop - }, - - live: { - add: function( handleObj ) { - jQuery.event.add( this, - liveConvert( handleObj.origType, handleObj.selector ), - jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); - }, - - remove: function( handleObj ) { - jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); - } - }, - - beforeunload: { - setup: function( data, namespaces, eventHandle ) { - // We only want to do this special case on windows - if ( jQuery.isWindow( this ) ) { - this.onbeforeunload = eventHandle; - } - }, - - teardown: function( namespaces, eventHandle ) { - if ( this.onbeforeunload === eventHandle ) { - this.onbeforeunload = null; - } - } - } - } -}; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - if ( elem.detachEvent ) { - elem.detachEvent( "on" + type, handle ); - } - }; - -jQuery.Event = function( src ) { - // Allow instantiation without the 'new' keyword - if ( !this.preventDefault ) { - return new jQuery.Event( src ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse; - - // Event type - } else { - this.type = src; - } - - // timeStamp is buggy for some events on Firefox(#3843) - // So we won't rely on the native value - this.timeStamp = jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -function returnFalse() { - return false; -} -function returnTrue() { - return true; -} - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - preventDefault: function() { - this.isDefaultPrevented = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - - // if preventDefault exists run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // otherwise set the returnValue property of the original event to false (IE) - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - this.isPropagationStopped = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - // if stopPropagation exists run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - // otherwise set the cancelBubble property of the original event to true (IE) - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - }, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse -}; - -// Checks if an event happened on an element within another element -// Used in jQuery.event.special.mouseenter and mouseleave handlers -var withinElement = function( event ) { - // Check if mouse(over|out) are still within the same parent element - var parent = event.relatedTarget; - - // Firefox sometimes assigns relatedTarget a XUL element - // which we cannot access the parentNode property of - try { - // Traverse up the tree - while ( parent && parent !== this ) { - parent = parent.parentNode; - } - - if ( parent !== this ) { - // set the correct event type - event.type = event.data; - - // handle event if we actually just moused on to a non sub-element - jQuery.event.handle.apply( this, arguments ); - } - - // assuming we've left the element since we most likely mousedover a xul element - } catch(e) { } -}, - -// In case of event delegation, we only need to rename the event.type, -// liveHandler will take care of the rest. -delegate = function( event ) { - event.type = event.data; - jQuery.event.handle.apply( this, arguments ); -}; - -// Create mouseenter and mouseleave events -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - setup: function( data ) { - jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); - }, - teardown: function( data ) { - jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); - } - }; -}); - -// submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function( data, namespaces ) { - if ( this.nodeName && this.nodeName.toLowerCase() !== "form" ) { - jQuery.event.add(this, "click.specialSubmit", function( e ) { - var elem = e.target, - type = elem.type; - - if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { - e.liveFired = undefined; - return trigger( "submit", this, arguments ); - } - }); - - jQuery.event.add(this, "keypress.specialSubmit", function( e ) { - var elem = e.target, - type = elem.type; - - if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { - e.liveFired = undefined; - return trigger( "submit", this, arguments ); - } - }); - - } else { - return false; - } - }, - - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialSubmit" ); - } - }; - -} - -// change delegation, happens here so we have bind. -if ( !jQuery.support.changeBubbles ) { - - var changeFilters, - - getVal = function( elem ) { - var type = elem.type, val = elem.value; - - if ( type === "radio" || type === "checkbox" ) { - val = elem.checked; - - } else if ( type === "select-multiple" ) { - val = elem.selectedIndex > -1 ? - jQuery.map( elem.options, function( elem ) { - return elem.selected; - }).join("-") : - ""; - - } else if ( elem.nodeName.toLowerCase() === "select" ) { - val = elem.selectedIndex; - } - - return val; - }, - - testChange = function testChange( e ) { - var elem = e.target, data, val; - - if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { - return; - } - - data = jQuery._data( elem, "_change_data" ); - val = getVal(elem); - - // the current data will be also retrieved by beforeactivate - if ( e.type !== "focusout" || elem.type !== "radio" ) { - jQuery._data( elem, "_change_data", val ); - } - - if ( data === undefined || val === data ) { - return; - } - - if ( data != null || val ) { - e.type = "change"; - e.liveFired = undefined; - return jQuery.event.trigger( e, arguments[1], elem ); - } - }; - - jQuery.event.special.change = { - filters: { - focusout: testChange, - - beforedeactivate: testChange, - - click: function( e ) { - var elem = e.target, type = elem.type; - - if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { - return testChange.call( this, e ); - } - }, - - // Change has to be called before submit - // Keydown will be called before keypress, which is used in submit-event delegation - keydown: function( e ) { - var elem = e.target, type = elem.type; - - if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || - (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || - type === "select-multiple" ) { - return testChange.call( this, e ); - } - }, - - // Beforeactivate happens also before the previous element is blurred - // with this event you can't trigger a change event, but you can store - // information - beforeactivate: function( e ) { - var elem = e.target; - jQuery._data( elem, "_change_data", getVal(elem) ); - } - }, - - setup: function( data, namespaces ) { - if ( this.type === "file" ) { - return false; - } - - for ( var type in changeFilters ) { - jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); - } - - return rformElems.test( this.nodeName ); - }, - - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialChange" ); - - return rformElems.test( this.nodeName ); - } - }; - - changeFilters = jQuery.event.special.change.filters; - - // Handle when the input is .focus()'d - changeFilters.focus = changeFilters.beforeactivate; -} - -function trigger( type, elem, args ) { - args[0].type = type; - return jQuery.event.handle.apply( elem, args ); -} - -// Create "bubbling" focus and blur events -if ( document.addEventListener ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - jQuery.event.special[ fix ] = { - setup: function() { - this.addEventListener( orig, handler, true ); - }, - teardown: function() { - this.removeEventListener( orig, handler, true ); - } - }; - - function handler( e ) { - e = jQuery.event.fix( e ); - e.type = fix; - return jQuery.event.handle.call( this, e ); - } - }); -} - -jQuery.each(["bind", "one"], function( i, name ) { - jQuery.fn[ name ] = function( type, data, fn ) { - // Handle object literals - if ( typeof type === "object" ) { - for ( var key in type ) { - this[ name ](key, data, type[key], fn); - } - return this; - } - - if ( jQuery.isFunction( data ) || data === false ) { - fn = data; - data = undefined; - } - - var handler = name === "one" ? jQuery.proxy( fn, function( event ) { - jQuery( this ).unbind( event, handler ); - return fn.apply( this, arguments ); - }) : fn; - - if ( type === "unload" && name !== "one" ) { - this.one( type, data, fn ); - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.add( this[i], type, handler, data ); - } - } - - return this; - }; -}); - -jQuery.fn.extend({ - unbind: function( type, fn ) { - // Handle object literals - if ( typeof type === "object" && !type.preventDefault ) { - for ( var key in type ) { - this.unbind(key, type[key]); - } - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.remove( this[i], type, fn ); - } - } - - return this; - }, - - delegate: function( selector, types, data, fn ) { - return this.live( types, data, fn, selector ); - }, - - undelegate: function( selector, types, fn ) { - if ( arguments.length === 0 ) { - return this.unbind( "live" ); - - } else { - return this.die( types, null, fn, selector ); - } - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - - triggerHandler: function( type, data ) { - if ( this[0] ) { - var event = jQuery.Event( type ); - event.preventDefault(); - event.stopPropagation(); - jQuery.event.trigger( event, data, this[0] ); - return event.result; - } - }, - - toggle: function( fn ) { - // Save reference to arguments for access in closure - var args = arguments, - i = 1; - - // link all the functions, so any of them can unbind this click handler - while ( i < args.length ) { - jQuery.proxy( fn, args[ i++ ] ); - } - - return this.click( jQuery.proxy( fn, function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - })); - }, - - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -}); - -var liveMap = { - focus: "focusin", - blur: "focusout", - mouseenter: "mouseover", - mouseleave: "mouseout" -}; - -jQuery.each(["live", "die"], function( i, name ) { - jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { - var type, i = 0, match, namespaces, preType, - selector = origSelector || this.selector, - context = origSelector ? this : jQuery( this.context ); - - if ( typeof types === "object" && !types.preventDefault ) { - for ( var key in types ) { - context[ name ]( key, data, types[key], selector ); - } - - return this; - } - - if ( jQuery.isFunction( data ) ) { - fn = data; - data = undefined; - } - - types = (types || "").split(" "); - - while ( (type = types[ i++ ]) != null ) { - match = rnamespaces.exec( type ); - namespaces = ""; - - if ( match ) { - namespaces = match[0]; - type = type.replace( rnamespaces, "" ); - } - - if ( type === "hover" ) { - types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); - continue; - } - - preType = type; - - if ( type === "focus" || type === "blur" ) { - types.push( liveMap[ type ] + namespaces ); - type = type + namespaces; - - } else { - type = (liveMap[ type ] || type) + namespaces; - } - - if ( name === "live" ) { - // bind live handler - for ( var j = 0, l = context.length; j < l; j++ ) { - jQuery.event.add( context[j], "live." + liveConvert( type, selector ), - { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); - } - - } else { - // unbind live handler - context.unbind( "live." + liveConvert( type, selector ), fn ); - } - } - - return this; - }; -}); - -function liveHandler( event ) { - var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, - elems = [], - selectors = [], - events = jQuery._data( this, eventKey ); - - if ( typeof events === "function" ) { - events = events.events; - } - - // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911) - if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) { - return; - } - - if ( event.namespace ) { - namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - event.liveFired = this; - - var live = events.live.slice(0); - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { - selectors.push( handleObj.selector ); - - } else { - live.splice( j--, 1 ); - } - } - - match = jQuery( event.target ).closest( selectors, event.currentTarget ); - - for ( i = 0, l = match.length; i < l; i++ ) { - close = match[i]; - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) { - elem = close.elem; - related = null; - - // Those two events require additional checking - if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { - event.type = handleObj.preType; - related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; - } - - if ( !related || related !== elem ) { - elems.push({ elem: elem, handleObj: handleObj, level: close.level }); - } - } - } - } - - for ( i = 0, l = elems.length; i < l; i++ ) { - match = elems[i]; - - if ( maxLevel && match.level > maxLevel ) { - break; - } - - event.currentTarget = match.elem; - event.data = match.handleObj.data; - event.handleObj = match.handleObj; - - ret = match.handleObj.origHandler.apply( match.elem, arguments ); - - if ( ret === false || event.isPropagationStopped() ) { - maxLevel = match.level; - - if ( ret === false ) { - stop = false; - } - if ( event.isImmediatePropagationStopped() ) { - break; - } - } - } - - return stop; -} - -function liveConvert( type, selector ) { - return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&"); -} - -jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error").split(" "), function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - if ( fn == null ) { - fn = data; - data = null; - } - - return arguments.length > 0 ? - this.bind( name, data, fn ) : - this.trigger( name ); - }; - - if ( jQuery.attrFn ) { - jQuery.attrFn[ name ] = true; - } -}); - - -/*! - * Sizzle CSS Selector Engine - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ - */ -(function(){ - -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true; - -// Here we check if the JavaScript engine is using some sort of -// optimization where it does not always call our comparision -// function. If that is the case, discard the hasDuplicate value. -// Thus far that includes Google Chrome. -[0, 0].sort(function() { - baseHasDuplicate = false; - return 0; -}); - -var Sizzle = function( selector, context, results, seed ) { - results = results || []; - context = context || document; - - var origContext = context; - - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - var m, set, checkSet, extra, ret, cur, pop, i, - prune = true, - contextXML = Sizzle.isXML( context ), - parts = [], - soFar = selector; - - // Reset the position of the chunker regexp (start from head) - do { - chunker.exec( "" ); - m = chunker.exec( soFar ); - - if ( m ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - } while ( m ); - - if ( parts.length > 1 && origPOS.exec( selector ) ) { - - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); - - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); - - while ( parts.length ) { - selector = parts.shift(); - - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); - } - - set = posProcess( selector, set ); - } - } - - } else { - // Take a shortcut and set the context if the root selector is an ID - // (but not if it'll be faster if the inner selector is an ID) - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - - ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? - Sizzle.filter( ret.expr, ret.set )[0] : - ret.set[0]; - } - - if ( context ) { - ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - - set = ret.expr ? - Sizzle.filter( ret.expr, ret.set ) : - ret.set; - - if ( parts.length > 0 ) { - checkSet = makeArray( set ); - - } else { - prune = false; - } - - while ( parts.length ) { - cur = parts.pop(); - pop = cur; - - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } - - if ( pop == null ) { - pop = context; - } - - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - - } else { - checkSet = parts = []; - } - } - - if ( !checkSet ) { - checkSet = set; - } - - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } - - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - - } else if ( context && context.nodeType === 1 ) { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { - results.push( set[i] ); - } - } - - } else { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } - } - } - - } else { - makeArray( checkSet, results ); - } - - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } - - return results; -}; - -Sizzle.uniqueSort = function( results ) { - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort( sortOrder ); - - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[ i - 1 ] ) { - results.splice( i--, 1 ); - } - } - } - } - - return results; -}; - -Sizzle.matches = function( expr, set ) { - return Sizzle( expr, null, null, set ); -}; - -Sizzle.matchesSelector = function( node, expr ) { - return Sizzle( expr, null, null, [node] ).length > 0; -}; - -Sizzle.find = function( expr, context, isXML ) { - var set; - - if ( !expr ) { - return []; - } - - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var match, - type = Expr.order[i]; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; - match.splice( 1, 1 ); - - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace(/\\/g, ""); - set = Expr.find[ type ]( match, context, isXML ); - - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } - - if ( !set ) { - set = typeof context.getElementsByTagName !== "undefined" ? - context.getElementsByTagName( "*" ) : - []; - } - - return { set: set, expr: expr }; -}; - -Sizzle.filter = function( expr, set, inplace, not ) { - var match, anyFound, - old = expr, - result = [], - curLoop = set, - isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); - - while ( expr && set.length ) { - for ( var type in Expr.filter ) { - if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var found, item, - filter = Expr.filter[ type ], - left = match[1]; - - anyFound = false; - - match.splice(1,1); - - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } - - if ( curLoop === result ) { - result = []; - } - - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - - if ( !match ) { - anyFound = found = true; - - } else if ( match === true ) { - continue; - } - } - - if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; - - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - - } else { - curLoop[i] = false; - } - - } else if ( pass ) { - result.push( item ); - anyFound = true; - } - } - } - } - - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } - - expr = expr.replace( Expr.match[ type ], "" ); - - if ( !anyFound ) { - return []; - } - - break; - } - } - } - - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - - } else { - break; - } - } - - old = expr; - } - - return curLoop; -}; - -Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; -}; - -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - - match: { - ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ - }, - - leftMatch: {}, - - attrMap: { - "class": "className", - "for": "htmlFor" - }, - - attrHandle: { - href: function( elem ) { - return elem.getAttribute( "href" ); - } - }, - - relative: { - "+": function(checkSet, part){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !/\W/.test( part ), - isPartStrNotTag = isPartStr && !isTag; - - if ( isTag ) { - part = part.toLowerCase(); - } - - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; - } - } - - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } - }, - - ">": function( checkSet, part ) { - var elem, - isPartStr = typeof part === "string", - i = 0, - l = checkSet.length; - - if ( isPartStr && !/\W/.test( part ) ) { - part = part.toLowerCase(); - - for ( ; i < l; i++ ) { - elem = checkSet[i]; - - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; - } - } - - } else { - for ( ; i < l; i++ ) { - elem = checkSet[i]; - - if ( elem ) { - checkSet[i] = isPartStr ? - elem.parentNode : - elem.parentNode === part; - } - } - - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); - } - } - }, - - "": function(checkSet, part, isXML){ - var nodeCheck, - doneName = done++, - checkFn = dirCheck; - - if ( typeof part === "string" && !/\W/.test(part) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } - - checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); - }, - - "~": function( checkSet, part, isXML ) { - var nodeCheck, - doneName = done++, - checkFn = dirCheck; - - if ( typeof part === "string" && !/\W/.test( part ) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } - - checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); - } - }, - - find: { - ID: function( match, context, isXML ) { - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }, - - NAME: function( match, context ) { - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], - results = context.getElementsByName( match[1] ); - - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); - } - } - - return ret.length === 0 ? null : ret; - } - }, - - TAG: function( match, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( match[1] ); - } - } - }, - preFilter: { - CLASS: function( match, curLoop, inplace, result, not, isXML ) { - match = " " + match[1].replace(/\\/g, "") + " "; - - if ( isXML ) { - return match; - } - - for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { - if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { - if ( !inplace ) { - result.push( elem ); - } - - } else if ( inplace ) { - curLoop[i] = false; - } - } - } - - return false; - }, - - ID: function( match ) { - return match[1].replace(/\\/g, ""); - }, - - TAG: function( match, curLoop ) { - return match[1].toLowerCase(); - }, - - CHILD: function( match ) { - if ( match[1] === "nth" ) { - if ( !match[2] ) { - Sizzle.error( match[0] ); - } - - match[2] = match[2].replace(/^\+|\s*/g, ''); - - // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( - match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - - // calculate the numbers (first)n+(last) including if they are negative - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } - else if ( match[2] ) { - Sizzle.error( match[0] ); - } - - // TODO: Move to normal caching system - match[0] = done++; - - return match; - }, - - ATTR: function( match, curLoop, inplace, result, not, isXML ) { - var name = match[1] = match[1].replace(/\\/g, ""); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; - } - - // Handle if an un-quoted value was used - match[4] = ( match[4] || match[5] || "" ).replace(/\\/g, ""); - - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; - } - - return match; - }, - - PSEUDO: function( match, curLoop, inplace, result, not ) { - if ( match[1] === "not" ) { - // If we're dealing with a complex expression, or a simple one - if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { - match[3] = Sizzle(match[3], null, null, curLoop); - - } else { - var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); - - if ( !inplace ) { - result.push.apply( result, ret ); - } - - return false; - } - - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; - } - - return match; - }, - - POS: function( match ) { - match.unshift( true ); - - return match; - } - }, - - filters: { - enabled: function( elem ) { - return elem.disabled === false && elem.type !== "hidden"; - }, - - disabled: function( elem ) { - return elem.disabled === true; - }, - - checked: function( elem ) { - return elem.checked === true; - }, - - selected: function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - elem.parentNode.selectedIndex; - - return elem.selected === true; - }, - - parent: function( elem ) { - return !!elem.firstChild; - }, - - empty: function( elem ) { - return !elem.firstChild; - }, - - has: function( elem, i, match ) { - return !!Sizzle( match[3], elem ).length; - }, - - header: function( elem ) { - return (/h\d/i).test( elem.nodeName ); - }, - - text: function( elem ) { - return "text" === elem.type; - }, - radio: function( elem ) { - return "radio" === elem.type; - }, - - checkbox: function( elem ) { - return "checkbox" === elem.type; - }, - - file: function( elem ) { - return "file" === elem.type; - }, - password: function( elem ) { - return "password" === elem.type; - }, - - submit: function( elem ) { - return "submit" === elem.type; - }, - - image: function( elem ) { - return "image" === elem.type; - }, - - reset: function( elem ) { - return "reset" === elem.type; - }, - - button: function( elem ) { - return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; - }, - - input: function( elem ) { - return (/input|select|textarea|button/i).test( elem.nodeName ); - } - }, - setFilters: { - first: function( elem, i ) { - return i === 0; - }, - - last: function( elem, i, match, array ) { - return i === array.length - 1; - }, - - even: function( elem, i ) { - return i % 2 === 0; - }, - - odd: function( elem, i ) { - return i % 2 === 1; - }, - - lt: function( elem, i, match ) { - return i < match[3] - 0; - }, - - gt: function( elem, i, match ) { - return i > match[3] - 0; - }, - - nth: function( elem, i, match ) { - return match[3] - 0 === i; - }, - - eq: function( elem, i, match ) { - return match[3] - 0 === i; - } - }, - filter: { - PSEUDO: function( elem, match, i, array ) { - var name = match[1], - filter = Expr.filters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; - - } else if ( name === "not" ) { - var not = match[3]; - - for ( var j = 0, l = not.length; j < l; j++ ) { - if ( not[j] === elem ) { - return false; - } - } - - return true; - - } else { - Sizzle.error( name ); - } - }, - - CHILD: function( elem, match ) { - var type = match[1], - node = elem; - - switch ( type ) { - case "only": - case "first": - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - - if ( type === "first" ) { - return true; - } - - node = elem; - - case "last": - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - - return true; - - case "nth": - var first = match[2], - last = match[3]; - - if ( first === 1 && last === 0 ) { - return true; - } - - var doneName = match[0], - parent = elem.parentNode; - - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; - - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - - parent.sizcache = doneName; - } - - var diff = elem.nodeIndex - last; - - if ( first === 0 ) { - return diff === 0; - - } else { - return ( diff % first === 0 && diff / first >= 0 ); - } - } - }, - - ID: function( elem, match ) { - return elem.nodeType === 1 && elem.getAttribute("id") === match; - }, - - TAG: function( elem, match ) { - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; - }, - - CLASS: function( elem, match ) { - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; - }, - - ATTR: function( elem, match ) { - var name = match[1], - result = Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - - return result == null ? - type === "!=" : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value !== check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; - }, - - POS: function( elem, match, i, array ) { - var name = match[2], - filter = Expr.setFilters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } - } - } -}; - -var origPOS = Expr.match.POS, - fescape = function(all, num){ - return "\\" + (num - 0 + 1); - }; - -for ( var type in Expr.match ) { - Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); - Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); -} - -var makeArray = function( array, results ) { - array = Array.prototype.slice.call( array, 0 ); - - if ( results ) { - results.push.apply( results, array ); - return results; - } - - return array; -}; - -// Perform a simple check to determine if the browser is capable of -// converting a NodeList to an array using builtin methods. -// Also verifies that the returned array holds DOM nodes -// (which is not the case in the Blackberry browser) -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; - -// Provide a fallback method if it does not work -} catch( e ) { - makeArray = function( array, results ) { - var i = 0, - ret = results || []; - - if ( toString.call(array) === "[object Array]" ) { - Array.prototype.push.apply( ret, array ); - - } else { - if ( typeof array.length === "number" ) { - for ( var l = array.length; i < l; i++ ) { - ret.push( array[i] ); - } - - } else { - for ( ; array[i]; i++ ) { - ret.push( array[i] ); - } - } - } - - return ret; - }; -} - -var sortOrder, siblingCheck; - -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - return a.compareDocumentPosition ? -1 : 1; - } - - return a.compareDocumentPosition(b) & 4 ? -1 : 1; - }; - -} else { - sortOrder = function( a, b ) { - var al, bl, - ap = [], - bp = [], - aup = a.parentNode, - bup = b.parentNode, - cur = aup; - - // The nodes are identical, we can exit early - if ( a === b ) { - hasDuplicate = true; - return 0; - - // If the nodes are siblings (or identical) we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - - // If no parents were found then the nodes are disconnected - } else if ( !aup ) { - return -1; - - } else if ( !bup ) { - return 1; - } - - // Otherwise they're somewhere else in the tree so we need - // to build up a full list of the parentNodes for comparison - while ( cur ) { - ap.unshift( cur ); - cur = cur.parentNode; - } - - cur = bup; - - while ( cur ) { - bp.unshift( cur ); - cur = cur.parentNode; - } - - al = ap.length; - bl = bp.length; - - // Start walking down the tree looking for a discrepancy - for ( var i = 0; i < al && i < bl; i++ ) { - if ( ap[i] !== bp[i] ) { - return siblingCheck( ap[i], bp[i] ); - } - } - - // We ended someplace up the tree so do a sibling check - return i === al ? - siblingCheck( a, bp[i], -1 ) : - siblingCheck( ap[i], b, 1 ); - }; - - siblingCheck = function( a, b, ret ) { - if ( a === b ) { - return ret; - } - - var cur = a.nextSibling; - - while ( cur ) { - if ( cur === b ) { - return -1; - } - - cur = cur.nextSibling; - } - - return 1; - }; -} - -// Utility function for retreiving the text value of an array of DOM nodes -Sizzle.getText = function( elems ) { - var ret = "", elem; - - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += Sizzle.getText( elem.childNodes ); - } - } - - return ret; -}; - -// Check to see if the browser returns elements by name when -// querying by getElementById (and provide a workaround) -(function(){ - // We're going to inject a fake input element with a specified name - var form = document.createElement("div"), - id = "script" + (new Date()).getTime(), - root = document.documentElement; - - form.innerHTML = ""; - - // Inject it into the root element, check its status, and remove it quickly - root.insertBefore( form, root.firstChild ); - - // The workaround has to do additional checks after a getElementById - // Which slows things down for other browsers (hence the branching) - if ( document.getElementById( id ) ) { - Expr.find.ID = function( match, context, isXML ) { - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - - return m ? - m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? - [m] : - undefined : - []; - } - }; - - Expr.filter.ID = function( elem, match ) { - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } - - root.removeChild( form ); - - // release memory in IE - root = form = null; -})(); - -(function(){ - // Check to see if the browser returns only elements - // when doing getElementsByTagName("*") - - // Create a fake element - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); - - // Make sure no comments are found - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function( match, context ) { - var results = context.getElementsByTagName( match[1] ); - - // Filter out possible comments - if ( match[1] === "*" ) { - var tmp = []; - - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); - } - } - - results = tmp; - } - - return results; - }; - } - - // Check to see if an attribute returns normalized href attributes - div.innerHTML = ""; - - if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && - div.firstChild.getAttribute("href") !== "#" ) { - - Expr.attrHandle.href = function( elem ) { - return elem.getAttribute( "href", 2 ); - }; - } - - // release memory in IE - div = null; -})(); - -if ( document.querySelectorAll ) { - (function(){ - var oldSizzle = Sizzle, - div = document.createElement("div"), - id = "__sizzle__"; - - div.innerHTML = "

"; - - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function( query, context, extra, seed ) { - context = context || document; - - // Only use querySelectorAll on non-XML documents - // (ID selectors don't work in non-HTML documents) - if ( !seed && !Sizzle.isXML(context) ) { - // See if we find a selector to speed up - var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); - - if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { - // Speed-up: Sizzle("TAG") - if ( match[1] ) { - return makeArray( context.getElementsByTagName( query ), extra ); - - // Speed-up: Sizzle(".CLASS") - } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { - return makeArray( context.getElementsByClassName( match[2] ), extra ); - } - } - - if ( context.nodeType === 9 ) { - // Speed-up: Sizzle("body") - // The body element only exists once, optimize finding it - if ( query === "body" && context.body ) { - return makeArray( [ context.body ], extra ); - - // Speed-up: Sizzle("#ID") - } else if ( match && match[3] ) { - var elem = context.getElementById( match[3] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id === match[3] ) { - return makeArray( [ elem ], extra ); - } - - } else { - return makeArray( [], extra ); - } - } - - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(qsaError) {} - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - var old = context.getAttribute( "id" ), - nid = old || id, - hasParent = context.parentNode, - relativeHierarchySelector = /^\s*[+~]/.test( query ); - - if ( !old ) { - context.setAttribute( "id", nid ); - } else { - nid = nid.replace( /'/g, "\\$&" ); - } - if ( relativeHierarchySelector && hasParent ) { - context = context.parentNode; - } - - try { - if ( !relativeHierarchySelector || hasParent ) { - return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); - } - - } catch(pseudoError) { - } finally { - if ( !old ) { - context.removeAttribute( "id" ); - } - } - } - } - - return oldSizzle(query, context, extra, seed); - }; - - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } - - // release memory in IE - div = null; - })(); -} - -(function(){ - var html = document.documentElement, - matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector, - pseudoWorks = false; - - try { - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( document.documentElement, "[test!='']:sizzle" ); - - } catch( pseudoError ) { - pseudoWorks = true; - } - - if ( matches ) { - Sizzle.matchesSelector = function( node, expr ) { - // Make sure that attribute selectors are quoted - expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); - - if ( !Sizzle.isXML( node ) ) { - try { - if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { - return matches.call( node, expr ); - } - } catch(e) {} - } - - return Sizzle(expr, null, null, [node]).length > 0; - }; - } -})(); - -(function(){ - var div = document.createElement("div"); - - div.innerHTML = "
"; - - // Opera can't find a second classname (in 9.6) - // Also, make sure that getElementsByClassName actually exists - if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { - return; - } - - // Safari caches class attributes, doesn't catch changes (in 3.2) - div.lastChild.className = "e"; - - if ( div.getElementsByClassName("e").length === 1 ) { - return; - } - - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function( match, context, isXML ) { - if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { - return context.getElementsByClassName(match[1]); - } - }; - - // release memory in IE - div = null; -})(); - -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - - if ( elem ) { - var match = false; - - elem = elem[dir]; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; - elem.sizset = i; - } - - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - - if ( elem ) { - var match = false; - - elem = elem[dir]; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem.sizcache = doneName; - elem.sizset = i; - } - - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; - } - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -if ( document.documentElement.contains ) { - Sizzle.contains = function( a, b ) { - return a !== b && (a.contains ? a.contains(b) : true); - }; - -} else if ( document.documentElement.compareDocumentPosition ) { - Sizzle.contains = function( a, b ) { - return !!(a.compareDocumentPosition(b) & 16); - }; - -} else { - Sizzle.contains = function() { - return false; - }; -} - -Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -var posProcess = function( selector, context ) { - var match, - tmpSet = [], - later = "", - root = context.nodeType ? [context] : context; - - // Position selectors must be done after the filter - // And so must :not(positional) so we move all PSEUDOs to the end - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); - } - - selector = Expr.relative[selector] ? selector + "*" : selector; - - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); - } - - return Sizzle.filter( later, tmpSet ); -}; - -// EXPOSE -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.filters; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - -})(); - - -var runtil = /Until$/, - rparentsprev = /^(?:parents|prevUntil|prevAll)/, - // Note: This RegExp should be improved, or likely pulled from Sizzle - rmultiselector = /,/, - isSimple = /^.[^:#\[\.,]*$/, - slice = Array.prototype.slice, - POS = jQuery.expr.match.POS, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend({ - find: function( selector ) { - var ret = this.pushStack( "", "find", selector ), - length = 0; - - for ( var i = 0, l = this.length; i < l; i++ ) { - length = ret.length; - jQuery.find( selector, this[i], ret ); - - if ( i > 0 ) { - // Make sure that the results are unique - for ( var n = length; n < ret.length; n++ ) { - for ( var r = 0; r < length; r++ ) { - if ( ret[r] === ret[n] ) { - ret.splice(n--, 1); - break; - } - } - } - } - } - - return ret; - }, - - has: function( target ) { - var targets = jQuery( target ); - return this.filter(function() { - for ( var i = 0, l = targets.length; i < l; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - not: function( selector ) { - return this.pushStack( winnow(this, selector, false), "not", selector); - }, - - filter: function( selector ) { - return this.pushStack( winnow(this, selector, true), "filter", selector ); - }, - - is: function( selector ) { - return !!selector && jQuery.filter( selector, this ).length > 0; - }, - - closest: function( selectors, context ) { - var ret = [], i, l, cur = this[0]; - - if ( jQuery.isArray( selectors ) ) { - var match, selector, - matches = {}, - level = 1; - - if ( cur && selectors.length ) { - for ( i = 0, l = selectors.length; i < l; i++ ) { - selector = selectors[i]; - - if ( !matches[selector] ) { - matches[selector] = jQuery.expr.match.POS.test( selector ) ? - jQuery( selector, context || this.context ) : - selector; - } - } - - while ( cur && cur.ownerDocument && cur !== context ) { - for ( selector in matches ) { - match = matches[selector]; - - if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { - ret.push({ selector: selector, elem: cur, level: level }); - } - } - - cur = cur.parentNode; - level++; - } - } - - return ret; - } - - var pos = POS.test( selectors ) ? - jQuery( selectors, context || this.context ) : null; - - for ( i = 0, l = this.length; i < l; i++ ) { - cur = this[i]; - - while ( cur ) { - if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { - ret.push( cur ); - break; - - } else { - cur = cur.parentNode; - if ( !cur || !cur.ownerDocument || cur === context ) { - break; - } - } - } - } - - ret = ret.length > 1 ? jQuery.unique(ret) : ret; - - return this.pushStack( ret, "closest", selectors ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - if ( !elem || typeof elem === "string" ) { - return jQuery.inArray( this[0], - // If it receives a string, the selector is used - // If it receives nothing, the siblings are used - elem ? jQuery( elem ) : this.parent().children() ); - } - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context ) : - jQuery.makeArray( selector ), - all = jQuery.merge( this.get(), set ); - - return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? - all : - jQuery.unique( all ) ); - }, - - andSelf: function() { - return this.add( this.prevObject ); - } -}); - -// A painfully simple check to see if an element is disconnected -// from a document (should be improved, where feasible). -function isDisconnected( node ) { - return !node || !node.parentNode || node.parentNode.nodeType === 11; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return jQuery.nth( elem, 2, "nextSibling" ); - }, - prev: function( elem ) { - return jQuery.nth( elem, 2, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( elem.parentNode.firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.makeArray( elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ), - // The variable 'args' was introduced in - // https://github.com/jquery/jquery/commit/52a0238 - // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed. - // http://code.google.com/p/v8/issues/detail?id=1050 - args = slice.call(arguments); - - if ( !runtil.test( name ) ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; - - if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - - return this.pushStack( ret, name, args.join(",") ); - }; -}); - -jQuery.extend({ - filter: function( expr, elems, not ) { - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 ? - jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : - jQuery.find.matches(expr, elems); - }, - - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - nth: function( cur, result, dir, elem ) { - result = result || 1; - var num = 0; - - for ( ; cur; cur = cur[dir] ) { - if ( cur.nodeType === 1 && ++num === result ) { - break; - } - } - - return cur; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, keep ) { - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - var retVal = !!qualifier.call( elem, i, elem ); - return retVal === keep; - }); - - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem, i ) { - return (elem === qualifier) === keep; - }); - - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); - } - } - - return jQuery.grep(elements, function( elem, i ) { - return (jQuery.inArray( elem, qualifier ) >= 0) === keep; - }); -} - - - - -var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, - rtagName = /<([\w:]+)/, - rtbody = /", "" ], - legend: [ 1, "
", "
" ], - thead: [ 1, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - col: [ 2, "", "
" ], - area: [ 1, "", "" ], - _default: [ 0, "", "" ] - }; - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// IE can't serialize and - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/documentation/build/html/functions.html b/documentation/build/html/functions.html deleted file mode 100644 index bc11f5d3..00000000 --- a/documentation/build/html/functions.html +++ /dev/null @@ -1,341 +0,0 @@ - - - - - - - - - Pyqtgraph’s Helper Functions — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

Pyqtgraph’s Helper Functions¶

-
-

Simple Data Display Functions¶

-
-
-pyqtgraph.plot(*args, **kargs)[source]¶
-
-
Create and return a PlotWindow (this is just a window with PlotWidget inside), plot data in it.
-
Accepts a title argument to set the title of the window.
-
All other arguments are used to plot data. (see PlotItem.plot())
-
-
- -
-
-pyqtgraph.image(*args, **kargs)[source]¶
-
-
Create and return an ImageWindow (this is just a window with ImageView widget inside), show image data inside.
-
Will show 2D or 3D image data.
-
Accepts a title argument to set the title of the window.
-
All other arguments are used to show data. (see ImageView.setImage())
-
-
- -
-
-

Color, Pen, and Brush Functions¶

-

Qt uses the classes QColor, QPen, and QBrush to determine how to draw lines and fill shapes. These classes are highly capable but somewhat awkward to use. Pyqtgraph offers the functions mkColor(), mkPen(), and mkBrush() to simplify the process of creating these classes. In most cases, however, it will be unnecessary to call these functions directly–any function or method that accepts pen or brush arguments will make use of these functions for you. For example, the following three lines all have the same effect:

-
pg.plot(xdata, ydata, pen='r')
-pg.plot(xdata, ydata, pen=pg.mkPen('r'))
-pg.plot(xdata, ydata, pen=QPen(QColor(255, 0, 0)))
-
-
-
-
-pyqtgraph.mkColor(*args)¶
-

Convenience function for constructing QColor from a variety of argument types. Accepted arguments are:

- ---- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
‘c’one of: r, g, b, c, m, y, k, w
R, G, B, [A]integers 0-255
(R, G, B, [A])tuple of integers 0-255
floatgreyscale, 0.0-1.0
intsee intColor()
(int, hues)see intColor()
“RGB”hexadecimal strings; may begin with ‘#’
“RGBA” 
“RRGGBB” 
“RRGGBBAA” 
QColorQColor instance; makes a copy.
-
- -
-
-pyqtgraph.mkPen(*args, **kargs)¶
-

Convenience function for constructing QPen.

-

Examples:

-
mkPen(color)
-mkPen(color, width=2)
-mkPen(cosmetic=False, width=4.5, color='r')
-mkPen({'color': "FF0", width: 2})
-mkPen(None)   # (no pen)
-
-
-

In these examples, color may be replaced with any arguments accepted by mkColor()

-
- -
-
-pyqtgraph.mkBrush(*args)¶
-
-
Convenience function for constructing Brush.
-
This function always constructs a solid brush and accepts the same arguments as mkColor()
-
Calling mkBrush(None) returns an invisible brush.
-
-
- -
-
-pyqtgraph.hsvColor(h, s=1.0, v=1.0, a=1.0)¶
-

Generate a QColor from HSVa values.

-
- -
-
-pyqtgraph.intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs)¶
-

Creates a QColor from a single index. Useful for stepping through a predefined list of colors.

-

The argument index determines which color from the set will be returned. All other arguments determine what the set of predefined colors will be

-

Colors are chosen by cycling across hues while varying the value (brightness). -By default, this selects from a list of 9 hues.

-
- -
-
-pyqtgraph.colorTuple(c)¶
-

Return a tuple (R,G,B,A) from a QColor

-
- -
-
-pyqtgraph.colorStr(c)¶
-

Generate a hex string code from a QColor

-
- -
-
-

Data Slicing¶

-
-
-pyqtgraph.affineSlice(data, shape, origin, vectors, axes, **kargs)¶
-

Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.

-

The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets.

-

For a graphical interface to this function, see ROI.getArrayRegion()

-

Arguments:

-
-
-
data (ndarray): the original dataset
-
shape: the shape of the slice to take (Note the return value may have more dimensions than len(shape))
-
origin: the location in the original dataset that will become the origin in the sliced data.
-
vectors: list of unit vectors which point in the direction of the slice axes
-
-
    -
  • each vector must have the same length as axes
  • -
  • If the vectors are not unit length, the result will be scaled.
  • -
  • If the vectors are not orthogonal, the result will be sheared.
  • -
-

axes: the axes in the original dataset which correspond to the slice vectors

-
-

Example: start with a 4D fMRI data set, take a diagonal-planar slice out of the last 3 axes

-
-
    -
  • data = array with dims (time, x, y, z) = (100, 40, 40, 40)
  • -
  • The plane to pull out is perpendicular to the vector (x,y,z) = (1,1,1)
  • -
  • The origin of the slice will be at (x,y,z) = (40, 0, 0)
  • -
  • We will slice a 20x20 plane from each timepoint, giving a final shape (100, 20, 20)
  • -
-
-

The call for this example would look like:

-
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
-
-
-

Note the following must be true:

-
-
-
len(shape) == len(vectors)
-
len(origin) == len(axes) == len(vectors[0])
-
-
-
- -
-
-

SI Unit Conversion Functions¶

-
-
-pyqtgraph.siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True)¶
-

Return the number x formatted in engineering notation with SI prefix.

-

Example:

-
siFormat(0.0001, suffix='V')  # returns "100 μV"
-
-
-
- -
-
-pyqtgraph.siScale(x, minVal=1e-25, allowUnicode=True)¶
-

Return the recommended scale factor and SI prefix string for x.

-

Example:

-
siScale(0.0001)   # returns (1e6, 'μ')
-# This indicates that the number 0.0001 is best represented as 0.0001 * 1e6 = 100 μUnits
-
-
-
- -
-
-pyqtgraph.siEval(s)¶
-

Convert a value written in SI notation to its equivalent prefixless value

-

Example:

-
siEval("100 μV")  # returns 0.0001
-
-
-
- -
-
- - -
-
-
-
-
-

Table Of Contents

- - -

Previous topic

-

API Reference

-

Next topic

-

Pyqtgraph’s Graphics Items

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/genindex.html b/documentation/build/html/genindex.html deleted file mode 100644 index 50d88bf2..00000000 --- a/documentation/build/html/genindex.html +++ /dev/null @@ -1,457 +0,0 @@ - - - - - - - - - Index — pyqtgraph v1.8 documentation - - - - - - - - - - - -
-
-
-
- - -

Index

- -
- _ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | P | R | S | T | U | V -
-

_

- - -
-
__init__() (pyqtgraph.ArrowItem method)
-
-
(pyqtgraph.AxisItem method)
-
(pyqtgraph.ButtonItem method)
-
(pyqtgraph.CheckTable method)
-
(pyqtgraph.ColorButton method)
-
(pyqtgraph.CurveArrow method)
-
(pyqtgraph.CurvePoint method)
-
(pyqtgraph.DataTreeWidget method)
-
(pyqtgraph.FileDialog method)
-
(pyqtgraph.GradientEditorItem method)
-
(pyqtgraph.GradientLegend method)
-
(pyqtgraph.GradientWidget method)
-
(pyqtgraph.GraphicsLayout method)
-
(pyqtgraph.GraphicsLayoutWidget method)
-
(pyqtgraph.GraphicsObject method)
-
(pyqtgraph.GraphicsView method)
-
(pyqtgraph.GraphicsWidget method)
-
(pyqtgraph.GridItem method)
-
(pyqtgraph.HistogramLUTItem method)
-
(pyqtgraph.HistogramLUTWidget method)
-
(pyqtgraph.ImageItem method)
-
(pyqtgraph.ImageView method)
-
(pyqtgraph.InfiniteLine method)
-
(pyqtgraph.JoystickButton method)
-
(pyqtgraph.LabelItem method)
-
(pyqtgraph.LinearRegionItem method)
-
(pyqtgraph.MultiPlotWidget method)
-
(pyqtgraph.PlotCurveItem method)
-
(pyqtgraph.PlotDataItem method)
-
(pyqtgraph.PlotItem method)
-
(pyqtgraph.PlotWidget method)
-
(pyqtgraph.ProgressDialog method)
-
(pyqtgraph.ROI method)
-
(pyqtgraph.RawImageWidget method)
-
(pyqtgraph.ScaleBar method)
-
(pyqtgraph.ScatterPlotItem method)
-
(pyqtgraph.SpinBox method)
-
(pyqtgraph.TableWidget method)
-
(pyqtgraph.TreeWidget method)
-
(pyqtgraph.UIGraphicsItem method)
-
(pyqtgraph.VTickGroup method)
-
(pyqtgraph.VerticalLabel method)
-
(pyqtgraph.ViewBox method)
-
-
- -

A

- - - -
-
addAvgCurve() (pyqtgraph.PlotItem method)
-
affineSlice() (in module pyqtgraph)
-
appendData() (pyqtgraph.TableWidget method)
-
-
ArrowItem (class in pyqtgraph)
-
AxisItem (class in pyqtgraph)
-
- -

B

- - -
-
ButtonItem (class in pyqtgraph)
-
- -

C

- - - -
-
CheckTable (class in pyqtgraph)
-
childrenBoundingRect() (pyqtgraph.ViewBox method)
-
childTransform() (pyqtgraph.ViewBox method)
-
ColorButton (class in pyqtgraph)
-
colorStr() (in module pyqtgraph)
-
-
colorTuple() (in module pyqtgraph)
-
copy() (pyqtgraph.TableWidget method)
-
CurveArrow (class in pyqtgraph)
-
CurvePoint (class in pyqtgraph)
-
- -

D

- - - -
-
DataTreeWidget (class in pyqtgraph)
-
-
deviceTransform() (pyqtgraph.GraphicsObject method)
-
- -

E

- - - -
-
editingFinishedEvent() (pyqtgraph.SpinBox method)
-
-
enableAutoScale() (pyqtgraph.PlotItem method)
-
- -

F

- - -
-
FileDialog (class in pyqtgraph)
-
- -

G

- - - -
-
getArrayRegion() (pyqtgraph.ROI method)
-
getArraySlice() (pyqtgraph.ROI method)
-
getBoundingParents() (pyqtgraph.GraphicsObject method)
-
getGlobalTransform() (pyqtgraph.ROI method)
-
getHistogram() (pyqtgraph.ImageItem method)
-
getLocalHandlePositions() (pyqtgraph.ROI method)
-
getLookupTable() (pyqtgraph.GradientEditorItem method)
-
getRegion() (pyqtgraph.LinearRegionItem method)
-
getViewBox() (pyqtgraph.GraphicsObject method)
-
getViewWidget() (pyqtgraph.GraphicsObject method)
-
-
GradientEditorItem (class in pyqtgraph)
-
GradientLegend (class in pyqtgraph)
-
GradientWidget (class in pyqtgraph)
-
GraphicsLayout (class in pyqtgraph)
-
GraphicsLayoutWidget (class in pyqtgraph)
-
GraphicsObject (class in pyqtgraph)
-
GraphicsView (class in pyqtgraph)
-
GraphicsWidget (class in pyqtgraph)
-
GridItem (class in pyqtgraph)
-
- -

H

- - - -
-
handleChange() (pyqtgraph.ROI method)
-
HistogramLUTItem (class in pyqtgraph)
-
-
HistogramLUTWidget (class in pyqtgraph)
-
hsvColor() (in module pyqtgraph)
-
- -

I

- - - -
-
image() (in module pyqtgraph)
-
ImageItem (class in pyqtgraph)
-
ImageView (class in pyqtgraph)
-
InfiniteLine (class in pyqtgraph)
-
intColor() (in module pyqtgraph)
-
-
interpret() (pyqtgraph.SpinBox method)
-
itemBoundingRect() (pyqtgraph.ViewBox method)
-
itemMoving() (pyqtgraph.TreeWidget method)
-
iteratorFn() (pyqtgraph.TableWidget method)
-
- -

J

- - - -
-
JoystickButton (class in pyqtgraph)
-
-
jumpFrames() (pyqtgraph.ImageView method)
-
- -

K

- - -
-
keyPressEvent() (pyqtgraph.ViewBox method)
-
- -

L

- - - -
-
LabelItem (class in pyqtgraph)
-
LinearRegionItem (class in pyqtgraph)
-
-
linkXChanged() (pyqtgraph.PlotItem method)
-
linkYChanged() (pyqtgraph.PlotItem method)
-
- -

M

- - - -
-
mapFromView() (pyqtgraph.ViewBox method)
-
mapSceneToView() (pyqtgraph.ViewBox method)
-
mapToView() (pyqtgraph.ViewBox method)
-
mapViewToScene() (pyqtgraph.ViewBox method)
-
mkBrush() (in module pyqtgraph)
-
-
mkColor() (in module pyqtgraph)
-
mkPen() (in module pyqtgraph)
-
mouseShape() (pyqtgraph.UIGraphicsItem method)
-
MultiPlotWidget (class in pyqtgraph)
-
- -

N

- - - -
-
nextCol() (pyqtgraph.GraphicsLayout method)
-
-
nextRow() (pyqtgraph.GraphicsLayout method)
-
- -

P

- - - -
-
pixelLength() (pyqtgraph.GraphicsObject method)
-
pixelSize() (pyqtgraph.GraphicsView method)
-
-
(pyqtgraph.ImageItem method)
-
-
pixelVectors() (pyqtgraph.GraphicsObject method)
-
plot() (in module pyqtgraph)
-
-
(pyqtgraph.PlotItem method)
-
-
PlotCurveItem (class in pyqtgraph)
-
PlotDataItem (class in pyqtgraph)
-
-
PlotItem (class in pyqtgraph)
-
PlotWidget (class in pyqtgraph)
-
ProgressDialog (class in pyqtgraph)
-
pyqtgraph.dockarea (module)
-
pyqtgraph.parametertree (module)
-
- -

R

- - - -
-
RawImageWidget (class in pyqtgraph)
-
realBoundingRect() (pyqtgraph.UIGraphicsItem method)
-
-
ROI (class in pyqtgraph)
-
- -

S

- - - -
-
saveState() (pyqtgraph.ROI method)
-
ScaleBar (class in pyqtgraph)
-
scaleBy() (pyqtgraph.ViewBox method)
-
scaleToImage() (pyqtgraph.GraphicsView method)
-
ScatterPlotItem (class in pyqtgraph)
-
setAngle() (pyqtgraph.InfiniteLine method)
-
setAspectLocked() (pyqtgraph.ViewBox method)
-
setAttr() (pyqtgraph.LabelItem method)
-
setBounds() (pyqtgraph.InfiniteLine method)
-
setCentralWidget() (pyqtgraph.GraphicsView method)
-
setData() (pyqtgraph.DataTreeWidget method)
-
-
(pyqtgraph.PlotCurveItem method)
-
(pyqtgraph.PlotDataItem method)
-
-
setGrid() (pyqtgraph.AxisItem method)
-
setImage() (pyqtgraph.ImageItem method)
-
-
(pyqtgraph.ImageView method)
-
(pyqtgraph.RawImageWidget method)
-
-
setLabel() (pyqtgraph.PlotItem method)
-
setLabels() (pyqtgraph.GradientLegend method)
-
setLevels() (pyqtgraph.ImageItem method)
-
setLookupTable() (pyqtgraph.ImageItem method)
-
setNewBounds() (pyqtgraph.UIGraphicsItem method)
-
setPen() (pyqtgraph.PlotDataItem method)
-
-
setPoints() (pyqtgraph.ScatterPlotItem method)
-
setProperty() (pyqtgraph.SpinBox method)
-
setPxMode() (pyqtgraph.ImageItem method)
-
setRange() (pyqtgraph.ViewBox method)
-
setScale() (pyqtgraph.AxisItem method)
-
setShadowPen() (pyqtgraph.PlotDataItem method)
-
setText() (pyqtgraph.LabelItem method)
-
setTitle() (pyqtgraph.PlotItem method)
-
setValue() (pyqtgraph.SpinBox method)
-
setXLink() (pyqtgraph.PlotItem method)
-
setYLink() (pyqtgraph.PlotItem method)
-
showAxis() (pyqtgraph.PlotItem method)
-
showLabel() (pyqtgraph.PlotItem method)
-
siEval() (in module pyqtgraph)
-
siFormat() (in module pyqtgraph)
-
sigRangeChanged (pyqtgraph.PlotItem attribute)
-
siScale() (in module pyqtgraph)
-
SpinBox (class in pyqtgraph)
-
- -

T

- - - -
-
TableWidget (class in pyqtgraph)
-
targetRect() (pyqtgraph.ViewBox method)
-
timeIndex() (pyqtgraph.ImageView method)
-
-
translate() (pyqtgraph.ROI method)
-
TreeWidget (class in pyqtgraph)
-
- -

U

- - - -
-
UIGraphicsItem (class in pyqtgraph)
-
updatePlotList() (pyqtgraph.PlotItem method)
-
-
updateXScale() (pyqtgraph.PlotItem method)
-
updateYScale() (pyqtgraph.PlotItem method)
-
- -

V

- - - -
-
VerticalLabel (class in pyqtgraph)
-
ViewBox (class in pyqtgraph)
-
viewChangedEvent() (pyqtgraph.UIGraphicsItem method)
-
viewGeometry() (pyqtgraph.PlotItem method)
-
-
viewRangeChanged() (pyqtgraph.UIGraphicsItem method)
-
viewRect() (pyqtgraph.GraphicsObject method)
-
-
(pyqtgraph.GraphicsView method)
-
(pyqtgraph.ViewBox method)
-
-
viewTransform() (pyqtgraph.GraphicsObject method)
-
VTickGroup (class in pyqtgraph)
-
- - - -
-
-
-
-
- - - - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/arrowitem.html b/documentation/build/html/graphicsItems/arrowitem.html deleted file mode 100644 index f6a543e6..00000000 --- a/documentation/build/html/graphicsItems/arrowitem.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - ArrowItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ArrowItem¶

-
-
-class pyqtgraph.ArrowItem(**opts)¶
-

For displaying scale-invariant arrows. -For arrows pointing to a location on a curve, see CurveArrow

-
-
-__init__(**opts)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

AxisItem

-

Next topic

-

CurvePoint

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/axisitem.html b/documentation/build/html/graphicsItems/axisitem.html deleted file mode 100644 index c370aea2..00000000 --- a/documentation/build/html/graphicsItems/axisitem.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - AxisItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

AxisItem¶

-
-
-class pyqtgraph.AxisItem(orientation, pen=None, linkView=None, parent=None, maxTickLength=-5)¶
-
-
-__init__(orientation, pen=None, linkView=None, parent=None, maxTickLength=-5)¶
-

GraphicsItem showing a single plot axis with ticks, values, and label. -Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. -Ticks can be extended to make a grid.

-
- -
-
-setGrid(grid)¶
-

Set the alpha value for the grid, or False to disable.

-
- -
-
-setScale(scale=None)¶
-

Set the value scaling for this axis. -The scaling value 1) multiplies the values displayed along the axis -and 2) changes the way units are displayed in the label. -For example:

-
-If the axis spans values from -0.1 to 0.1 and has units set to ‘V’ -then a scale of 1000 would cause the axis to display values -100 to 100 -and the units would appear as ‘mV’
-

If scale is None, then it will be determined automatically based on the current -range displayed by the axis.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

GraphicsLayout

-

Next topic

-

ArrowItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/buttonitem.html b/documentation/build/html/graphicsItems/buttonitem.html deleted file mode 100644 index 85e06f4e..00000000 --- a/documentation/build/html/graphicsItems/buttonitem.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - ButtonItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ButtonItem¶

-
-
-class pyqtgraph.ButtonItem(imageFile, width=None, parentItem=None)¶
-

Button graphicsItem displaying an image.

-
-
-__init__(imageFile, width=None, parentItem=None)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

GradientLegend

-

Next topic

-

GraphicsObject

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/curvearrow.html b/documentation/build/html/graphicsItems/curvearrow.html deleted file mode 100644 index 72605c08..00000000 --- a/documentation/build/html/graphicsItems/curvearrow.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - CurveArrow — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

CurveArrow¶

-
-
-class pyqtgraph.CurveArrow(curve, index=0, pos=None, **opts)¶
-

Provides an arrow that points to any specific sample on a PlotCurveItem. -Provides properties that can be animated.

-
-
-__init__(curve, index=0, pos=None, **opts)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

CurvePoint

-

Next topic

-

GridItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/curvepoint.html b/documentation/build/html/graphicsItems/curvepoint.html deleted file mode 100644 index a1833226..00000000 --- a/documentation/build/html/graphicsItems/curvepoint.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - CurvePoint — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

CurvePoint¶

-
-
-class pyqtgraph.CurvePoint(curve, index=0, pos=None)¶
-

A GraphicsItem that sets its location to a point on a PlotCurveItem. -Also rotates to be tangent to the curve. -The position along the curve is a Qt property, and thus can be easily animated.

-

Note: This class does not display anything; see CurveArrow for an applied example

-
-
-__init__(curve, index=0, pos=None)¶
-

Position can be set either as an index referring to the sample number or -the position 0.0 - 1.0

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ArrowItem

-

Next topic

-

CurveArrow

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/gradienteditoritem.html b/documentation/build/html/graphicsItems/gradienteditoritem.html deleted file mode 100644 index c3061263..00000000 --- a/documentation/build/html/graphicsItems/gradienteditoritem.html +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - GradientEditorItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GradientEditorItem¶

-
-
-class pyqtgraph.GradientEditorItem(*args, **kargs)¶
-
-
-__init__(*args, **kargs)¶
-
- -
-
-getLookupTable(nPts, alpha=True)¶
-

Return an RGB/A lookup table.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

VTickGroup

-

Next topic

-

HistogramLUTItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/gradientlegend.html b/documentation/build/html/graphicsItems/gradientlegend.html deleted file mode 100644 index 0d949f0b..00000000 --- a/documentation/build/html/graphicsItems/gradientlegend.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - GradientLegend — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GradientLegend¶

-
-
-class pyqtgraph.GradientLegend(view, size, offset)¶
-

Draws a color gradient rectangle along with text labels denoting the value at specific -points along the gradient.

-
-
-__init__(view, size, offset)¶
-
- -
-
-setLabels(l)¶
-

Defines labels to appear next to the color scale. Accepts a dict of {text: value} pairs

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

HistogramLUTItem

-

Next topic

-

ButtonItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/graphicslayout.html b/documentation/build/html/graphicsItems/graphicslayout.html deleted file mode 100644 index 6ba6ebf8..00000000 --- a/documentation/build/html/graphicsItems/graphicslayout.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - GraphicsLayout — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GraphicsLayout¶

-
-
-class pyqtgraph.GraphicsLayout(parent=None, border=None)¶
-

Used for laying out GraphicsWidgets in a grid.

-
-
-__init__(parent=None, border=None)¶
-
- -
-
-nextCol(colspan=1)¶
-

Advance to next column, while returning the current column number -(generally only for internal use–called by addItem)

-
- -
-
-nextRow()¶
-

Advance to next row for automatic item placement

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ROI

-

Next topic

-

AxisItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/graphicsobject.html b/documentation/build/html/graphicsItems/graphicsobject.html deleted file mode 100644 index ad3dcd5f..00000000 --- a/documentation/build/html/graphicsItems/graphicsobject.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - GraphicsObject — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GraphicsObject¶

-
-
-class pyqtgraph.GraphicsObject(*args)¶
-

Extends QGraphicsObject with a few important functions. -(Most of these assume that the object is in a scene with a single view)

-

This class also generates a cache of the Qt-internal addresses of each item -so that GraphicsScene.items() can return the correct objects (this is a PyQt bug)

-
-
-__init__(*args)¶
-
- -
-
-deviceTransform(viewportTransform=None)¶
-

Return the transform that converts item coordinates to device coordinates (usually pixels). -Extends deviceTransform to automatically determine the viewportTransform.

-
- -
-
-getBoundingParents()¶
-

Return a list of parents to this item that have child clipping enabled.

-
- -
-
-getViewBox()¶
-

Return the first ViewBox or GraphicsView which bounds this item’s visible space. -If this item is not contained within a ViewBox, then the GraphicsView is returned. -If the item is contained inside nested ViewBoxes, then the inner-most ViewBox is returned. -The result is cached; clear the cache with forgetViewBox()

-
- -
-
-getViewWidget()¶
-

Return the view widget for this item. If the scene has multiple views, only the first view is returned. -The return value is cached; clear the cached value with forgetViewWidget()

-
- -
-
-pixelLength(direction)¶
-

Return the length of one pixel in the direction indicated (in local coordinates)

-
- -
-
-pixelVectors()¶
-

Return vectors in local coordinates representing the width and height of a view pixel.

-
- -
-
-viewRect()¶
-

Return the bounds (in item coordinates) of this item’s ViewBox or GraphicsWidget

-
- -
-
-viewTransform()¶
-

Return the transform that maps from local coordinates to the item’s ViewBox coordinates -If there is no ViewBox, return the scene transform. -Returns None if the item does not have a view.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ButtonItem

-

Next topic

-

GraphicsWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/graphicswidget.html b/documentation/build/html/graphicsItems/graphicswidget.html deleted file mode 100644 index d0283c97..00000000 --- a/documentation/build/html/graphicsItems/graphicswidget.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - GraphicsWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GraphicsWidget¶

-
-
-class pyqtgraph.GraphicsWidget(*args, **kargs)¶
-
-
-__init__(*args, **kargs)¶
-

Extends QGraphicsWidget with a workaround for a PyQt bug. -This class is otherwise identical to QGraphicsWidget.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

GraphicsObject

-

Next topic

-

UIGraphicsItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/griditem.html b/documentation/build/html/graphicsItems/griditem.html deleted file mode 100644 index 8abbf11f..00000000 --- a/documentation/build/html/graphicsItems/griditem.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - GridItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GridItem¶

-
-
-class pyqtgraph.GridItem¶
-

Displays a rectangular grid of lines indicating major divisions within a coordinate system. -Automatically determines what divisions to use.

-
-
-__init__()¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

CurveArrow

-

Next topic

-

ScaleBar

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/histogramlutitem.html b/documentation/build/html/graphicsItems/histogramlutitem.html deleted file mode 100644 index 529876dc..00000000 --- a/documentation/build/html/graphicsItems/histogramlutitem.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - HistogramLUTItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

HistogramLUTItem¶

-
-
-class pyqtgraph.HistogramLUTItem(image=None)¶
-
-
-__init__(image=None)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

GradientEditorItem

-

Next topic

-

GradientLegend

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/imageitem.html b/documentation/build/html/graphicsItems/imageitem.html deleted file mode 100644 index e4a2aff2..00000000 --- a/documentation/build/html/graphicsItems/imageitem.html +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - ImageItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ImageItem¶

-
-
-class pyqtgraph.ImageItem(image=None, **kargs)¶
-

GraphicsObject displaying an image. Optimized for rapid update (ie video display)

-
-
-__init__(image=None, **kargs)¶
-

See setImage for all allowed arguments.

-
- -
-
-getHistogram(bins=500, step=3)¶
-

returns x and y arrays containing the histogram values for the current image. -The step argument causes pixels to be skipped when computing the histogram to save time.

-
- -
-
-pixelSize()¶
-

return scene-size of a single pixel in the image

-
- -
-
-setImage(image=None, autoLevels=None, **kargs)¶
-

Update the image displayed by this item. -Arguments:

-
-image -autoLevels -lut -levels -opacity -compositionMode -border
-
- -
-
-setLevels(levels, update=True)¶
-
-
Set image scaling levels. Can be one of:
-
[blackLevel, whiteLevel] -[[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]]
-
-

Only the first format is compatible with lookup tables.

-
- -
-
-setLookupTable(lut, update=True)¶
-

Set the lookup table to use for this image. (see functions.makeARGB for more information on how this is used) -Optionally, lut can be a callable that accepts the current image as an argument and returns the lookup table to use.

-
- -
-
-setPxMode(b)¶
-

Set whether the item ignores transformations and draws directly to screen pixels.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

PlotItem

-

Next topic

-

ViewBox

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/index.html b/documentation/build/html/graphicsItems/index.html deleted file mode 100644 index 7d8fbe62..00000000 --- a/documentation/build/html/graphicsItems/index.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - Pyqtgraph’s Graphics Items — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

Pyqtgraph’s Graphics Items¶

-

Since pyqtgraph relies on Qt’s GraphicsView framework, most of its graphics functionality is implemented as QGraphicsItem subclasses. This has two important consequences: 1) virtually anything you want to draw can be easily accomplished using the functionality provided by Qt. 2) Many of pyqtgraph’s GraphicsItem classes can be used in any normal QGraphicsScene.

-

Contents:

- -
- - -
-
-
-
-
-

Previous topic

-

Pyqtgraph’s Helper Functions

-

Next topic

-

PlotDataItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/infiniteline.html b/documentation/build/html/graphicsItems/infiniteline.html deleted file mode 100644 index 0f7b3630..00000000 --- a/documentation/build/html/graphicsItems/infiniteline.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - InfiniteLine — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

InfiniteLine¶

-
-
-class pyqtgraph.InfiniteLine(pos=None, angle=90, pen=None, movable=False, bounds=None)¶
-

Displays a line of infinite length. -This line may be dragged to indicate a position in data coordinates.

-
-
-__init__(pos=None, angle=90, pen=None, movable=False, bounds=None)¶
-
-
Initialization options:
-
pos - Position of the line. This can be a QPointF or a single value for vertical/horizontal lines. -angle - Angle of line in degrees. 0 is horizontal, 90 is vertical. -pen - Pen to use when drawing line -movable - If True, the line can be dragged to a new position by the user -bounds - Optional [min, max] bounding values. Bounds are only valid if the line is vertical or horizontal.
-
-
- -
-
-setAngle(angle)¶
-

Takes angle argument in degrees. -0 is horizontal; 90 is vertical.

-

Note that the use of value() and setValue() changes if the line is -not vertical or horizontal.

-
- -
-
-setBounds(bounds)¶
-

Set the (minimum, maximum) allowable values when dragging.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

LinearRegionItem

-

Next topic

-

ROI

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/labelitem.html b/documentation/build/html/graphicsItems/labelitem.html deleted file mode 100644 index 058fd7da..00000000 --- a/documentation/build/html/graphicsItems/labelitem.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - LabelItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

LabelItem¶

-
-
-class pyqtgraph.LabelItem(text, parent=None, **args)¶
-

GraphicsWidget displaying text. -Used mainly as axis labels, titles, etc.

-
-
Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use QGraphicsTextItem
-
with the flag ItemIgnoresTransformations set.
-
-
-
-__init__(text, parent=None, **args)¶
-
- -
-
-setAttr(attr, value)¶
-

Set default text properties. See setText() for accepted parameters.

-
- -
-
-setText(text, **args)¶
-

Set the text and text properties in the label. Accepts optional arguments for auto-generating -a CSS style string:

-
-color: string (example: ‘CCFF00’) -size: string (example: ‘8pt’) -bold: boolean -italic: boolean
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ScaleBar

-

Next topic

-

VTickGroup

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/linearregionitem.html b/documentation/build/html/graphicsItems/linearregionitem.html deleted file mode 100644 index 70795d12..00000000 --- a/documentation/build/html/graphicsItems/linearregionitem.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - LinearRegionItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

LinearRegionItem¶

-
-
-class pyqtgraph.LinearRegionItem(values=[, 0, 1], orientation=None, brush=None, movable=True, bounds=None)¶
-

Used for marking a horizontal or vertical region in plots. -The region can be dragged and is bounded by lines which can be dragged individually.

-
-
-__init__(values=[, 0, 1], orientation=None, brush=None, movable=True, bounds=None)¶
-
- -
-
-getRegion()¶
-

Return the values at the edges of the region.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ViewBox

-

Next topic

-

InfiniteLine

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/plotcurveitem.html b/documentation/build/html/graphicsItems/plotcurveitem.html deleted file mode 100644 index a2586879..00000000 --- a/documentation/build/html/graphicsItems/plotcurveitem.html +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - PlotCurveItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

PlotCurveItem¶

-
-
-class pyqtgraph.PlotCurveItem(y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, color=None, clickable=False)¶
-

Class representing a single plot curve. Provides: -- Fast data update -- FFT display mode -- shadow pen -- mouse interaction

-
-
-__init__(y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, color=None, clickable=False)¶
-
- -
-
-setData(x, y, copy=False)¶
-

For Qwt compatibility

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

PlotDataItem

-

Next topic

-

ScatterPlotItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/plotdataitem.html b/documentation/build/html/graphicsItems/plotdataitem.html deleted file mode 100644 index e0f95a14..00000000 --- a/documentation/build/html/graphicsItems/plotdataitem.html +++ /dev/null @@ -1,289 +0,0 @@ - - - - - - - - - PlotDataItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

PlotDataItem¶

-
-
-class pyqtgraph.PlotDataItem(*args, **kargs)¶
-

GraphicsItem for displaying plot curves, scatter plots, or both.

-
-
-__init__(*args, **kargs)¶
-

There are many different ways to create a PlotDataItem:

-

Data initialization: (x,y data only)

-
- ---- - - - - - - - - - - - - - - -
PlotDataItem(xValues, yValues)x and y values may be any sequence (including ndarray) of real numbers
PlotDataItem(yValues)y values only – x will be automatically set to range(len(y))
PlotDataItem(x=xValues, y=yValues)x and y given by keyword arguments
PlotDataItem(ndarray(Nx2))numpy array with shape (N, 2) where x=data[:,0] and y=data[:,1]
-
-

Data initialization: (x,y data AND may include spot style)

-
- ---- - - - - - - - - - - - - - - -
PlotDataItem(recarray)numpy array with dtype=[(‘x’, float), (‘y’, float), ...]
PlotDataItem(list-of-dicts)[{‘x’: x, ‘y’: y, ...}, ...]
PlotDataItem(dict-of-lists){‘x’: [...], ‘y’: [...], ...}
PlotDataItem(MetaArray)1D array of Y values with X sepecified as axis values -OR 2D array with a column ‘y’ and extra columns as needed.
-
-

Line style keyword arguments:

-
- ---- - - - - - - - - - - - - - - -
penpen to use for drawing line between points. Default is solid grey, 1px width. Use None to disable line drawing.
shadowPenpen for secondary line to draw behind the primary line. disabled by default.
fillLevelfill the area between the curve and fillLevel
fillBrushfill to use when fillLevel is specified
-
-

Point style keyword arguments:

-
- ---- - - - - - - - - - - - - - - - - - -
symbolsymbol to use for drawing points OR list of symbols, one per point. Default is no symbol. -options are o, s, t, d, +
symbolPenoutline pen for drawing points OR list of pens, one per point
symbolBrushbrush for filling points OR list of brushes, one per point
symbolSizediameter of symbols OR list of diameters
pxMode(bool) If True, then symbolSize is specified in pixels. If False, then symbolSize is -specified in data coordinates.
-
-

Optimization keyword arguments:

-
- ---- - - - - - - - - -
identicalspots are all identical. The spot image will be rendered only once and repeated for every point
decimate(int) decimate data
-
-

Meta-info keyword arguments:

-
- ---- - - - - - -
namename of dataset. This would appear in a legend
-
-
- -
-
-setData(*args, **kargs)¶
-

Clear any data displayed by this item and display new data. -See __init__() for details; it accepts the same arguments.

-
- -
-
-setPen(pen)¶
-
-
Sets the pen used to draw lines between points.
-
pen can be a QPen or any argument accepted by pyqtgraph.mkPen()
-
-
- -
-
-setShadowPen(pen)¶
-
-
Sets the shadow pen used to draw lines between points (this is for enhancing contrast or -emphacizing data).
-
This line is drawn behind the primary pen (see setPen()) -and should generally be assigned greater width than the primary pen.
-
pen can be a QPen or any argument accepted by pyqtgraph.mkPen()
-
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

Pyqtgraph’s Graphics Items

-

Next topic

-

PlotCurveItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/plotitem.html b/documentation/build/html/graphicsItems/plotitem.html deleted file mode 100644 index 9c07f964..00000000 --- a/documentation/build/html/graphicsItems/plotitem.html +++ /dev/null @@ -1,245 +0,0 @@ - - - - - - - - - PlotItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

PlotItem¶

-
-
-class pyqtgraph.PlotItem(parent=None, name=None, labels=None, title=None, **kargs)¶
-
-
-__init__(parent=None, name=None, labels=None, title=None, **kargs)¶
-
- -
-
-addAvgCurve(curve)¶
-

Add a single curve into the pool of curves averaged together

-
- -
-
-enableAutoScale()¶
-

Enable auto-scaling. The plot will continuously scale to fit the boundaries of its data.

-
- -
-
-linkXChanged(plot)¶
-

Called when a linked plot has changed its X scale

-
- -
-
-linkYChanged(plot)¶
-

Called when a linked plot has changed its Y scale

-
- -
-
-plot(*args, **kargs)¶
-

Add and return a new plot. -See PlotDataItem.__init__ for data arguments

-
-
Extra allowed arguments are:
-
clear - clear all plots before displaying new data -params - meta-parameters to associate with this data
-
-
- -
-
-setLabel(axis, text=None, units=None, unitPrefix=None, **args)¶
-

Set the label for an axis. Basic HTML formatting is allowed. -Arguments:

-
-

axis - must be one of ‘left’, ‘bottom’, ‘right’, or ‘top’ -text - text to display along the axis. HTML allowed. -units - units to display after the title. If units are given,

-
-then an SI prefix will be automatically appended -and the axis values will be scaled accordingly. -(ie, use ‘V’ instead of ‘mV’; ‘m’ will be added automatically)
-
-
- -
-
-setTitle(title=None, **args)¶
-

Set the title of the plot. Basic HTML formatting is allowed. -If title is None, then the title will be hidden.

-
- -
- -

Link this plot’s X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)

-
- -
- -

Link this plot’s Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)

-
- -
-
-showAxis(axis, show=True)¶
-

Show or hide one of the plot’s axes. -axis must be one of ‘left’, ‘bottom’, ‘right’, or ‘top’

-
- -
-
-showLabel(axis, show=True)¶
-

Show or hide one of the plot’s axis labels (the axis itself will be unaffected). -axis must be one of ‘left’, ‘bottom’, ‘right’, or ‘top’

-
- -
-
-sigRangeChanged¶
-

Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox.

-
- -
-
-updatePlotList()¶
-

Update the list of all plotWidgets in the “link” combos

-
- -
-
-updateXScale()¶
-

Set plot to autoscale or not depending on state of radio buttons

-
- -
-
-updateYScale(b=False)¶
-

Set plot to autoscale or not depending on state of radio buttons

-
- -
-
-viewGeometry()¶
-

return the screen geometry of the viewbox

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ScatterPlotItem

-

Next topic

-

ImageItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/roi.html b/documentation/build/html/graphicsItems/roi.html deleted file mode 100644 index 111304f4..00000000 --- a/documentation/build/html/graphicsItems/roi.html +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - - - - ROI — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ROI¶

-
-
-class pyqtgraph.ROI(pos, size=Point(1.000000, 1.000000), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True)¶
-

Generic region-of-interest widget. -Can be used for implementing many types of selection box with rotate/translate/scale handles.

-
-
-__init__(pos, size=Point(1.000000, 1.000000), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True)¶
-
- -
-
-getArrayRegion(data, img, axes=(0, 1))¶
-

Use the position of this ROI relative to an imageItem to pull a slice from an array.

-
- -
-
-getArraySlice(data, img, axes=(0, 1), returnSlice=True)¶
-

Return a tuple of slice objects that can be used to slice the region from data covered by this ROI. -Also returns the transform which maps the ROI into data coordinates.

-

If returnSlice is set to False, the function returns a pair of tuples with the values that would have -been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))

-
- -
-
-getGlobalTransform(relativeTo=None)¶
-

Return global transformation (rotation angle+translation) required to move from relative state to current state. If relative state isn’t specified, -then we use the state of the ROI when mouse is pressed.

-
- -
-
-getLocalHandlePositions(index=None)¶
-

Returns the position of a handle in ROI coordinates

-
- -
-
-handleChange()¶
-

The state of the ROI has changed; redraw if needed.

-
- -
-
-saveState()¶
-

Return the state of the widget in a format suitable for storing to disk.

-
- -
-
-translate(*args, **kargs)¶
-

accepts either (x, y, snap) or ([x,y], snap) as arguments

-
-
snap can be:
-
None (default): use self.translateSnap and self.snapSize to determine whether/how to snap -False: do no snap -Point(w,h) snap to rectangular grid with spacing (w,h) -True: snap using self.snapSize (and ignoring self.translateSnap)
-
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

InfiniteLine

-

Next topic

-

GraphicsLayout

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/scalebar.html b/documentation/build/html/graphicsItems/scalebar.html deleted file mode 100644 index 2a198eb2..00000000 --- a/documentation/build/html/graphicsItems/scalebar.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - ScaleBar — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ScaleBar¶

-
-
-class pyqtgraph.ScaleBar(size, width=5, color=(100, 100, 255))¶
-

Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view.

-
-
-__init__(size, width=5, color=(100, 100, 255))¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

GridItem

-

Next topic

-

LabelItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/scatterplotitem.html b/documentation/build/html/graphicsItems/scatterplotitem.html deleted file mode 100644 index 4333b0c4..00000000 --- a/documentation/build/html/graphicsItems/scatterplotitem.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - ScatterPlotItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ScatterPlotItem¶

-
-
-class pyqtgraph.ScatterPlotItem(spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=7, symbol=None, identical=False, data=None)¶
-
-
-__init__(spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=7, symbol=None, identical=False, data=None)¶
-
-
Arguments:
-
-
spots: list of dicts. Each dict specifies parameters for a single spot:
-
{‘pos’: (x,y), ‘size’, ‘pen’, ‘brush’, ‘symbol’}
-
-

x,y: array of x,y values. Alternatively, specify spots[‘pos’] = (x,y) -pxMode: If True, spots are always the same size regardless of scaling, and size is given in px.

-
-Otherwise, size is in scene coordinates and the spots scale with the view.
-
-
identical: If True, all spots are forced to look identical.
-
This can result in performance enhancement.
-
symbol can be one of:
-
‘o’ circle -‘s’ square -‘t’ triangle -‘d’ diamond -‘+’ plus
-
-
-
-
- -
-
-setPoints(spots=None, x=None, y=None, data=None)¶
-

Remove all existing points in the scatter plot and add a new set. -Arguments:

-
-
-
spots - list of dicts specifying parameters for each spot
-
[ {‘pos’: (x,y), ‘pen’: ‘r’, ...}, ...]
-
x, y - arrays specifying location of spots to add.
-
all other parameters (pen, symbol, etc.) will be set to the default -values for this scatter plot. -these arguments are IGNORED if ‘spots’ is specified
-
data - list of arbitrary objects to be assigned to spot.data for each spot
-
(this is useful for identifying spots that are clicked on)
-
-
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

PlotCurveItem

-

Next topic

-

PlotItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/uigraphicsitem.html b/documentation/build/html/graphicsItems/uigraphicsitem.html deleted file mode 100644 index 36ac517d..00000000 --- a/documentation/build/html/graphicsItems/uigraphicsitem.html +++ /dev/null @@ -1,179 +0,0 @@ - - - - - - - - - UIGraphicsItem — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

UIGraphicsItem¶

-
-
-class pyqtgraph.UIGraphicsItem(bounds=None, parent=None)¶
-

Base class for graphics items with boundaries relative to a GraphicsView or ViewBox. -The purpose of this class is to allow the creation of GraphicsItems which live inside -a scalable view, but whose boundaries will always stay fixed relative to the view’s boundaries. -For example: GridItem, InfiniteLine

-

The view can be specified on initialization or it can be automatically detected when the item is painted.

-

NOTE: Only the item’s boundingRect is affected; the item is not transformed in any way. Use viewRangeChanged -to respond to changes in the view.

-
-
-__init__(bounds=None, parent=None)¶
-
-
Initialization Arguments:
-

#view: The view box whose bounds will be used as a reference vor this item’s bounds -bounds: QRectF with coordinates relative to view box. The default is QRectF(0,0,1,1),

-
-which means the item will have the same bounds as the view.
-
-
-
- -
-
-mouseShape()¶
-

Return the shape of this item after expanding by 2 pixels

-
- -
-
-realBoundingRect()¶
-

Called by ViewBox for determining the auto-range bounds. -If the height or with of the rect is 0, that dimension will be ignored. -By default, UIGraphicsItems are excluded from autoRange by returning -a zero-size rect.

-
- -
-
-setNewBounds()¶
-

Update the item’s bounding rect to match the viewport

-
- -
-
-viewChangedEvent()¶
-

Called whenever the view coordinates have changed. -This is a good method to override if you want to respond to change of coordinates.

-
- -
-
-viewRangeChanged()¶
-

Called when the view widget/viewbox is resized/rescaled

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

GraphicsWidget

-

Next topic

-

Pyqtgraph’s Widgets

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/viewbox.html b/documentation/build/html/graphicsItems/viewbox.html deleted file mode 100644 index 044c71cf..00000000 --- a/documentation/build/html/graphicsItems/viewbox.html +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - - - ViewBox — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ViewBox¶

-
-
-class pyqtgraph.ViewBox(parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False)¶
-

Box that allows internal scaling/panning of children by mouse drag. -Not really compatible with GraphicsView having the same functionality.

-
-
-__init__(parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False)¶
-
- -
-
-childTransform()¶
-

Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. -(This maps from inside the viewbox to outside)

-
- -
-
-childrenBoundingRect(item=None)¶
-

Return the bounding rect of all children. Returns None if there are no bounded children

-
- -
-
-itemBoundingRect(item)¶
-

Return the bounding rect of the item in view coordinates

-
- -
-
-keyPressEvent(ev)¶
-

This routine should capture key presses in the current view box. -Key presses are used only when self.useLeftButtonPan is false -The following events are implemented: -ctrl-A : zooms out to the default “full” view of the plot -ctrl-+ : moves forward in the zooming stack (if it exists) -ctrl– : moves backward in the zooming stack (if it exists)

-
- -
-
-mapFromView(obj)¶
-

Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox

-
- -
-
-mapSceneToView(obj)¶
-

Maps from scene coordinates to the coordinate system displayed inside the ViewBox

-
- -
-
-mapToView(obj)¶
-

Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox

-
- -
-
-mapViewToScene(obj)¶
-

Maps from the coordinate system displayed inside the ViewBox to scene coordinates

-
- -
-
-scaleBy(s, center=None)¶
-

Scale by s around given center point (or center of view)

-
- -
-
-setAspectLocked(lock=True, ratio=1)¶
-

If the aspect ratio is locked, view scaling is always forced to be isotropic. -By default, the ratio is set to 1; x and y both have the same scaling. -This ratio can be overridden (width/height), or use None to lock in the current ratio.

-
- -
-
-setRange(ax, minimum=None, maximum=None, padding=0.02, update=True)¶
-

Set the visible range of the ViewBox. -Can be called with a QRectF:

-
-setRange(QRectF(x, y, w, h))
-
-
Or with axis, min, max:
-
setRange(0, xMin, xMax) -setRange(1, yMin, yMax)
-
-
- -
-
-targetRect()¶
-

Return the region which has been requested to be visible. -(this is not necessarily the same as the region that is actually visible)

-
- -
-
-viewRect()¶
-

Return a QRectF bounding the region visible within the ViewBox

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ImageItem

-

Next topic

-

LinearRegionItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicsItems/vtickgroup.html b/documentation/build/html/graphicsItems/vtickgroup.html deleted file mode 100644 index d84e5bdb..00000000 --- a/documentation/build/html/graphicsItems/vtickgroup.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - VTickGroup — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

VTickGroup¶

-
-
-class pyqtgraph.VTickGroup(xvals=None, yrange=None, pen=None)¶
-

Draws a set of tick marks which always occupy the same vertical range of the view, -but have x coordinates relative to the data within the view.

-
-
-__init__(xvals=None, yrange=None, pen=None)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

LabelItem

-

Next topic

-

GradientEditorItem

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/graphicswindow.html b/documentation/build/html/graphicswindow.html deleted file mode 100644 index 87026901..00000000 --- a/documentation/build/html/graphicswindow.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - Basic display widgets — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

Basic display widgets¶

-
-
    -
  • GraphicsWindow
  • -
  • GraphicsView
  • -
  • GraphicsLayoutItem
  • -
  • ViewBox
  • -
-
-
- - -
-
-
-
-
-

Previous topic

-

Region-of-interest controls

-

Next topic

-

Rapid GUI prototyping

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/how_to_use.html b/documentation/build/html/how_to_use.html deleted file mode 100644 index a95e0688..00000000 --- a/documentation/build/html/how_to_use.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - - - - How to use pyqtgraph — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

How to use pyqtgraph¶

-

There are a few suggested ways to use pyqtgraph:

-
    -
  • From the interactive shell (python -i, ipython, etc)
  • -
  • Displaying pop-up windows from an application
  • -
  • Embedding widgets in a PyQt application
  • -
-
-

Command-line use¶

-

Pyqtgraph makes it very easy to visualize data from the command line. Observe:

-
import pyqtgraph as pg
-pg.plot(data)   # data can be a list of values or a numpy array
-
-
-

The example above would open a window displaying a line plot of the data given. I don’t think it could reasonably be any simpler than that. The call to pg.plot returns a handle to the plot widget that is created, allowing more data to be added to the same window.

-

Further examples:

-
pw = pg.plot(xVals, yVals, pen='r')  # plot x vs y in red
-pw.plot(xVals, yVals2, pen='b')
-
-win = pg.GraphicsWindow()  # Automatically generates grids with multiple items
-win.addPlot(data1, row=0, col=0)
-win.addPlot(data2, row=0, col=1)
-win.addPlot(data3, row=1, col=0, colspan=2)
-
-pg.show(imageData)  # imageData must be a numpy array with 2 to 4 dimensions
-
-
-

We’re only scratching the surface here–these functions accept many different data formats and options for customizing the appearance of your data.

-
-
-

Displaying windows from within an application¶

-

While I consider this approach somewhat lazy, it is often the case that ‘lazy’ is indistinguishable from ‘highly efficient’. The approach here is simply to use the very same functions that would be used on the command line, but from within an existing application. I often use this when I simply want to get a immediate feedback about the state of data in my application without taking the time to build a user interface for it.

-
-
-

Embedding widgets inside PyQt applications¶

-

For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: PlotWidget, ImageView, GraphicsView, GraphicsLayoutWidget. Pyqtgraph’s widgets can be included in Designer’s ui files via the “Promote To...” functionality.

-
-
- - -
-
-
-
-
-

Table Of Contents

- - -

Previous topic

-

Introduction

-

Next topic

-

Plotting in pyqtgraph

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/images.html b/documentation/build/html/images.html deleted file mode 100644 index 81213b28..00000000 --- a/documentation/build/html/images.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - Displaying images and video — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

Displaying images and video¶

-

Pyqtgraph displays 2D numpy arrays as images and provides tools for determining how to translate between the numpy data type and RGB values on the screen. If you want to display data from common image and video file formats, you will need to load the data first using another library (PIL works well for images and built-in numpy conversion).

-

The easiest way to display 2D or 3D data is using the pyqtgraph.image() function:

-
import pyqtgraph as pg
-pg.image(imageData)
-
-
-

This function will accept any floating-point or integer data types and displays a single ImageView widget containing your data. This widget includes controls for determining how the image data will be converted to 32-bit RGBa values. Conversion happens in two steps (both are optional):

-
    -
  1. Scale and offset the data (by selecting the dark/light levels on the displayed histogram)
  2. -
  3. Convert the data to color using a lookup table (determined by the colors shown in the gradient editor)
  4. -
-

If the data is 3D (time, x, y), then a time axis will be shown with a slider that can set the currently displayed frame. (if the axes in your data are ordered differently, use numpy.transpose to rearrange them)

-

There are a few other methods for displaying images as well:

-
    -
  • The ImageView class can also be instantiated directly and embedded in Qt applications.
  • -
  • Instances of ImageItem can be used inside a GraphicsView.
  • -
  • For higher performance, use RawImageWidget.
  • -
-

Any of these classes are acceptable for displaying video by calling setImage() to display a new frame. To increase performance, the image processing system uses scipy.weave to produce compiled libraries. If your computer has a compiler available, weave will automatically attempt to build the libraries it needs on demand. If this fails, then the slower pure-python methods will be used instead.

-

For more information, see the classes listed above and the ‘VideoSpeedTest’, ‘ImageItem’, ‘ImageView’, and ‘HistogramLUT’ Examples.

-
- - -
-
-
-
-
-

Previous topic

-

Plotting in pyqtgraph

-

Next topic

-

Line, Fill, and Color

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/index.html b/documentation/build/html/index.html deleted file mode 100644 index 25db4fd6..00000000 --- a/documentation/build/html/index.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - Welcome to the documentation for pyqtgraph 1.8 — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/documentation/build/html/introduction.html b/documentation/build/html/introduction.html deleted file mode 100644 index fc156976..00000000 --- a/documentation/build/html/introduction.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - Introduction — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

Introduction¶

-
-

What is pyqtgraph?¶

-

Pyqtgraph is a graphics and user interface library for Python that provides functionality commonly required in engineering and science applications. Its primary goals are 1) to provide fast, interactive graphics for displaying data (plots, video, etc.) and 2) to provide tools to aid in rapid application development (for example, property trees such as used in Qt Designer).

-

Pyqtgraph makes heavy use of the Qt GUI platform (via PyQt or PySide) for its high-performance graphics and numpy for heavy number crunching. In particular, pyqtgraph uses Qt’s GraphicsView framework which is a highly capable graphics system on its own; we bring optimized and simplified primitives to this framework to allow data visualization with minimal effort.

-

It is known to run on Linux, Windows, and OSX

-
-
-

What can it do?¶

-

Amongst the core features of pyqtgraph are:

-
    -
  • Basic data visualization primitives: Images, line and scatter plots
  • -
  • Fast enough for realtime update of video/plot data
  • -
  • Interactive scaling/panning, averaging, FFTs, SVG/PNG export
  • -
  • Widgets for marking/selecting plot regions
  • -
  • Widgets for marking/selecting image region-of-interest and automatically slicing multi-dimensional image data
  • -
  • Framework for building customized image region-of-interest widgets
  • -
  • Docking system that replaces/complements Qt’s dock system to allow more complex (and more predictable) docking arrangements
  • -
  • ParameterTree widget for rapid prototyping of dynamic interfaces (Similar to the property trees in Qt Designer and many other applications)
  • -
-
-
-

Examples¶

-

Pyqtgraph includes an extensive set of examples that can be accessed by running:

-
import pyqtgraph.examples
-pyqtgraph.examples.run()
-
-
-

This will start a launcher with a list of available examples. Select an item from the list to view its source code and double-click an item to run the example.

-
-
-

How does it compare to...¶

-
    -
  • matplotlib: For plotting and making publication-quality graphics, matplotlib is far more mature than pyqtgraph. However, matplotlib is also much slower and not suitable for applications requiring realtime update of plots/video or rapid interactivity. It also does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph.
  • -
  • pyqwt5: pyqwt is generally more mature than pyqtgraph for plotting and is about as fast. The major differences are 1) pyqtgraph is written in pure python, so it is somewhat more portable than pyqwt, which often lags behind pyqt in development (and can be a pain to install on some platforms) and 2) like matplotlib, pyqwt does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph.
  • -
-

(My experience with these libraries is somewhat outdated; please correct me if I am wrong here)

-
-
- - -
-
-
-
-
-

Table Of Contents

- - -

Previous topic

-

Welcome to the documentation for pyqtgraph 1.8

-

Next topic

-

How to use pyqtgraph

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/objects.inv b/documentation/build/html/objects.inv deleted file mode 100644 index aa34069b2bd1ed05c16836f5f8c7044c9f2be7df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2225 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 diff --git a/documentation/build/html/parametertree.html b/documentation/build/html/parametertree.html deleted file mode 100644 index 211df78d..00000000 --- a/documentation/build/html/parametertree.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - Rapid GUI prototyping — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

Rapid GUI prototyping¶

-
-
    -
  • parametertree
  • -
  • dockarea
  • -
  • flowchart
  • -
  • canvas
  • -
-
-
- - -
-
-
-
-
-

Previous topic

-

Basic display widgets

-

Next topic

-

API Reference

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/plotting.html b/documentation/build/html/plotting.html deleted file mode 100644 index 3855cbec..00000000 --- a/documentation/build/html/plotting.html +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - - - - Plotting in pyqtgraph — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

Plotting in pyqtgraph¶

-

There are a few basic ways to plot data in pyqtgraph:

- ---- - - - - - - - - - - - - - - -
pyqtgraph.plot()Create a new plot window showing your data
PlotWidget.plot()Add a new set of data to an existing plot widget
PlotItem.plot()Add a new set of data to an existing plot widget
GraphicsWindow.addPlot()Add a new plot to a grid of plots
-

All of these will accept the same basic arguments which control how the plot data is interpreted and displayed:

-
    -
  • x - Optional X data; if not specified, then a range of integers will be generated automatically.
  • -
  • y - Y data.
  • -
  • pen - The pen to use when drawing plot lines, or None to disable lines.
  • -
  • symbol - A string describing the shape of symbols to use for each point. Optionally, this may also be a sequence of strings with a different symbol for each point.
  • -
  • symbolPen - The pen (or sequence of pens) to use when drawing the symbol outline.
  • -
  • symbolBrush - The brush (or sequence of brushes) to use when filling the symbol.
  • -
  • fillLevel - Fills the area under the plot curve to this Y-value.
  • -
  • brush - The brush to use when filling under the curve.
  • -
-

See the ‘plotting’ example for a demonstration of these arguments.

-

All of the above functions also return handles to the objects that are created, allowing the plots and data to be further modified.

-
-

Organization of Plotting Classes¶

-

There are several classes invloved in displaying plot data. Most of these classes are instantiated automatically, but it is useful to understand how they are organized and relate to each other. Pyqtgraph is based heavily on Qt’s GraphicsView framework–if you are not already familiar with this, it’s worth reading about (but not essential). Most importantly: 1) Qt GUIs are composed of QWidgets, 2) A special widget called QGraphicsView is used for displaying complex graphics, and 3) QGraphicsItems define the objects that are displayed within a QGraphicsView.

-
    -
  • -
    Data Classes (all subclasses of QGraphicsItem)
    -
      -
    • PlotCurveItem - Displays a plot line given x,y data
    • -
    • ScatterPlotItem - Displays points given x,y data
    • -
    • PlotDataItem - Combines PlotCurveItem and ScatterPlotItem. The plotting functions discussed above create objects of this type.
    • -
    -
    -
    -
  • -
  • -
    Container Classes (subclasses of QGraphicsItem; contain other QGraphicsItem objects and must be viewed from within a GraphicsView)
    -
      -
    • PlotItem - Contains a ViewBox for displaying data as well as AxisItems and labels for displaying the axes and title. This is a QGraphicsItem subclass and thus may only be used from within a GraphicsView
    • -
    • GraphicsLayoutItem - QGraphicsItem subclass which displays a grid of items. This is used to display multiple PlotItems together.
    • -
    • ViewBox - A QGraphicsItem subclass for displaying data. The user may scale/pan the contents of a ViewBox using the mouse. Typically all PlotData/PlotCurve/ScatterPlotItems are displayed from within a ViewBox.
    • -
    • AxisItem - Displays axis values, ticks, and labels. Most commonly used with PlotItem.
    • -
    -
    -
    -
  • -
  • -
    Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs)
    -
      -
    • PlotWidget - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget.
    • -
    • GraphicsLayoutWidget - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget.
    • -
    -
    -
    -
  • -
-_images/plottingClasses.png -
-
-

Examples¶

-

See the ‘plotting’ and ‘PlotWidget’ examples included with pyqtgraph for more information.

-

Show x,y data as scatter plot:

-
import pyqtgraph as pg
-import numpy as np
-x = np.random.normal(size=1000)
-y = np.random.normal(size=1000)
-pg.plot(x, y, pen=None, symbol='o')  ## setting pen=None disables line drawing
-
-
-

Create/show a plot widget, display three data curves:

-
import pyqtgraph as pg
-import numpy as np
-x = np.arange(1000)
-y = np.random.normal(size=(3, 1000))
-plotWidget = pg.plot(title="Three plot curves")
-for i in range(3):
-    plotWidget.plot(x, y[i], pen=(i,3))  ## setting pen=(i,3) automaticaly creates three different-colored pens
-
-
-
-
- - -
-
-
-
-
-

Table Of Contents

- - -

Previous topic

-

How to use pyqtgraph

-

Next topic

-

Displaying images and video

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/py-modindex.html b/documentation/build/html/py-modindex.html deleted file mode 100644 index 50a684ee..00000000 --- a/documentation/build/html/py-modindex.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - Python Module Index — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- - -

Python Module Index

- -
- p -
- - - - - - - - - - - - - -
 
- p
- pyqtgraph -
    - pyqtgraph.dockarea -
    - pyqtgraph.parametertree -
- - -
-
-
-
-
- - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/region_of_interest.html b/documentation/build/html/region_of_interest.html deleted file mode 100644 index 85b39832..00000000 --- a/documentation/build/html/region_of_interest.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - Region-of-interest controls — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

Region-of-interest controls¶

-
-

Slicing Multidimensional Data¶

-
-
-

Linear Selection and Marking¶

-
-
-

2D Selection and Marking¶

-
    -
  • translate / rotate / scale
  • -
  • highly configurable control handles
  • -
  • automated data slicing
  • -
  • linearregion, infiniteline
  • -
-
-
- - -
-
-
-
-
-

Table Of Contents

- - -

Previous topic

-

Line, Fill, and Color

-

Next topic

-

Basic display widgets

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/search.html b/documentation/build/html/search.html deleted file mode 100644 index e734bc16..00000000 --- a/documentation/build/html/search.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - Search — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - - -
-
-
-
- -

Search

-
- -

- Please activate JavaScript to enable the search - functionality. -

-
-

- From here you can search these documents. Enter your search - words into the box below and click "search". Note that the search - function will automatically search for all of the words. Pages - containing fewer words won't appear in the result list. -

-
- - - -
- -
- -
- -
-
-
-
-
-
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/searchindex.js b/documentation/build/html/searchindex.js deleted file mode 100644 index 5c07321b..00000000 --- a/documentation/build/html/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({objects:{"pyqtgraph.UIGraphicsItem":{setNewBounds:[10,2,1],viewRangeChanged:[10,2,1],viewChangedEvent:[10,2,1],"__init__":[10,2,1],mouseShape:[10,2,1],realBoundingRect:[10,2,1]},"pyqtgraph.PlotCurveItem":{"__init__":[15,2,1],setData:[15,2,1]},"pyqtgraph.PlotItem":{setXLink:[14,2,1],plot:[14,2,1],setLabel:[14,2,1],enableAutoScale:[14,2,1],linkYChanged:[14,2,1],linkXChanged:[14,2,1],showLabel:[14,2,1],setTitle:[14,2,1],setYLink:[14,2,1],updateXScale:[14,2,1],updateYScale:[14,2,1],viewGeometry:[14,2,1],showAxis:[14,2,1],addAvgCurve:[14,2,1],updatePlotList:[14,2,1],sigRangeChanged:[14,4,1],"__init__":[14,2,1]},"pyqtgraph.ScatterPlotItem":{setPoints:[9,2,1],"__init__":[9,2,1]},"pyqtgraph.ScaleBar":{"__init__":[57,2,1]},"pyqtgraph.HistogramLUTWidget":{"__init__":[40,2,1]},"pyqtgraph.HistogramLUTItem":{"__init__":[41,2,1]},"pyqtgraph.GradientLegend":{setLabels:[32,2,1],"__init__":[32,2,1]},"pyqtgraph.DataTreeWidget":{"__init__":[53,2,1],setData:[53,2,1]},"pyqtgraph.CurveArrow":{"__init__":[33,2,1]},"pyqtgraph.GraphicsView":{scaleToImage:[23,2,1],viewRect:[23,2,1],pixelSize:[23,2,1],setCentralWidget:[23,2,1],"__init__":[23,2,1]},"pyqtgraph.GridItem":{"__init__":[19,2,1]},"pyqtgraph.PlotWidget":{"__init__":[7,2,1]},"pyqtgraph.AxisItem":{"__init__":[26,2,1],setScale:[26,2,1],setGrid:[26,2,1]},"pyqtgraph.ROI":{saveState:[24,2,1],getGlobalTransform:[24,2,1],getLocalHandlePositions:[24,2,1],getArrayRegion:[24,2,1],handleChange:[24,2,1],translate:[24,2,1],getArraySlice:[24,2,1],"__init__":[24,2,1]},"pyqtgraph.CheckTable":{"__init__":[56,2,1]},"pyqtgraph.LinearRegionItem":{getRegion:[45,2,1],"__init__":[45,2,1]},"pyqtgraph.PlotDataItem":{setShadowPen:[1,2,1],setData:[1,2,1],"__init__":[1,2,1],setPen:[1,2,1]},"pyqtgraph.GraphicsWidget":{"__init__":[29,2,1]},"pyqtgraph.InfiniteLine":{setAngle:[47,2,1],setBounds:[47,2,1],"__init__":[47,2,1]},"pyqtgraph.JoystickButton":{"__init__":[44,2,1]},"pyqtgraph.GradientWidget":{"__init__":[22,2,1]},"pyqtgraph.TableWidget":{iteratorFn:[0,2,1],appendData:[0,2,1],copy:[0,2,1],"__init__":[0,2,1]},"pyqtgraph.ImageView":{jumpFrames:[42,2,1],timeIndex:[42,2,1],"__init__":[42,2,1],setImage:[42,2,1]},"pyqtgraph.ArrowItem":{"__init__":[11,2,1]},"pyqtgraph.CurvePoint":{"__init__":[34,2,1]},"pyqtgraph.ColorButton":{"__init__":[51,2,1]},"pyqtgraph.ImageItem":{setPxMode:[3,2,1],setImage:[3,2,1],getHistogram:[3,2,1],setLookupTable:[3,2,1],pixelSize:[3,2,1],setLevels:[3,2,1],"__init__":[3,2,1]},"pyqtgraph.ViewBox":{targetRect:[4,2,1],mapFromView:[4,2,1],mapToView:[4,2,1],itemBoundingRect:[4,2,1],mapViewToScene:[4,2,1],viewRect:[4,2,1],keyPressEvent:[4,2,1],scaleBy:[4,2,1],childrenBoundingRect:[4,2,1],childTransform:[4,2,1],mapSceneToView:[4,2,1],setAspectLocked:[4,2,1],"__init__":[4,2,1],setRange:[4,2,1]},"pyqtgraph.VTickGroup":{"__init__":[28,2,1]},"pyqtgraph.RawImageWidget":{"__init__":[37,2,1],setImage:[37,2,1]},"pyqtgraph.VerticalLabel":{"__init__":[13,2,1]},"pyqtgraph.TreeWidget":{itemMoving:[6,2,1],"__init__":[6,2,1]},"pyqtgraph.GradientEditorItem":{getLookupTable:[49,2,1],"__init__":[49,2,1]},"pyqtgraph.ProgressDialog":{"__init__":[5,2,1]},"pyqtgraph.GraphicsObject":{viewTransform:[8,2,1],getBoundingParents:[8,2,1],pixelVectors:[8,2,1],viewRect:[8,2,1],getViewWidget:[8,2,1],getViewBox:[8,2,1],pixelLength:[8,2,1],deviceTransform:[8,2,1],"__init__":[8,2,1]},pyqtgraph:{VTickGroup:[28,3,1],GraphicsWidget:[29,3,1],affineSlice:[18,1,1],ScaleBar:[57,3,1],image:[18,1,1],mkBrush:[18,1,1],PlotDataItem:[1,3,1],GraphicsObject:[8,3,1],ImageItem:[3,3,1],LinearRegionItem:[45,3,1],ImageView:[42,3,1],FileDialog:[48,3,1],HistogramLUTWidget:[40,3,1],CheckTable:[56,3,1],MultiPlotWidget:[27,3,1],mkPen:[18,1,1],plot:[18,1,1],InfiniteLine:[47,3,1],HistogramLUTItem:[41,3,1],PlotWidget:[7,3,1],GradientWidget:[22,3,1],GridItem:[19,3,1],GradientEditorItem:[49,3,1],GradientLegend:[32,3,1],AxisItem:[26,3,1],ViewBox:[4,3,1],dockarea:[43,0,0],ArrowItem:[11,3,1],hsvColor:[18,1,1],PlotItem:[14,3,1],colorStr:[18,1,1],GraphicsLayout:[46,3,1],siEval:[18,1,1],LabelItem:[16,3,1],ROI:[24,3,1],JoystickButton:[44,3,1],CurveArrow:[33,3,1],CurvePoint:[34,3,1],SpinBox:[31,3,1],mkColor:[18,1,1],GraphicsLayoutWidget:[54,3,1],PlotCurveItem:[15,3,1],ButtonItem:[21,3,1],TreeWidget:[6,3,1],siFormat:[18,1,1],parametertree:[35,0,0],VerticalLabel:[13,3,1],intColor:[18,1,1],ColorButton:[51,3,1],RawImageWidget:[37,3,1],DataTreeWidget:[53,3,1],GraphicsView:[23,3,1],UIGraphicsItem:[10,3,1],siScale:[18,1,1],TableWidget:[0,3,1],ScatterPlotItem:[9,3,1],ProgressDialog:[5,3,1],colorTuple:[18,1,1]},"pyqtgraph.SpinBox":{setProperty:[31,2,1],setValue:[31,2,1],editingFinishedEvent:[31,2,1],"__init__":[31,2,1],interpret:[31,2,1]},"pyqtgraph.GraphicsLayoutWidget":{"__init__":[54,2,1]},"pyqtgraph.LabelItem":{setText:[16,2,1],"__init__":[16,2,1],setAttr:[16,2,1]},"pyqtgraph.ButtonItem":{"__init__":[21,2,1]},"pyqtgraph.MultiPlotWidget":{"__init__":[27,2,1]},"pyqtgraph.FileDialog":{"__init__":[48,2,1]},"pyqtgraph.GraphicsLayout":{nextCol:[46,2,1],nextRow:[46,2,1],"__init__":[46,2,1]}},terms:{roi:[18,24,52,50],all:[1,9,3,4,18,55,14,25],code:[20,18],gradienteditoritem:[52,49,50],edg:45,orthogon:18,osx:20,skip:3,global:24,makeargb:[37,3],rapid:[20,17,39,3,31],prefix:[18,14,31],subclass:[2,25,50],screen:[12,14,3,23],follow:[18,30,4],disk:24,children:4,row:[46,55],hsva:18,whose:10,setlabel:[14,32],middl:23,depend:14,decim:[1,31],intermedi:31,linkxchang:14,mapscenetoview:4,setpen:1,worth:25,sent:37,sourc:[20,18],everi:1,string:[16,0,18,30,25],delaysign:31,fals:[26,1,9,14,4,5,18,31,6,53,23,24,47,15],mous:[24,15,25,23,4],"1px":1,veri:[37,55],affect:10,setcentralwidget:23,exact:23,getarrayregion:[18,24],dim:18,imagedata:[12,55],level:[12,42,3],button:[5,14,23,21],scalabl:10,list:[0,1,9,20,18,8,55,14,12,53],griditem:[19,10,52,50],gethistogram:3,item:[26,17,1,3,4,20,46,50,6,8,52,10,23,55,14,25],vector:[8,18,23],dockarea:[43,2,39,52],refer:[17,52,10,34],arang:25,dimens:[55,18,10],properti:[16,20,33,34],slower:[12,20],direct:[8,18],consequ:50,zero:10,video:[17,3,20,12,37,42],pass:14,further:[55,25],getarrayslic:24,translatesnap:24,click:[20,9],append:14,even:[37,18],index:[17,18,42,30,6,34,33,24],what:[20,17,18,19],hide:14,appear:[26,1,55,32],compar:[20,17],section:18,current:[26,3,4,46,31,12,24],clipboard:0,rgba:[12,18],"new":[1,9,14,12,47,25],"public":20,contrast:1,qgraphicsscen:[23,50],widget:[17,18,2,53,20,42,52,31,6,7,8,27,10,23,36,12,37,24,25,55],full:[23,4],gener:[16,1,2,20,46,18,8,55,37,24,25],whitelevel:3,len:[1,18],tangent:34,uigraphicsitem:[52,10,50],address:8,locat:[34,18,9,11],along:[26,34,14,32],becom:18,modifi:25,legend:1,valu:[16,26,1,9,3,45,42,18,31,32,8,30,55,14,12,24,47,25],wait:5,invis:18,solid:[1,18],convert:[12,8,18],purpos:10,convers:[12,18,52],ahead:42,across:18,larger:18,step:[12,18,3,31],precis:18,within:[17,18,4,28,8,55,19,25],chang:[26,31,10,14,24,47],commonli:[20,25],portabl:20,overrid:[42,10],diamet:1,configur:[26,38],regardless:9,parallelepip:18,labelitem:[16,52,50],extra:[37,1,14,31],appli:34,modul:[43,17,2,52,35],getlookupt:49,setaspectlock:4,api:[17,52],visibl:[8,23,4],ax1stop:24,colortupl:18,ymin:4,select:[0,18,17,20,31,38,12,24],highli:[20,18,55,38],plot:[26,17,1,9,4,20,18,45,55,14,15,25],hexadecim:18,from:[26,17,18,4,20,8,55,12,10,24,25],describ:25,would:[26,1,24,55,18],minval:[5,18],mkbrush:[18,30],regist:14,two:[12,50],next:[46,32],few:[12,8,55,25],live:10,call:[18,4,46,6,10,14,12,55,25],graphicsitem:[26,1,21,50,34,10],recommend:18,dict:[0,1,9,53,32],type:[0,18,31,12,24,25],useopengl:23,more:[18,3,20,30,55,12,37,25],mkpen:[1,30,18],graphicslayout:[46,52,50],intcolor:[18,30],qtreewidget:6,pyqwt:20,relat:25,ital:16,enhanc:[1,9],flag:16,accept:[16,1,3,18,32,6,30,55,12,24,25],particular:20,known:20,central:23,effort:20,cach:8,must:[18,6,55,14,37,25],none:[1,3,4,5,6,7,8,9,10,16,18,21,22,23,24,25,26,27,28,44,31,33,34,14,37,47,40,41,42,45,46,51,53,54,15],graphic:[17,18,20,50,52,10,14,25],xvalu:1,getviewwidget:8,outlin:[1,25],invlov:25,column:[46,1,56],kwarg:31,can:[0,1,2,3,4,8,10,12,17,30,20,23,24,26,9,33,34,14,47,45,50,55],graphicswindow:[36,55,25],progressdialog:[5,2,52],scatter:[20,1,9,25],setbound:47,nearest:31,linkview:26,give:18,process:[12,5,18],imagewindow:18,itemignorestransform:16,indic:[17,18,19,8,42,47,57],plotwindow:18,high:20,xmin:4,minimum:[5,47,4],maxgreen:3,want:[12,55,10,50],graphicsobject:[8,52,3,50],itemmov:6,setxlink:14,alwai:[18,9,10,28,4],surfac:55,multipl:[8,55,25,31],goal:20,awkward:18,anoth:[12,14],pyqt:[17,20,29,8,55,25],divis:[19,57],how:[17,18,3,20,55,12,24,25],sever:[2,25],pure:[12,20],reject:6,opt:[33,11],instead:[12,14],simpl:[18,52],css:16,updat:[3,4,20,15,31,10,23,42,14],qwt:15,map:[8,24,4],lai:46,overridden:[23,4],max:[42,47,4],after:[10,14,31],spot:[1,9],befor:[5,14],showlabel:14,plane:18,scratch:55,aribtrari:18,compat:[15,3,31,4],mai:[1,30,23,47,25,18],npt:49,secondari:1,data:[0,1,9,17,20,15,28,18,38,52,55,14,12,53,24,47,25],averag:[20,14],attempt:12,setproperti:31,seriou:55,gradientwidget:[2,22,52],minim:20,correspond:18,exclud:10,caus:[26,3],inform:[12,3,25],maintain:6,combin:25,allow:[0,3,4,20,47,31,6,10,23,55,14,25],callabl:3,first:[12,8,3],order:12,iteratorfn:0,qgraphicswidget:[29,23],rotat:[34,24,38],fft:[20,15],rang:[26,1,4,28,10,23,25],symbols:1,through:[18,30,25],treewidget:[6,2,52],scatterplotitem:[52,9,25,50],vari:18,ax0stop:24,paramet:[16,9,14],qcolor:[18,30],style:[16,1,30],movabl:[45,24,47],directli:[12,18,3],img:[37,42,24],chosen:18,settitl:14,symbolpen:[1,25],clickabl:15,parametertre:[20,2,39,52,35],platform:20,window:[20,17,18,55,25],enablemous:[23,4],hidden:14,unitprefix:14,pixel:[1,10,3,8,23],shear:18,arrowitem:[11,52,50],them:12,good:10,"return":[0,18,42,3,4,45,46,31,6,8,10,49,23,55,24,14,25],greater:1,thei:[6,18,25],handl:[0,24,55,25,38],auto:[16,10,14],linkychang:14,timeindex:42,rectangl:32,ff0:18,framework:[20,25,50],filedialog:[48,2,52],qgraphicsitem:[25,50],dataset:[1,18],videospeedtest:12,setvalu:[5,47,31],introduct:[20,17],plotitem:[18,27,50,7,52,14,25],updateyscal:14,recarrai:1,anyth:[34,50],edit:31,drop:6,easili:[34,30,50],tablewidget:[0,2,52],mode:15,arrow:[33,11],each:[42,8,9,18,25],returnslic:24,redraw:24,side:26,mean:10,compil:12,imageitem:[3,50,52,12,37,24],maxvalu:18,replac:[20,18],individu:45,continu:14,realli:4,heavi:20,meta:[1,14],greyscal:[18,30],iter:[0,30],siscal:18,xval:[28,42,55],happen:12,lockaspect:4,extract:18,orient:[26,45,18,13,22],special:25,out:[46,18,31,4],shown:12,maptoview:4,unbound:31,space:[8,24,18],gradient:[12,32],weav:12,predefin:18,content:[17,52,2,25,50],suitabl:[20,24],rel:[28,24,10,57],correct:[20,8],red:55,yvals2:55,lag:20,linear:[17,18,31,38],insid:[16,17,18,27,4,7,8,10,12,37,55],advanc:46,given:[1,9,4,55,14,25],delai:31,reason:55,base:[26,10,25],timepoint:18,dictionari:[42,53],usual:8,region:[17,4,20,38,45,24],rect:[10,4],extend:[26,0,5,29,6,8],childrenboundingrect:4,wai:[26,1,55,12,10,25],minvalu:18,angl:[24,47],could:[5,55],synchron:26,forcewidth:13,length:[18,5,6,8,23,47],addplot:[55,25],isn:24,outsid:4,geometri:[14,23],assign:[1,9],frequent:2,histogramlut:12,origin:18,pleas:20,major:[20,19],suffix:18,render:1,symbolbrush:[1,25],onc:1,arrai:[0,1,9,3,18,55,12,53,24],scalesnap:24,qualiti:20,number:[20,34,1,18,46],alreadi:25,cosmet:18,"1e6":18,indistinguish:55,open:55,primari:[20,1],size:[16,9,3,32,10,23,24,25,57],fmri:18,guess:42,workaround:29,width:[1,4,21,18,8,23,57],associ:14,top:14,compositionmod:3,system:[12,20,19,23,4],construct:18,paint:10,necessarili:4,demonstr:[6,25],axisitem:[26,52,25,50],exampl:[16,17,18,20,5,34,10,12,55,26,25],white:42,"\u03bcunit":18,"final":18,store:24,mingreen:3,hue:18,shell:55,option:[16,1,3,31,55,12,42,47,25],tool:[12,20],copi:[0,18,15],specifi:[30,9,1,10,24,25],slider:[12,42],ydata:18,somewhat:[20,18,55],essenti:25,than:[20,1,55,18],png:20,mapfromview:4,conveni:18,setattr:16,whenev:10,provid:[0,2,20,50,33,12,15,25],remov:[9,23],pyqtgraph:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,14,37,44,47,40,41,42,45,46,48,49,50,51,52,53,54,55,56,57],tree:[6,20],structur:53,charact:30,sigrangechang:14,light:12,posit:[34,24,47],arrang:20,appenddata:0,other:[18,9,20,55,12,25],initi:[47,1,10],grei:1,graphicslayoutitem:[36,25],comput:[12,3],clip:8,rawimagewidget:[12,37,2,52],checktabl:[2,56,52],datatreewidget:[2,53,52],curvepoint:[34,52,50],ani:[26,1,2,20,18,50,33,55,14,12,10],infinitelin:[47,50,10,52,38],karg:[40,1,3,29,22,18,49,7,14,54,37,24],have:[18,4,28,31,8,10,24],tabl:[12,17,3,49],need:[1,42,18,12,37,24],minr:3,border:[46,3,4],sat:18,getboundingpar:8,engin:[20,18,2],multiplotwidget:[2,52,27],equival:18,min:[42,47,4],maxr:3,self:[24,4],plotdataitem:[1,52,25,14,50],isotrop:4,note:[16,34,18,10,47],also:[30,20,5,6,34,8,23,12,24,25],qgraphicstextitem:16,take:[47,18,55],which:[18,2,4,20,28,45,8,10,24,25],histogramlutwidget:[40,2,52],data1:55,noth:37,data3:55,data2:55,simplifi:[20,18],begin:18,pain:20,normal:[25,50],multipli:26,object:[57,8,24,25,9],rrggbbaa:18,linearregionitem:[45,52,50],most:[8,50,55,18,25],graphicsscen:8,childtransform:4,pair:[24,32],alpha:[26,18,49],"class":[0,1,3,4,5,6,7,8,9,10,11,12,13,15,16,17,18,19,21,22,23,24,25,26,27,28,29,30,31,32,33,34,14,37,44,47,40,41,42,45,46,48,49,50,51,53,54,56,57],ax0start:24,placement:46,hideroot:53,clear:[1,8,14],differ:[12,20,1,55,25],doe:[20,17,8,34],mri:18,determin:[26,18,19,8,10,12,24],axi:[16,26,1,4,14,12,42,25],minhu:18,viewtransform:8,think:55,viewchangedev:10,show:[26,18,5,55,14,25],forgetviewwidget:8,getregion:45,filllevel:[1,15,25],random:25,bring:20,bright:18,radio:14,feedback:55,vtickgroup:[28,52,50],minblu:3,onli:[1,3,4,46,31,8,55,10,42,47,25],coerc:31,ratio:4,colorbutton:[2,51,52],"true":[1,24,3,4,45,5,18,31,6,9,49,14,37,42,13,47],metaarrai:[0,1],behind:[20,1],should:[1,53,4],graphicslayoutwidget:[52,2,55,25,54],busi:5,black:42,viewport:10,qwidget:[2,25],combo:14,qprogressdialog:5,local:[8,4],control:[12,17,25,23,38],realboundingrect:10,keypressev:4,qgraphicsview:[25,23],info:1,predict:20,move:[6,42,24,31,4],get:[37,55],familiar:25,autom:38,unscal:37,state:[6,24,55,14],"import":[20,50,8,55,12,25],increas:[12,23],maxblu:3,requir:[20,5,24],setdata:[15,1,53],scale:[16,26,18,9,3,4,20,42,32,38,23,11,12,37,24,14,25,57],child:[8,4],bar:57,keyword:1,organ:[17,25],diamond:9,mkcolor:[18,30],method:[12,18,30,10,25],setlookupt:3,stuff:5,common:12,xmax:4,contain:[12,8,3,25],qgraphicsobject:8,where:[1,30],valid:47,view:[16,18,9,4,20,28,32,8,10,23,42,25,57],respond:10,set:[16,26,1,9,3,4,20,47,28,18,31,34,23,12,42,24,14,25],qpointf:47,setscal:26,crunch:20,frame:[12,42],displai:[0,1,3,4,5,55,37,12,19,16,17,18,20,21,42,25,26,34,14,11,47,52,53,36,15,57],see:[16,1,3,18,34,30,55,14,12,11,25],subtre:6,result:[8,9,18,23],arg:[16,0,1,42,48,40,29,22,18,49,8,14,37,24],fail:12,horizont:[45,47],yvalu:1,best:18,plotdata:25,infinit:47,detect:10,lut:3,getglobaltransform:24,mainli:16,boundari:[14,10,31,23],exist:[9,55,25,4],label:[16,26,25,14,32],enough:[20,37],dynam:20,between:[12,1],updateplotlist:14,drawn:1,experi:20,approach:55,qbrush:[18,30],parentitem:21,altern:9,autolevel:[42,3],kei:4,numer:31,inverti:4,complement:20,extens:20,maxhu:18,steroid:31,here:[20,55],pixelvector:8,rotatesnap:24,ipython:55,ax1start:24,pixels:[3,23],notat:[18,31],both:[12,1,4],last:18,fit:[26,42,14],cycl:18,howev:[20,18],lazi:55,showaxi:14,viewbox:[16,26,4,50,8,52,10,14,36,25],etc:[16,20,9,55,31],plotcurveitem:[50,33,34,52,15,25],instanc:[12,18],"__init__":[0,1,3,4,5,6,7,8,9,10,11,13,15,16,44,19,21,22,23,24,26,27,28,29,31,32,33,34,14,37,47,40,41,42,45,46,48,49,51,53,54,56,57],ccff00:16,mani:[1,20,30,50,55,24],fix:10,load:12,simpli:55,point:[1,9,4,18,32,33,34,12,11,24,25],instanti:[12,25],colspan:[46,55],pop:55,height:[8,10,4],header:0,written:[20,18],linux:20,cancel:5,typic:25,mouseshap:10,assum:8,viewporttransform:8,scaletoimag:23,save:3,vertic:[45,28,13,47],rgb:[12,18,49],invert:24,devic:8,compos:25,been:[6,24,4],sinc:50,much:20,interpret:[42,25,31],easiest:12,basic:[20,17,25,14,36],unambigu:23,blacklevel:3,box:[24,10,4],pxmode:[1,9],setimag:[12,37,18,42,3],imag:[17,1,3,20,21,18,41,23,12,37,42],numpi:[0,1,20,55,12,25],search:17,argument:[16,1,9,3,5,18,30,10,14,37,24,47,25],coordin:[1,24,4,28,8,9,10,23,19,47],understand:25,togeth:[25,14],demand:12,emphac:1,spin:31,opac:3,"case":[18,55],canceltext:5,multi:[20,18],ident:[1,9,29],sieval:18,look:[18,9],launcher:20,setylink:14,additem:46,graphicsview:[2,4,20,52,50,7,8,27,10,23,36,12,37,25,55],rectangular:[24,18,19,57],cursor:5,defin:[25,32],"while":[46,18,55],abov:[12,55,25],error:18,wascancel:5,aid:20,scene:[9,3,4,8,14,23],shadowpen:[1,15],observ:55,bin:3,planar:18,helper:[17,18,52],ctrl:4,pool:14,itself:14,qrectf:[10,4],vor:10,pixellength:8,histogramlutitem:[41,52,50],primit:20,verticallabel:[2,13,52],pyqwt5:20,scienc:[20,2],parent:[4,5,6,7,8,10,16,44,22,23,42,26,27,31,14,37,51,40,24,46,53,54,15],colorstr:18,enableautoscal:14,develop:[20,55],welcom:17,design:[20,55],perform:[12,37,9,23,20],suggest:55,make:[6,20,18,26,55],format:[0,18,3,55,14,12,24],fillbrush:1,same:[1,9,4,28,18,55,23,10,25],instal:20,nextcol:46,python:[12,20,55,53],pysid:20,complex:[20,30,25],pad:4,gui:[20,17,39,25],scalebar:[57,52,50],document:17,yval:55,pan:[20,42,25,23,4],higher:12,finish:[5,31],optim:[20,37,1,3],viewrect:[8,23,4],nest:[8,53],effect:18,qpen:[1,30,18],plotcurv:25,capabl:[20,18],lookup:[12,3,49],rais:[5,31],user:[2,20,5,55,47,25],canva:39,addavgcurv:14,stack:4,expand:[6,10],built:[12,30],appropri:30,center:4,labeltext:5,relativeto:24,thu:[34,25],nx2:1,well:[12,25],col:55,matplotlib:20,anim:[33,34],without:55,command:[17,55],thi:[1,3,4,6,8,10,12,18,20,23,42,25,26,9,29,31,34,14,37,47,24,50,55],dimension:[20,18],left:14,graphicswidget:[16,46,29,50,8,52],identifi:9,just:[18,55,31],hierarch:53,curvearrow:[33,34,50,52,11],getviewbox:8,shape:[37,1,10,18,25],via:[20,55,23],virtual:50,aspect:4,heavili:25,dock:20,jumpfram:42,shadow:[1,15],viewgeometri:14,signific:23,easi:55,except:[5,31],param:14,discuss:25,color:[16,17,18,15,30,32,52,12,51,25,57],add:[9,25,14],autorang:[42,10],inner:8,win:55,snap:24,els:37,busycursor:5,match:[10,23],build:[12,20,2,55],real:1,applic:[12,17,2,55,20],around:4,itemboundingrect:4,read:25,mapviewtoscen:4,dark:12,buttonitem:[21,52,50],grid:[26,19,46,55,24,25],xdata:18,background:23,press:[24,4],bit:12,tick:[26,28,25],rescal:10,name:[1,42,14],maxval:5,ignor:[9,24,10,3],like:[20,18,55],specif:[33,32],plotwidget:[16,18,2,7,52,55,14,25],qspinbox:31,childgroup:4,signal:31,arbitrari:[18,9],html:14,integ:[12,18,25,31],forgetviewbox:8,"boolean":16,singl:[26,18,27,3,15,30,7,8,9,14,12,47,25],realtim:20,setshadowpen:1,resiz:[10,23],imagefil:21,unnecessari:18,setrang:[23,4],right:[14,23],often:[20,55],captur:4,settext:16,interact:[20,15,55,14],some:[20,0],draw:[1,3,28,18,32,50,47,25],zoom:4,intern:[6,46,8,4],sampl:[33,34],setnewbound:10,importantli:[55,25],useleftbuttonpan:4,librari:[12,20],slice:[17,18,20,38,52,24],bottom:[22,14],maxticklength:26,per:1,prop:31,wrong:20,"20x20":18,pen:[26,1,9,15,28,52,18,30,55,24,47,25],scrollbar:23,unit:[26,18,52,14],allowunicod:18,notabl:30,either:[34,24,14],core:20,plu:9,run:20,bold:16,spinbox:[2,52,31],perpendicular:18,yrang:28,promot:55,offset:[12,32],rrggbb:18,joystickbutton:[44,2,52],simpler:55,lock:4,about:[20,37,55,25],actual:[31,4],transpos:12,setangl:47,page:17,degre:47,statement:5,includ:[12,20,1,55,25],dialog:5,span:26,getlocalhandleposit:24,disabl:[26,5,1,25],produc:12,"8pt":16,routin:4,own:20,effici:55,snapsiz:24,"float":[12,1,18,31],bound:[4,45,31,8,10,47],automat:[26,0,1,14,20,46,30,8,10,23,12,55,19,25],three:[18,25],diagon:18,targetrect:4,brush:[1,9,45,30,52,15,25,18],devicetransform:8,factor:18,mark:[20,17,28,45,38],your:[12,55,25],gradientlegend:[50,52,32],occupi:28,accordingli:14,dlg:5,triangl:9,ymax:4,val:31,area:[1,25],enabl:[8,14,23],hex:18,transform:[8,24,10,3,4],fast:[20,37,15],custom:[20,55],avail:[12,20,55,25],start:[20,18],reli:50,interfac:[20,18,2,55],editor:12,under:25,forward:4,setpoint:9,entir:23,"function":[0,18,2,3,4,20,17,30,50,8,52,55,12,37,24,25],creation:10,interest:[20,17,24,38],offer:18,forc:[9,4],tupl:[18,24,30],linearregion:38,hsvcolor:[18,30],amongst:20,histogram:[12,3],nextrow:46,link:14,translat:[12,24,38],setgrid:26,don:55,line:[17,1,20,18,45,30,55,19,47,25],dtype:1,bug:[8,29],scalebi:4,reset:31,pull:[18,24],immedi:55,flowchart:39,boundingrect:10,possibl:18,whether:[42,24,3],maxbound:24,access:20,maximum:[5,30,47,4],sepecifi:1,until:5,record:0,scipi:12,otherwis:[9,29],handlechang:24,similar:20,setlevel:3,curv:[1,33,34,14,11,15,25],affineslic:18,featur:[20,31],pil:12,creat:[1,55,18,23,25],"int":[1,18,31],cover:24,repres:[8,30,15,18],autoscal:14,editingfinishedev:31,implement:[27,23,4,50,7,14,24],file:[12,55],imageview:[18,2,52,55,12,42],request:4,attr:16,work:[6,12],rearrang:12,fill:[17,1,30,23,25,18],denot:32,automaticali:25,titl:[16,18,25,14],when:[1,3,4,47,6,55,23,10,24,14,25],detail:1,invalid:31,event:4,"default":[16,1,9,4,42,18,10,23,24],circl:9,bool:[1,42],varieti:18,squar:9,you:[18,50,10,12,37,25],autopixelrang:23,absurd:6,matur:20,repeat:1,"_only_":37,sequenc:[1,25],symbol:[1,9,25],qtablewidget:0,ndarrai:[37,1,42,18],multidimension:[17,38],elsewher:6,drag:[6,45,47,4],accomplish:50,embed:[12,17,55,25],consid:55,savest:24,doubl:20,setpxmod:3,prefixless:18,unaffect:14,stai:10,outdat:20,invari:11,viewrangechang:10,svg:20,updatexscal:14,visual:[20,55],tradeoff:37,text:[16,5,31,32,14,13],obj:4,time:[18,3,5,55,12,42],far:20,siformat:18,"export":20,backward:4,prototyp:[20,17,39]},objtypes:{"0":"py:module","1":"py:function","2":"py:method","3":"py:class","4":"py:attribute"},titles:["TableWidget","PlotDataItem","Pyqtgraph’s Widgets","ImageItem","ViewBox","ProgressDialog","TreeWidget","PlotWidget","GraphicsObject","ScatterPlotItem","UIGraphicsItem","ArrowItem","Displaying images and video","VerticalLabel","PlotItem","PlotCurveItem","LabelItem","Welcome to the documentation for pyqtgraph 1.8","Pyqtgraph’s Helper Functions","GridItem","Introduction","ButtonItem","GradientWidget","GraphicsView","ROI","Plotting in pyqtgraph","AxisItem","MultiPlotWidget","VTickGroup","GraphicsWidget","Line, Fill, and Color","SpinBox","GradientLegend","CurveArrow","CurvePoint","parametertree module","Basic display widgets","RawImageWidget","Region-of-interest controls","Rapid GUI prototyping","HistogramLUTWidget","HistogramLUTItem","ImageView","dockarea module","JoystickButton","LinearRegionItem","GraphicsLayout","InfiniteLine","FileDialog","GradientEditorItem","Pyqtgraph’s Graphics Items","ColorButton","API Reference","DataTreeWidget","GraphicsLayoutWidget","How to use pyqtgraph","CheckTable","ScaleBar"],objnames:{"0":"Python module","1":"Python function","2":"Python method","3":"Python class","4":"Python attribute"},filenames:["widgets/tablewidget","graphicsItems/plotdataitem","widgets/index","graphicsItems/imageitem","graphicsItems/viewbox","widgets/progressdialog","widgets/treewidget","widgets/plotwidget","graphicsItems/graphicsobject","graphicsItems/scatterplotitem","graphicsItems/uigraphicsitem","graphicsItems/arrowitem","images","widgets/verticallabel","graphicsItems/plotitem","graphicsItems/plotcurveitem","graphicsItems/labelitem","index","functions","graphicsItems/griditem","introduction","graphicsItems/buttonitem","widgets/gradientwidget","widgets/graphicsview","graphicsItems/roi","plotting","graphicsItems/axisitem","widgets/multiplotwidget","graphicsItems/vtickgroup","graphicsItems/graphicswidget","style","widgets/spinbox","graphicsItems/gradientlegend","graphicsItems/curvearrow","graphicsItems/curvepoint","widgets/parametertree","graphicswindow","widgets/rawimagewidget","region_of_interest","parametertree","widgets/histogramlutwidget","graphicsItems/histogramlutitem","widgets/imageview","widgets/dockarea","widgets/joystickbutton","graphicsItems/linearregionitem","graphicsItems/graphicslayout","graphicsItems/infiniteline","widgets/filedialog","graphicsItems/gradienteditoritem","graphicsItems/index","widgets/colorbutton","apireference","widgets/datatreewidget","widgets/graphicslayoutwidget","how_to_use","widgets/checktable","graphicsItems/scalebar"]}) \ No newline at end of file diff --git a/documentation/build/html/style.html b/documentation/build/html/style.html deleted file mode 100644 index 3cdea05f..00000000 --- a/documentation/build/html/style.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - Line, Fill, and Color — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

Line, Fill, and Color¶

-

Many functions and methods in pyqtgraph accept arguments specifying the line style (pen), fill style (brush), or color.

-

For these function arguments, the following values may be used:

-
    -
  • single-character string representing color (b, g, r, c, m, y, k, w)
  • -
  • (r, g, b) or (r, g, b, a) tuple
  • -
  • single greyscale value (0.0 - 1.0)
  • -
  • (index, maximum) tuple for automatically iterating through colors (see functions.intColor)
  • -
  • QColor
  • -
  • QPen / QBrush where appropriate
  • -
-

Notably, more complex pens and brushes can be easily built using the mkPen() / mkBrush() functions or with Qt’s QPen and QBrush classes.

-

Colors can also be built using mkColor(), intColor(), hsvColor(), or Qt’s QColor class

-
- - -
-
-
-
-
-

Previous topic

-

Displaying images and video

-

Next topic

-

Region-of-interest controls

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/checktable.html b/documentation/build/html/widgets/checktable.html deleted file mode 100644 index 22f15e17..00000000 --- a/documentation/build/html/widgets/checktable.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - CheckTable — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

CheckTable¶

-
-
-class pyqtgraph.CheckTable(columns)¶
-
-
-__init__(columns)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

DataTreeWidget

-

Next topic

-

TableWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/colorbutton.html b/documentation/build/html/widgets/colorbutton.html deleted file mode 100644 index 6865391e..00000000 --- a/documentation/build/html/widgets/colorbutton.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - ColorButton — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ColorButton¶

-
-
-class pyqtgraph.ColorButton(parent=None, color=(128, 128, 128))¶
-
-
-__init__(parent=None, color=(128, 128, 128))¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

GradientWidget

-

Next topic

-

GraphicsLayoutWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/datatreewidget.html b/documentation/build/html/widgets/datatreewidget.html deleted file mode 100644 index 449b0b48..00000000 --- a/documentation/build/html/widgets/datatreewidget.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - DataTreeWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

DataTreeWidget¶

-
-
-class pyqtgraph.DataTreeWidget(parent=None, data=None)¶
-

Widget for displaying hierarchical python data structures -(eg, nested dicts, lists, and arrays)

-
-
-__init__(parent=None, data=None)¶
-
- -
-
-setData(data, hideRoot=False)¶
-

data should be a dictionary.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ImageView

-

Next topic

-

CheckTable

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/dockarea.html b/documentation/build/html/widgets/dockarea.html deleted file mode 100644 index 8473421d..00000000 --- a/documentation/build/html/widgets/dockarea.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - dockarea module — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

dockarea module¶

-
- - -
-
-
-
-
-

Previous topic

-

GraphicsLayoutWidget

-

Next topic

-

parametertree module

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/filedialog.html b/documentation/build/html/widgets/filedialog.html deleted file mode 100644 index 45fa4ab1..00000000 --- a/documentation/build/html/widgets/filedialog.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - FileDialog — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

FileDialog¶

-
-
-class pyqtgraph.FileDialog(*args)¶
-
-
-__init__(*args)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

SpinBox

-

Next topic

-

GraphicsView

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/gradientwidget.html b/documentation/build/html/widgets/gradientwidget.html deleted file mode 100644 index 3a11e659..00000000 --- a/documentation/build/html/widgets/gradientwidget.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - GradientWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GradientWidget¶

-
-
-class pyqtgraph.GradientWidget(parent=None, orientation='bottom', *args, **kargs)¶
-
-
-__init__(parent=None, orientation='bottom', *args, **kargs)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

TableWidget

-

Next topic

-

ColorButton

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/graphicslayoutwidget.html b/documentation/build/html/widgets/graphicslayoutwidget.html deleted file mode 100644 index 3cbc7e00..00000000 --- a/documentation/build/html/widgets/graphicslayoutwidget.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - GraphicsLayoutWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GraphicsLayoutWidget¶

-
-
-class pyqtgraph.GraphicsLayoutWidget(parent=None, **kargs)¶
-
-
-__init__(parent=None, **kargs)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ColorButton

-

Next topic

-

dockarea module

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/graphicsview.html b/documentation/build/html/widgets/graphicsview.html deleted file mode 100644 index 4474a07f..00000000 --- a/documentation/build/html/widgets/graphicsview.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - - - GraphicsView — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

GraphicsView¶

-
-
-class pyqtgraph.GraphicsView(parent=None, useOpenGL=None, background='k')¶
-
-
-__init__(parent=None, useOpenGL=None, background='k')¶
-

Re-implementation of QGraphicsView that removes scrollbars and allows unambiguous control of the -viewed coordinate range. Also automatically creates a QGraphicsScene and a central QGraphicsWidget -that is automatically scaled to the full view geometry.

-

By default, the view coordinate system matches the widget’s pixel coordinates and -automatically updates when the view is resized. This can be overridden by setting -autoPixelRange=False. The exact visible range can be set with setRange().

-

The view can be panned using the middle mouse button and scaled using the right mouse button if -enabled via enableMouse().

-
- -
-
-pixelSize()¶
-

Return vector with the length and width of one view pixel in scene coordinates

-
- -
-
-scaleToImage(image)¶
-

Scales such that pixels in image are the same size as screen pixels. This may result in a significant performance increase.

-
- -
-
-setCentralWidget(item)¶
-

Sets a QGraphicsWidget to automatically fill the entire view.

-
- -
-
-viewRect()¶
-

Return the boundaries of the view in scene coordinates

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

FileDialog

-

Next topic

-

JoystickButton

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/histogramlutwidget.html b/documentation/build/html/widgets/histogramlutwidget.html deleted file mode 100644 index 5866ae01..00000000 --- a/documentation/build/html/widgets/histogramlutwidget.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - HistogramLUTWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

HistogramLUTWidget¶

-
-
-class pyqtgraph.HistogramLUTWidget(parent=None, *args, **kargs)¶
-
-
-__init__(parent=None, *args, **kargs)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

parametertree module

-

Next topic

-

ProgressDialog

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/index.html b/documentation/build/html/widgets/index.html deleted file mode 100644 index 8e84cb40..00000000 --- a/documentation/build/html/widgets/index.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - Pyqtgraph’s Widgets — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

Pyqtgraph’s Widgets¶

-

Pyqtgraph provides several QWidget subclasses which are useful for building user interfaces. These widgets can generally be used in any Qt application and provide functionality that is frequently useful in science and engineering applications.

-

Contents:

- -
- - -
-
-
-
-
-

Previous topic

-

UIGraphicsItem

-

Next topic

-

PlotWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/joystickbutton.html b/documentation/build/html/widgets/joystickbutton.html deleted file mode 100644 index 60e8eee3..00000000 --- a/documentation/build/html/widgets/joystickbutton.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - JoystickButton — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

JoystickButton¶

-
-
-class pyqtgraph.JoystickButton(parent=None)¶
-
-
-__init__(parent=None)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

GraphicsView

-

Next topic

-

MultiPlotWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/multiplotwidget.html b/documentation/build/html/widgets/multiplotwidget.html deleted file mode 100644 index 8624e2e1..00000000 --- a/documentation/build/html/widgets/multiplotwidget.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - MultiPlotWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

MultiPlotWidget¶

-
-
-class pyqtgraph.MultiPlotWidget(parent=None)¶
-

Widget implementing a graphicsView with a single PlotItem inside.

-
-
-__init__(parent=None)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

JoystickButton

-

Next topic

-

TreeWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/parametertree.html b/documentation/build/html/widgets/parametertree.html deleted file mode 100644 index cb2ff84f..00000000 --- a/documentation/build/html/widgets/parametertree.html +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - parametertree module — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

parametertree module¶

-
- - -
-
-
-
-
-

Previous topic

-

dockarea module

-

Next topic

-

HistogramLUTWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/plotwidget.html b/documentation/build/html/widgets/plotwidget.html deleted file mode 100644 index c8b060f2..00000000 --- a/documentation/build/html/widgets/plotwidget.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - PlotWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

PlotWidget¶

-
-
-class pyqtgraph.PlotWidget(parent=None, **kargs)¶
-

Widget implementing a graphicsView with a single PlotItem inside.

-
-
-__init__(parent=None, **kargs)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

Pyqtgraph’s Widgets

-

Next topic

-

ImageView

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/progressdialog.html b/documentation/build/html/widgets/progressdialog.html deleted file mode 100644 index 969b09af..00000000 --- a/documentation/build/html/widgets/progressdialog.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - ProgressDialog — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

ProgressDialog¶

-
-
-class pyqtgraph.ProgressDialog(labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False)¶
-

Extends QProgressDialog for use in ‘with’ statements. -Arguments:

-
-labelText (required) -cancelText Text to display on cancel button, or None to disable it. -minimum -maximum -parent -wait Length of time (im ms) to wait before displaying dialog -busyCursor If True, show busy cursor until dialog finishes
-
-
Example:
-
-
with ProgressDialog(“Processing..”, minVal, maxVal) as dlg:
-

# do stuff -dlg.setValue(i) ## could also use dlg += 1 -if dlg.wasCanceled():

-
-raise Exception(“Processing canceled by user”)
-
-
-
-
-
-
-__init__(labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

HistogramLUTWidget

-

Next topic

-

SpinBox

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/rawimagewidget.html b/documentation/build/html/widgets/rawimagewidget.html deleted file mode 100644 index 88a89584..00000000 --- a/documentation/build/html/widgets/rawimagewidget.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - RawImageWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - -
-
-
-
- -
-

RawImageWidget¶

-
-
-class pyqtgraph.RawImageWidget(parent=None, scaled=True)¶
-

Widget optimized for very fast video display. -Generally using an ImageItem inside GraphicsView is fast enough, -but if you need even more performance, this widget is about as fast as it gets.

-

The tradeoff is that this widget will _only_ display the unscaled image -and nothing else.

-
-
-__init__(parent=None, scaled=True)¶
-
- -
-
-setImage(img, *args, **kargs)¶
-

img must be ndarray of shape (x,y), (x,y,3), or (x,y,4). -Extra arguments are sent to functions.makeARGB

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

VerticalLabel

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/spinbox.html b/documentation/build/html/widgets/spinbox.html deleted file mode 100644 index 5eaf1b51..00000000 --- a/documentation/build/html/widgets/spinbox.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - SpinBox — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

SpinBox¶

-
-
-class pyqtgraph.SpinBox(parent=None, value=0.0, **kwargs)¶
-

QSpinBox widget on steroids. Allows selection of numerical value, with extra features: -- SI prefix notation -- Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.) -- Option for unbounded values -- Delayed signals (allows multiple rapid changes with only one change signal)

-
-
-__init__(parent=None, value=0.0, **kwargs)¶
-
- -
-
-editingFinishedEvent()¶
-

Edit has finished; set value.

-
- -
-
-interpret()¶
-

Return value of text. Return False if text is invalid, raise exception if text is intermediate

-
- -
-
-setProperty(prop, val)¶
-

setProperty is just for compatibility with QSpinBox

-
- -
-
-setValue(value=None, update=True, delaySignal=False)¶
-

Set the value of this spin. -If the value is out of bounds, it will be moved to the nearest boundary -If the spin is integer type, the value will be coerced to int -Returns the actual value set.

-

If value is None, then the current value is used (this is for resetting -the value after bounds, etc. have changed)

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

ProgressDialog

-

Next topic

-

FileDialog

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/tablewidget.html b/documentation/build/html/widgets/tablewidget.html deleted file mode 100644 index 5e6febc4..00000000 --- a/documentation/build/html/widgets/tablewidget.html +++ /dev/null @@ -1,168 +0,0 @@ - - - - - - - - - TableWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

TableWidget¶

-
-
-class pyqtgraph.TableWidget(*args)¶
-

Extends QTableWidget with some useful functions for automatic data handling. -Can automatically format and display:

-
-

numpy arrays -numpy record arrays -metaarrays -list-of-lists [[1,2,3], [4,5,6]] -dict-of-lists {‘x’: [1,2,3], ‘y’: [4,5,6]} -list-of-dicts [

-
-
-{‘x’: 1, ‘y’: 4}, -{‘x’: 2, ‘y’: 5}, -{‘x’: 3, ‘y’: 6}
-

]

-
-
-
-
-__init__(*args)¶
-
- -
-
-appendData(data)¶
-

Types allowed: -1 or 2D numpy array or metaArray -1D numpy record array -list-of-lists, list-of-dicts or dict-of-lists

-
- -
-
-copy()¶
-

Copy selected data to clipboard.

-
- -
-
-iteratorFn(data)¶
-

Return 1) a function that will provide an iterator for data and 2) a list of header strings

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

CheckTable

-

Next topic

-

GradientWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/treewidget.html b/documentation/build/html/widgets/treewidget.html deleted file mode 100644 index d20bcbc4..00000000 --- a/documentation/build/html/widgets/treewidget.html +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - TreeWidget — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

TreeWidget¶

-
-
-class pyqtgraph.TreeWidget(parent=None)¶
-

Extends QTreeWidget to allow internal drag/drop with widgets in the tree. -Also maintains the expanded state of subtrees as they are moved. -This class demonstrates the absurd lengths one must go to to make drag/drop work.

-
-
-__init__(parent=None)¶
-
- -
-
-itemMoving(item, parent, index)¶
-

Called when item has been dropped elsewhere in the tree. -Return True to accept the move, False to reject.

-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

MultiPlotWidget

-

Next topic

-

VerticalLabel

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/build/html/widgets/verticallabel.html b/documentation/build/html/widgets/verticallabel.html deleted file mode 100644 index 335a9c1e..00000000 --- a/documentation/build/html/widgets/verticallabel.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - VerticalLabel — pyqtgraph v1.8 documentation - - - - - - - - - - - - - - -
-
-
-
- -
-

VerticalLabel¶

-
-
-class pyqtgraph.VerticalLabel(text, orientation='vertical', forceWidth=True)¶
-
-
-__init__(text, orientation='vertical', forceWidth=True)¶
-
- -
- -
- - -
-
-
-
-
-

Previous topic

-

TreeWidget

-

Next topic

-

RawImageWidget

-

This Page

- - - -
-
-
-
- - - - \ No newline at end of file diff --git a/documentation/source/index.rst b/documentation/source/index.rst index aa6753ef..76c60380 100644 --- a/documentation/source/index.rst +++ b/documentation/source/index.rst @@ -12,6 +12,7 @@ Contents: :maxdepth: 2 introduction + mouse_interaction how_to_use plotting images diff --git a/examples/Arrow.py b/examples/Arrow.py index 4f7b970a..cb1f1bc6 100755 --- a/examples/Arrow.py +++ b/examples/Arrow.py @@ -10,7 +10,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) mw = QtGui.QMainWindow() -mw.resize(800,800) +mw.resize(300,300) p = pg.PlotWidget() mw.setCentralWidget(p) diff --git a/examples/PlotSpeedTest.py b/examples/PlotSpeedTest.py index 866f30d2..b695bd86 100644 --- a/examples/PlotSpeedTest.py +++ b/examples/PlotSpeedTest.py @@ -15,9 +15,10 @@ app = QtGui.QApplication([]) #mw.resize(800,800) p = pg.plot() - +p.setRange(QtCore.QRectF(0, -10, 5000, 20)) +p.setLabel('bottom', 'Index', units='B') curve = p.plot() -data = np.random.normal(size=(10,50000)) +data = np.random.normal(size=(50,5000)) ptr = 0 lastTime = time.time() fps = None diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 365b7ed6..0373ee7a 100755 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -51,7 +51,7 @@ s1.sigClicked.connect(clicked) s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) pos = np.random.normal(size=(2,n), scale=1e-5) -spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'style': i%5, 'size': 5+i/10.} for i in xrange(n)] +spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in xrange(n)] s2.addPoints(spots) w2.addItem(s2) w2.setRange(s2.boundingRect()) diff --git a/examples/__main__.py b/examples/__main__.py index b245beae..02c27b76 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -11,7 +11,7 @@ examples = OrderedDict([ ('Command-line usage', 'CLIexample.py'), ('Basic Plotting', 'Plotting.py'), ('GraphicsItems', OrderedDict([ - ('PlotItem', 'PlotItem.py'), + #('PlotItem', 'PlotItem.py'), ('ImageItem - video', 'ImageItem.py'), ('ImageItem - draw', 'Draw.py'), ('Region-of-Interest', 'ROItypes.py'), diff --git a/flowchart/Flowchart.py b/flowchart/Flowchart.py index acbdb27e..3e854d54 100644 --- a/flowchart/Flowchart.py +++ b/flowchart/Flowchart.py @@ -13,7 +13,7 @@ import FlowchartCtrlTemplate from Terminal import Terminal from numpy import ndarray import library -from debug import printExc +from pyqtgraph.debug import printExc import configfile import pyqtgraph.dockarea as dockarea import pyqtgraph as pg diff --git a/flowchart/Node.py b/flowchart/Node.py index 88a6d3b2..5604fc15 100644 --- a/flowchart/Node.py +++ b/flowchart/Node.py @@ -5,7 +5,7 @@ from pyqtgraph.graphicsItems.GraphicsObject import GraphicsObject import pyqtgraph.functions as fn from Terminal import * from collections import OrderedDict -from debug import * +from pyqtgraph.debug import * import numpy as np #from pyqtgraph.ObjectWorkaround import QObjectWorkaround from eq import * diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 60621ff0..40a6f1b7 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -184,9 +184,8 @@ class AxisItem(GraphicsWidget): #if self.drawLabel: ## If there is a label, then we are free to rescale the values if self.label.isVisible(): d = self.range[1] - self.range[0] - #pl = 1-int(log10(d)) - #scale = 10 ** pl - (scale, prefix) = fn.siScale(d / 2.) + #(scale, prefix) = fn.siScale(d / 2.) + (scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1]))) if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. scale = 1.0 prefix = '' @@ -342,7 +341,7 @@ class AxisItem(GraphicsWidget): #if dif / (pw*intervals[i]) < 10: #break - textLevel = 0 ## draw text at this scale level + textLevel = 1 ## draw text at this scale level #print "range: %s dif: %f power: %f interval: %f spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp) @@ -365,8 +364,8 @@ class AxisItem(GraphicsWidget): if i1+i >= len(intervals) or i1+i < 0: print "AxisItem.paint error: i1=%d, i=%d, len(intervals)=%d" % (i1, i, len(intervals)) continue - ## spacing for this interval + ## spacing for this interval sp = pw*intervals[i1+i] ## determine starting tick @@ -380,7 +379,8 @@ class AxisItem(GraphicsWidget): ## Number of decimal places to print maxVal = max(abs(start), abs(last)) - places = max(0, 1-int(np.log10(sp*self.scale))) + places = max(0, np.ceil(-np.log10(sp*self.scale))) + #print i, sp, sp*self.scale, np.log10(sp*self.scale), places ## length of tick #h = np.clip((self.tickLength*3 / num) - 1., min(0, self.tickLength), max(0, self.tickLength)) @@ -424,36 +424,39 @@ class AxisItem(GraphicsWidget): if p1[1-axis] < 0: continue p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha))) + # draw tick only if there is none tickPos = p1[1-axis] - if tickPos not in tickPositions: - p.drawLine(Point(p1), Point(p2)) - tickPositions.add(tickPos) - if i >= textLevel: - if abs(v) < .001 or abs(v) >= 10000: - vstr = "%g" % (v * self.scale) - else: - vstr = ("%%0.%df" % places) % (v * self.scale) - - textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) - height = textRect.height() - self.textHeight = height - if self.orientation == 'left': - textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height) - elif self.orientation == 'right': - textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height) - elif self.orientation == 'top': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height) - elif self.orientation == 'bottom': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) + + #if tickPos not in tickPositions: + p.drawLine(Point(p1), Point(p2)) + #tickPositions.add(tickPos) + if i == textLevel: + if abs(v*self.scale) < .001 or abs(v*self.scale) >= 10000: + vstr = "%g" % (v * self.scale) + else: + vstr = ("%%0.%df" % places) % (v * self.scale) + #print " ", v*self.scale, places, vstr - #p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a))) - #p.drawText(rect, textFlags, vstr) - texts.append((rect, textFlags, vstr, textAlpha)) + textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + height = textRect.height() + self.textHeight = height + if self.orientation == 'left': + textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height) + elif self.orientation == 'right': + textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height) + elif self.orientation == 'top': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height) + elif self.orientation == 'bottom': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) + + #p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a))) + #p.drawText(rect, textFlags, vstr) + texts.append((rect, textFlags, vstr, textAlpha)) prof.mark('draw ticks') for args in texts: diff --git a/widgets/GraphicsView.py b/widgets/GraphicsView.py index 10a85626..22c5736a 100644 --- a/widgets/GraphicsView.py +++ b/widgets/GraphicsView.py @@ -154,7 +154,7 @@ class GraphicsView(QtGui.QGraphicsView): return if self.autoPixelRange: self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height()) - self.setRange(self.range, padding=0, disableAutoPixel=False) + GraphicsView.setRange(self, self.range, padding=0, disableAutoPixel=False) self.updateMatrix() def updateMatrix(self, propagate=True): @@ -241,7 +241,7 @@ class GraphicsView(QtGui.QGraphicsView): w = self.size().width() * pxSize[0] h = self.size().height() * pxSize[1] range = QtCore.QRectF(tl.x(), tl.y(), w, h) - self.setRange(range, padding=0) + GraphicsView.setRange(self, range, padding=0) self.sigScaleChanged.connect(image.setScaledMode) @@ -254,13 +254,13 @@ class GraphicsView(QtGui.QGraphicsView): r1 = QtCore.QRectF(self.range) r1.setLeft(r.left()) r1.setRight(r.right()) - self.setRange(r1, padding=[padding, 0], propagate=False) + GraphicsView.setRange(self, r1, padding=[padding, 0], propagate=False) def setYRange(self, r, padding=0.05): r1 = QtCore.QRectF(self.range) r1.setTop(r.top()) r1.setBottom(r.bottom()) - self.setRange(r1, padding=[0, padding], propagate=False) + GraphicsView.setRange(self, r1, padding=[0, padding], propagate=False) #def invertY(self, invert=True): ##if self.yInverted != invert: diff --git a/widgets/PlotWidget.py b/widgets/PlotWidget.py index 310838c5..15956150 100644 --- a/widgets/PlotWidget.py +++ b/widgets/PlotWidget.py @@ -23,7 +23,7 @@ class PlotWidget(GraphicsView): self.plotItem = PlotItem(**kargs) self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange']: + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange']: setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged) From aa853ff9bf7ea1b09e2f98d15c2139eb56c0586f Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 12 Mar 2012 12:31:17 -0400 Subject: [PATCH 018/238] Added mouse interaction documentation --- documentation/source/mouse_interaction.rst | 42 ++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 documentation/source/mouse_interaction.rst diff --git a/documentation/source/mouse_interaction.rst b/documentation/source/mouse_interaction.rst new file mode 100644 index 00000000..085baaa1 --- /dev/null +++ b/documentation/source/mouse_interaction.rst @@ -0,0 +1,42 @@ +Mouse Interaction +================= + +Most applications that use pyqtgraph's data visualization will generate widgets that can be interactively scaled, panned, and otherwise configured using the mouse. This section describes mouse interaction with these widgets. + + +2D Graphics +----------- + +In pyqtgraph, most 2D visualizations follow the following mouse interaction: + +* Left button: Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. +* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. +* Right button click: Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. +* Middle button (or wheel) drag: Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). +* Wheel spin: Zooms the scene in and out. + +For machines where dragging with the right or middle buttons is difficult (usually Mac), another mouse interaction mode exists. In this mode, dragging with the left mouse button draws a box over a region of the scene. After the button is released, the scene is scaled and panned to fit the box. This mode can be accessed in the context menu or by calling:: + + pyqtgraph.setConfigOption('leftButtonPan', False) + + +Context Menu +------------ + +Right-clicking on most scenes will show a context menu with various options for changing the behavior of the scene. Some of the options available in this menu are: + +* Enable/disable automatic scaling when the data range changes +* Link the axes of multiple views together +* Enable disable mouse interaction per axis +* Explicitly set the visible range values + +The exact set of items available in the menu depends on the contents of the scene and the object clicked on. + + +3D Graphics +----------- + +3D visualizations use the following mouse interaction: + +* Left button drag: Rotates the scene around a central point +* Wheel spin: zoom in/out From 7401d3f3da8f73281f272365b1a716dc2d18d58d Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 13 Mar 2012 13:11:36 -0400 Subject: [PATCH 019/238] removed print statement --- graphicsItems/AxisItem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 40a6f1b7..31e43b75 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -400,7 +400,6 @@ class AxisItem(GraphicsWidget): textAlpha = a if self.grid is not False: - print self.grid lineAlpha = int(lineAlpha * self.grid / 255.) if axis == 0: From fcf2c53c46eb7dea7d12cb245cb361e08a1986ac Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 17 Mar 2012 11:47:20 -0400 Subject: [PATCH 020/238] Example updates --- examples/Arrow.py | 16 ++++++++++++---- examples/CLIexample.py | 10 ++++------ examples/Draw.py | 8 +++----- examples/ImageItem.py | 5 +---- examples/PlotWidget.py | 12 ++++++------ examples/Plotting.py | 17 +++++++++++------ examples/initExample.py | 3 +++ examples/template.py | 11 +++++++++++ examples/text.py | 25 +++++++++++++++++++++++++ 9 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 examples/initExample.py create mode 100644 examples/template.py create mode 100644 examples/text.py diff --git a/examples/Arrow.py b/examples/Arrow.py index cb1f1bc6..446e243e 100755 --- a/examples/Arrow.py +++ b/examples/Arrow.py @@ -1,7 +1,15 @@ # -*- coding: utf-8 -*- + +## Display an animated arrowhead following a curve. +## This example uses the CurveArrow class, which is a combination +## of ArrowItem and CurvePoint. +## +## To place a static arrow anywhere in a scene, use ArrowItem. +## To attach other types of item to a curve, use CurvePoint. + + ## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +import initExample ## Add path to library (just for examples; you do not need this) import numpy as np from pyqtgraph.Qt import QtGui, QtCore @@ -23,6 +31,6 @@ mw.show() anim = a.makeAnimation(loop=-1) anim.start() -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: +## Start Qt event loop unless running in interactive mode or using pyside. +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() diff --git a/examples/CLIexample.py b/examples/CLIexample.py index 1957328d..83ffa343 100644 --- a/examples/CLIexample.py +++ b/examples/CLIexample.py @@ -1,7 +1,4 @@ -## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) - +import initExample ## Add path to library (just for examples; you do not need this) from pyqtgraph.Qt import QtGui, QtCore import numpy as np @@ -17,6 +14,7 @@ data = np.random.normal(size=(500,500)) pg.show(data, title="Simplest possible image example") -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() diff --git a/examples/Draw.py b/examples/Draw.py index 4c687354..6a9b1323 100644 --- a/examples/Draw.py +++ b/examples/Draw.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +import initExample ## Add path to library (just for examples; you do not need this) from pyqtgraph.Qt import QtCore, QtGui @@ -39,6 +37,6 @@ kern = np.array([ img.setDrawKernel(kern, mask=kern, center=(1,1), mode='add') img.setLevels([0, 10]) -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: +## Start Qt event loop unless running in interactive mode or using pyside. +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() diff --git a/examples/ImageItem.py b/examples/ImageItem.py index f754a0bc..c3ee489e 100644 --- a/examples/ImageItem.py +++ b/examples/ImageItem.py @@ -11,11 +11,8 @@ import pyqtgraph.ptime as ptime app = QtGui.QApplication([]) ## Create window with GraphicsView widget -win = QtGui.QMainWindow() -win.resize(800,800) view = pg.GraphicsView() -win.setCentralWidget(view) -win.show() +view.show() ## show view alone in its own window ## Allow mouse scale/pan. Normally we use a ViewBox for this, but ## for simple examples this is easier. diff --git a/examples/PlotWidget.py b/examples/PlotWidget.py index 15d0a036..05da37a2 100644 --- a/examples/PlotWidget.py +++ b/examples/PlotWidget.py @@ -1,8 +1,5 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +import initExample ## Add path to library (just for examples; you do not need this) from pyqtgraph.Qt import QtGui, QtCore @@ -82,6 +79,9 @@ line = pg.InfiniteLine(angle=90, movable=True) pw3.addItem(line) line.setBounds([0,200]) -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: +import initExample ## Add path to library (just for examples; you do not need this) + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() diff --git a/examples/Plotting.py b/examples/Plotting.py index 84979f02..6f2516b8 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -1,8 +1,11 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) + +## This example demonstrates many of the 2D plotting capabilities +## in pyqtgraph. All of the plots may be panned/scaled by dragging with +## the left/right mouse buttons. Right click on any plot to show a context menu. + + +import initExample ## Add path to library (just for examples; you do not need this) from pyqtgraph.Qt import QtGui, QtCore @@ -82,6 +85,8 @@ def update(): lr.sigRegionChanged.connect(update) update() -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() + diff --git a/examples/initExample.py b/examples/initExample.py new file mode 100644 index 00000000..8f98f535 --- /dev/null +++ b/examples/initExample.py @@ -0,0 +1,3 @@ +## make this version of pyqtgraph importable before any others +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) diff --git a/examples/template.py b/examples/template.py new file mode 100644 index 00000000..8e230ac0 --- /dev/null +++ b/examples/template.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/examples/text.py b/examples/text.py new file mode 100644 index 00000000..da2025fd --- /dev/null +++ b/examples/text.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +## This example shows how to insert text into a scene using QTextItem + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + +x = np.linspace(-100, 100, 1000) +y = np.sin(x) / x +plot = pg.plot(x, y) + +## Create text object, use HTML tags to specify color (default is black; won't be visible) +text = pg.TextItem(html='
This is the
PEAK
') +plot.addItem(text) +text.setPos(0, y.max()) + + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() From fbbe4ef9466d9c6913b3607794571f1ce115e7d7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 17 Mar 2012 11:48:21 -0400 Subject: [PATCH 021/238] SignalProxy now uses thread-safe timer. --- SignalProxy.py | 3 ++- ThreadsafeTimer.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 ThreadsafeTimer.py diff --git a/SignalProxy.py b/SignalProxy.py index 95d94ba8..e3719fcf 100644 --- a/SignalProxy.py +++ b/SignalProxy.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from Qt import QtCore from ptime import time +import ThreadsafeTimer __all__ = ['SignalProxy'] @@ -27,7 +28,7 @@ class SignalProxy(QtCore.QObject): self.signal = signal self.delay = delay self.args = None - self.timer = QtCore.QTimer() + self.timer = ThreadsafeTimer.ThreadsafeTimer() self.timer.timeout.connect(self.flush) self.block = False self.slot = slot diff --git a/ThreadsafeTimer.py b/ThreadsafeTimer.py new file mode 100644 index 00000000..d8c4bcee --- /dev/null +++ b/ThreadsafeTimer.py @@ -0,0 +1,41 @@ +from pyqtgraph.Qt import QtCore, QtGui + +class ThreadsafeTimer(QtCore.QObject): + """ + Thread-safe replacement for QTimer. + """ + + timeout = QtCore.Signal() + sigTimerStopRequested = QtCore.Signal() + sigTimerStartRequested = QtCore.Signal(object) + + def __init__(self): + QtCore.QObject.__init__(self) + self.timer = QtCore.QTimer() + self.timer.timeout.connect(self.timerFinished) + self.timer.moveToThread(QtCore.QCoreApplication.instance().thread()) + self.moveToThread(QtCore.QCoreApplication.instance().thread()) + self.sigTimerStopRequested.connect(self.stop, QtCore.Qt.QueuedConnection) + self.sigTimerStartRequested.connect(self.start, QtCore.Qt.QueuedConnection) + + + def start(self, timeout): + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + #print "start timer", self, "from gui thread" + self.timer.start(timeout) + else: + #print "start timer", self, "from remote thread" + self.sigTimerStartRequested.emit(timeout) + + def stop(self): + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + #print "stop timer", self, "from gui thread" + self.timer.stop() + else: + #print "stop timer", self, "from remote thread" + self.sigTimerStopRequested.emit() + + def timerFinished(self): + self.timeout.emit() \ No newline at end of file From cd24530eb164482a806164875d619cf7b280b8ee Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 17 Mar 2012 12:10:51 -0400 Subject: [PATCH 022/238] Bugfixes: - Corrected ImageItem.setRect transformation order - PlotCurveItem uses nkPen for interpreting shadowPen arguments - PlotItem and PlotWidget wrap a few more missing methods from ViewBox --- graphicsItems/ImageItem.py | 2 +- graphicsItems/PlotCurveItem.py | 5 +++-- graphicsItems/PlotItem/PlotItem.py | 2 +- widgets/PlotWidget.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/graphicsItems/ImageItem.py b/graphicsItems/ImageItem.py index 088b5891..89bd6b64 100644 --- a/graphicsItems/ImageItem.py +++ b/graphicsItems/ImageItem.py @@ -128,8 +128,8 @@ class ImageItem(GraphicsObject): def setRect(self, rect): """Scale and translate the image to fit within rect.""" self.resetTransform() - self.scale(rect.width() / self.width(), rect.height() / self.height()) self.translate(rect.left(), rect.top()) + self.scale(rect.width() / self.width(), rect.height() / self.height()) def setImage(self, image=None, autoLevels=None, **kargs): """ diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index 91fb8661..505333b9 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -35,7 +35,8 @@ class PlotCurveItem(GraphicsObject): else: self.setPen(pen) - self.shadowPen = shadowPen + self.setShadowPen(shadowPen) + if y is not None: self.updateData(y, x, copy) @@ -159,7 +160,7 @@ class PlotCurveItem(GraphicsObject): self.update() def setShadowPen(self, pen): - self.shadowPen = pen + self.shadowPen = fn.mkPen(pen) self.update() def setDownsampling(self, ds): diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index a81d50db..e6e76cb4 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -134,7 +134,7 @@ class PlotItem(GraphicsWidget): for m in [ 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', - 'enableAutoRange', 'disableAutoRange']: + 'enableAutoRange', 'disableAutoRange', 'setAspectLocked']: setattr(self, m, getattr(self.vb, m)) self.items = [] diff --git a/widgets/PlotWidget.py b/widgets/PlotWidget.py index 15956150..807e609f 100644 --- a/widgets/PlotWidget.py +++ b/widgets/PlotWidget.py @@ -23,7 +23,7 @@ class PlotWidget(GraphicsView): self.plotItem = PlotItem(**kargs) self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange']: + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled']: setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged) From 66dd6f974ead98463ce0c57888f5579782a95e70 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 17 Mar 2012 23:10:00 -0400 Subject: [PATCH 023/238] Added TextItem and example --- examples/__main__.py | 1 + examples/text.py | 37 +++++++++-- graphicsItems/CurvePoint.py | 10 ++- graphicsItems/TextItem.py | 120 ++++++++++++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 graphicsItems/TextItem.py diff --git a/examples/__main__.py b/examples/__main__.py index 02c27b76..628b93fd 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -17,6 +17,7 @@ examples = OrderedDict([ ('Region-of-Interest', 'ROItypes.py'), ('GraphicsLayout', 'GraphicsLayout.py'), ('Scatter Plot', 'ScatterPlot.py'), + ('Text Item', 'text.py'), ('ViewBox', 'ViewBox.py'), ('Arrow', 'Arrow.py'), ])), diff --git a/examples/text.py b/examples/text.py index da2025fd..202fb572 100644 --- a/examples/text.py +++ b/examples/text.py @@ -9,15 +9,44 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np -x = np.linspace(-100, 100, 1000) +x = np.linspace(-20, 20, 1000) y = np.sin(x) / x -plot = pg.plot(x, y) +plot = pg.plot() ## create an empty plot widget +plot.setYRange(-1, 2) +curve = plot.plot(x,y) ## add a single curve -## Create text object, use HTML tags to specify color (default is black; won't be visible) -text = pg.TextItem(html='
This is the
PEAK
') +## Create text object, use HTML tags to specify color/size +text = pg.TextItem(html='
This is the
PEAK
', anchor=(-0.3,1.3), border='w', fill=(0, 0, 255, 100)) plot.addItem(text) text.setPos(0, y.max()) +## Draw an arrowhead next to the text box +arrow = pg.ArrowItem(pos=(0, y.max()), angle=-45) +plot.addItem(arrow) + + +## Set up an animated arrow and text that track the curve +curvePoint = pg.CurvePoint(curve) +plot.addItem(curvePoint) +text2 = pg.TextItem("test", anchor=(0.5, -1.0)) +text2.setParentItem(curvePoint) +arrow2 = pg.ArrowItem(angle=90) +arrow2.setParentItem(curvePoint) + +## update position every 10ms +index = 0 +def update(): + global curvePoint, index + index = (index + 1) % len(x) + curvePoint.setPos(float(index)/(len(x)-1)) + #text2.viewRangeChanged() + text2.setText('[%0.1f, %0.1f]' % (x[index], y[index])) + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(10) + + ## Start Qt event loop unless running in interactive mode or using pyside. import sys diff --git a/graphicsItems/CurvePoint.py b/graphicsItems/CurvePoint.py index a1ec5ae4..8fae5e7f 100644 --- a/graphicsItems/CurvePoint.py +++ b/graphicsItems/CurvePoint.py @@ -14,12 +14,15 @@ class CurvePoint(GraphicsObject): Note: This class does not display anything; see CurveArrow for an applied example """ - def __init__(self, curve, index=0, pos=None): + def __init__(self, curve, index=0, pos=None, rotate=True): """Position can be set either as an index referring to the sample number or - the position 0.0 - 1.0""" + the position 0.0 - 1.0 + If *rotate* is True, then the item rotates to match the tangent of the curve. + """ GraphicsObject.__init__(self) #QObjectWorkaround.__init__(self) + self._rotate = rotate self.curve = weakref.ref(curve) self.setParentItem(curve) self.setProperty('position', 0.0) @@ -76,7 +79,8 @@ class CurvePoint(GraphicsObject): p2 = self.parentItem().mapToScene(QtCore.QPointF(x[i2], y[i2])) ang = np.arctan2(p2.y()-p1.y(), p2.x()-p1.x()) ## returns radians self.resetTransform() - self.rotate(180+ ang * 180 / np.pi) ## takes degrees + if self._rotate: + self.rotate(180+ ang * 180 / np.pi) ## takes degrees QtGui.QGraphicsItem.setPos(self, *newPos) return True diff --git a/graphicsItems/TextItem.py b/graphicsItems/TextItem.py new file mode 100644 index 00000000..734de9c3 --- /dev/null +++ b/graphicsItems/TextItem.py @@ -0,0 +1,120 @@ +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +from UIGraphicsItem import * + +class TextItem(UIGraphicsItem): + """ + GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox). + """ + def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None): + """ + Arguments: + *text* The text to display + *color* The color of the text (any format accepted by pg.mkColor) + *html* If specified, this overrides both *text* and *color* + *anchor* A QPointF or (x,y) sequence indicating what region of the text box will + be anchored to the item's position. A value of (0,0) sets the upper-left corner + of the text box to be at the position specified by setPos(), while a value of (1,1) + sets the lower-right corner. + *border* A pen to use when drawing the border + *fill* A brush to use when filling within the border + """ + UIGraphicsItem.__init__(self) + self.textItem = QtGui.QGraphicsTextItem() + self.lastTransform = None + self._bounds = QtCore.QRectF() + if html is None: + self.setText(text, color) + else: + self.setHtml(html) + self.anchor = pg.Point(anchor) + self.fill = pg.mkBrush(fill) + self.border = pg.mkPen(border) + #self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport + + def setText(self, text, color=(200,200,200)): + color = pg.mkColor(color) + self.textItem.setDefaultTextColor(color) + self.textItem.setPlainText(text) + #html = '%s' % (color, text) + #self.setHtml(html) + + def updateAnchor(self): + pass + #self.resetTransform() + #self.translate(0, 20) + + def setPlainText(self, *args): + self.textItem.setPlainText(*args) + self.updateText() + + def setHtml(self, *args): + self.textItem.setHtml(*args) + self.updateText() + + def setTextWidth(self, *args): + self.textItem.setTextWidth(*args) + self.updateText() + + def setFont(self, *args): + self.textItem.setFont(*args) + self.updateText() + + def updateText(self): + self.viewRangeChanged() + + #def getImage(self): + #if self.img is None: + #br = self.textItem.boundingRect() + #img = QtGui.QImage(int(br.width()), int(br.height()), QtGui.QImage.Format_ARGB32) + #p = QtGui.QPainter(img) + #self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None) + #p.end() + #self.img = img + #return self.img + + def textBoundingRect(self): + ## return the bounds of the text box in device coordinates + pos = self.mapToDevice(QtCore.QPointF(0,0)) + if pos is None: + return None + tbr = self.textItem.boundingRect() + return QtCore.QRectF(pos.x() - tbr.width()*self.anchor.x(), pos.y() - tbr.height()*self.anchor.y(), tbr.width(), tbr.height()) + + + def viewRangeChanged(self): + br = self.textBoundingRect() + if br is None: + return + self.prepareGeometryChange() + self._bounds = self.deviceTransform().inverted()[0].mapRect(br) + #print self._bounds + + def boundingRect(self): + return self._bounds + + def paint(self, p, *args): + tr = p.transform() + if self.lastTransform is not None: + if tr != self.lastTransform: + self.viewRangeChanged() + self.lastTransform = tr + + + tbr = self.textBoundingRect() + + #p.setPen(pg.mkPen('r')) + #p.drawRect(self.boundingRect()) + + p.setPen(self.border) + p.setBrush(self.fill) + + + #p.fillRect(tbr) + p.resetTransform() + p.drawRect(tbr) + + + p.translate(tbr.left(), tbr.top()) + self.textItem.paint(p, QtGui.QStyleOptionGraphicsItem(), None) + \ No newline at end of file From 59ed9397a3758339cbcd63314db50b621bf74f8c Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 18 Mar 2012 14:57:36 -0400 Subject: [PATCH 024/238] Fixes for PlotCurveItem, PlotDataItem, ScatterPlotItem. Made APIs more complete and consistent. --- examples/Plotting.py | 2 +- graphicsItems/PlotCurveItem.py | 311 ++++++++++---------- graphicsItems/PlotDataItem.py | 116 ++++++-- graphicsItems/ScatterPlotItem.py | 473 +++++++++++++++++++++++-------- 4 files changed, 601 insertions(+), 301 deletions(-) diff --git a/examples/Plotting.py b/examples/Plotting.py index 6f2516b8..79d0d4ac 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -56,7 +56,7 @@ def update(): global curve, data, ptr, p6 curve.setData(data[ptr%10]) if ptr == 0: - p6.enableAutoRange('xy', False) + p6.enableAutoRange('xy', False) ## stop auto-scaling after the first data set is plotted ptr += 1 timer = QtCore.QTimer() timer.timeout.connect(update) diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index 505333b9..8f6bacd7 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -22,41 +22,34 @@ class PlotCurveItem(GraphicsObject): sigPlotChanged = QtCore.Signal(object) sigClicked = QtCore.Signal(object) - def __init__(self, y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, color=None, clickable=False): + def __init__(self, y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, clickable=False): GraphicsObject.__init__(self, parent) self.clear() self.path = None self.fillPath = None - if pen is None: - if color is None: - self.setPen((200,200,200)) - else: - self.setPen(color) - else: - self.setPen(pen) - - self.setShadowPen(shadowPen) if y is not None: - self.updateData(y, x, copy) + self.updateData(y, x) ## this is disastrous for performance. #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - self.fillLevel = fillLevel - self.brush = brush - self.metaData = {} self.opts = { - 'spectrumMode': False, - 'logMode': [False, False], - 'pointMode': False, - 'pointStyle': None, - 'downsample': False, - 'alphaHint': 1.0, - 'alphaMode': False + #'spectrumMode': False, + #'logMode': [False, False], + #'downsample': False, + #'alphaHint': 1.0, + #'alphaMode': False, + 'pen': 'w', + 'shadowPen': None, + 'fillLevel': fillLevel, + 'brush': brush, } - + self.setPen(pen) + self.setShadowPen(shadowPen) + self.setFillLevel(fillLevel) + self.setBrush(brush) self.setClickable(clickable) #self.fps = None @@ -71,35 +64,36 @@ class PlotCurveItem(GraphicsObject): def getData(self): - if self.xData is None: - return (None, None) - if self.xDisp is None: - nanMask = np.isnan(self.xData) | np.isnan(self.yData) - if any(nanMask): - x = self.xData[~nanMask] - y = self.yData[~nanMask] - else: - x = self.xData - y = self.yData - ds = self.opts['downsample'] - if ds > 1: - x = x[::ds] - #y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing - y = y[::ds] - if self.opts['spectrumMode']: - f = fft(y) / len(y) - y = abs(f[1:len(f)/2]) - dt = x[-1] - x[0] - x = np.linspace(0, 0.5*len(x)/dt, len(y)) - if self.opts['logMode'][0]: - x = np.log10(x) - if self.opts['logMode'][1]: - y = np.log10(y) - self.xDisp = x - self.yDisp = y - #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() - #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() - return self.xDisp, self.yDisp + return self.xData, self.yData + #if self.xData is None: + #return (None, None) + #if self.xDisp is None: + #nanMask = np.isnan(self.xData) | np.isnan(self.yData) + #if any(nanMask): + #x = self.xData[~nanMask] + #y = self.yData[~nanMask] + #else: + #x = self.xData + #y = self.yData + #ds = self.opts['downsample'] + #if ds > 1: + #x = x[::ds] + ##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing + #y = y[::ds] + #if self.opts['spectrumMode']: + #f = fft(y) / len(y) + #y = abs(f[1:len(f)/2]) + #dt = x[-1] - x[0] + #x = np.linspace(0, 0.5*len(x)/dt, len(y)) + #if self.opts['logMode'][0]: + #x = np.log10(x) + #if self.opts['logMode'][1]: + #y = np.log10(y) + #self.xDisp = x + #self.yDisp = y + ##print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() + ##print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() + #return self.xDisp, self.yDisp #def generateSpecData(self): #f = fft(self.yData) / len(self.yData) @@ -124,120 +118,121 @@ class PlotCurveItem(GraphicsObject): else: return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) - def setMeta(self, data): - self.metaData = data + #def setMeta(self, data): + #self.metaData = data - def meta(self): - return self.metaData + #def meta(self): + #return self.metaData - def setPen(self, pen): - self.pen = fn.mkPen(pen) + def setPen(self, *args, **kargs): + self.opts['pen'] = fn.mkPen(*args, **kargs) self.update() - def setColor(self, color): - self.pen.setColor(color) - self.update() - - def setAlpha(self, alpha, auto): - self.opts['alphaHint'] = alpha - self.opts['alphaMode'] = auto - self.update() - - def setSpectrumMode(self, mode): - self.opts['spectrumMode'] = mode - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setLogMode(self, mode): - self.opts['logMode'] = mode - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setPointMode(self, mode): - self.opts['pointMode'] = mode - self.update() - - def setShadowPen(self, pen): - self.shadowPen = fn.mkPen(pen) + def setShadowPen(self, *args, **kargs): + self.opts['shadowPen'] = fn.mkPen(*args, **kargs) self.update() - def setDownsampling(self, ds): - if self.opts['downsample'] != ds: - self.opts['downsample'] = ds - self.xDisp = self.yDisp = None - self.path = None - self.update() - - def setData(self, x, y, copy=False): - """For Qwt compatibility""" - self.updateData(y, x, copy) + def setBrush(self, *args, **kargs): + self.opts['brush'] = fn.mkBrush(*args, **kargs) + self.update() - def updateData(self, data, x=None, copy=False): + def setFillLevel(self, level): + self.opts['fillLevel'] = level + self.fillPath = None + self.update() + + #def setColor(self, color): + #self.pen.setColor(color) + #self.update() + + #def setAlpha(self, alpha, auto): + #self.opts['alphaHint'] = alpha + #self.opts['alphaMode'] = auto + #self.update() + + #def setSpectrumMode(self, mode): + #self.opts['spectrumMode'] = mode + #self.xDisp = self.yDisp = None + #self.path = None + #self.update() + + #def setLogMode(self, mode): + #self.opts['logMode'] = mode + #self.xDisp = self.yDisp = None + #self.path = None + #self.update() + + #def setPointMode(self, mode): + #self.opts['pointMode'] = mode + #self.update() + + + #def setDownsampling(self, ds): + #if self.opts['downsample'] != ds: + #self.opts['downsample'] = ds + #self.xDisp = self.yDisp = None + #self.path = None + #self.update() + + def setData(self, *args, **kargs): + """Same as updateData()""" + self.updateData(*args, **kargs) + + def updateData(self, *args, **kargs): prof = debug.Profiler('PlotCurveItem.updateData', disabled=True) - if isinstance(data, list): - data = np.array(data) - if isinstance(x, list): - x = np.array(x) - if not isinstance(data, np.ndarray) or data.ndim > 2: - raise Exception("Plot data must be 1 or 2D ndarray (data shape is %s)" % str(data.shape)) - if x == None: + + if len(args) == 1: + kargs['y'] = args[0] + elif len(args) == 2: + kargs['x'] = args[0] + kargs['y'] = args[1] + + if 'y' not in kargs or kargs['y'] is None: + kargs['y'] = np.array([]) + if 'x' not in kargs or kargs['x'] is None: + kargs['x'] = np.arange(len(kargs['y'])) + + for k in ['x', 'y']: + data = kargs[k] + if isinstance(data, list): + kargs['k'] = np.array(data) + if not isinstance(data, np.ndarray) or data.ndim > 1: + raise Exception("Plot data must be 1D ndarray.") if 'complex' in str(data.dtype): raise Exception("Can not plot complex data types.") - else: - if 'complex' in str(data.dtype)+str(x.dtype): - raise Exception("Can not plot complex data types.") - - if data.ndim == 2: ### If data is 2D array, then assume x and y values are in first two columns or rows. - if x is not None: - raise Exception("Plot data may be 2D only if no x argument is supplied.") - ax = 0 - if data.shape[0] > 2 and data.shape[1] == 2: - ax = 1 - ind = [slice(None), slice(None)] - ind[ax] = 0 - y = data[tuple(ind)] - ind[ax] = 1 - x = data[tuple(ind)] - elif data.ndim == 1: - y = data + prof.mark("data checks") - self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly + #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly ## Test this bug with test_PlotWidget and zoom in on the animated plot - self.prepareGeometryChange() - if copy: - self.yData = y.view(np.ndarray).copy() - else: - self.yData = y.view(np.ndarray) - - if x is None: - self.xData = np.arange(0, self.yData.shape[0]) - else: - if copy: - self.xData = x.view(np.ndarray).copy() - else: - self.xData = x.view(np.ndarray) + self.yData = kargs['y'].view(np.ndarray) + self.xData = kargs['x'].view(np.ndarray) + prof.mark('copy') - if self.xData.shape != self.yData.shape: raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) self.path = None - self.xDisp = self.yDisp = None + self.fillPath = None + #self.xDisp = self.yDisp = None + + if 'pen' in kargs: + self.setPen(kargs['pen']) + if 'shadowPen' in kargs: + self.setShadowPen(kargs['shadowPen']) + if 'fillLevel' in kargs: + self.setFillLevel(kargs['fillLevel']) + if 'brush' in kargs: + self.setBrush(kargs['brush']) + prof.mark('set') self.update() prof.mark('update') - #self.emit(QtCore.SIGNAL('plotChanged'), self) self.sigPlotChanged.emit(self) prof.mark('emit') - #prof.finish() - #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - prof.mark('set cache mode') prof.finish() def generatePath(self, x, y): @@ -303,10 +298,10 @@ class PlotCurveItem(GraphicsObject): return QtCore.QRectF() - if self.shadowPen is not None: - lineWidth = (max(self.pen.width(), self.shadowPen.width()) + 1) + if self.opts['shadowPen'] is not None: + lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) else: - lineWidth = (self.pen.width()+1) + lineWidth = (self.opts['pen'].width()+1) pixels = self.pixelVectors() @@ -343,34 +338,32 @@ class PlotCurveItem(GraphicsObject): path = self.path prof.mark('generate path') - if self.brush is not None: + if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: if self.fillPath is None: if x is None: x,y = self.getData() p2 = QtGui.QPainterPath(self.path) - p2.lineTo(x[-1], self.fillLevel) - p2.lineTo(x[0], self.fillLevel) + p2.lineTo(x[-1], self.opts['fillLevel']) + p2.lineTo(x[0], self.opts['fillLevel']) + p2.lineTo(x[0], y[0]) p2.closeSubpath() self.fillPath = p2 - p.fillPath(self.fillPath, fn.mkBrush(self.brush)) + p.fillPath(self.fillPath, self.opts['brush']) - if self.shadowPen is not None: - sp = QtGui.QPen(self.shadowPen) - else: - sp = None ## Copy pens and apply alpha adjustment - cp = QtGui.QPen(self.pen) - for pen in [sp, cp]: - if pen is None: - continue - c = pen.color() - c.setAlpha(c.alpha() * self.opts['alphaHint']) - pen.setColor(c) - #pen.setCosmetic(True) + sp = QtGui.QPen(self.opts['shadowPen']) + cp = QtGui.QPen(self.opts['pen']) + #for pen in [sp, cp]: + #if pen is None: + #continue + #c = pen.color() + #c.setAlpha(c.alpha() * self.opts['alphaHint']) + #pen.setColor(c) + ##pen.setCosmetic(True) - if self.shadowPen is not None: + if sp is not None: p.setPen(sp) p.drawPath(path) p.setPen(cp) diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index 91ea77a7..abb197eb 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -78,9 +78,16 @@ class PlotDataItem(GraphicsObject): self.setFlag(self.ItemHasNoContents) self.xData = None self.yData = None - self.curves = [] - self.scatters = [] - self.clear() + self.xDisp = None + self.yDisp = None + #self.curves = [] + #self.scatters = [] + self.curve = PlotCurveItem() + self.scatter = ScatterPlotItem() + self.curve.setParentItem(self) + self.scatter.setParentItem(self) + + #self.clear() self.opts = { 'fftMode': False, 'logMode': [False, False], @@ -130,17 +137,20 @@ class PlotDataItem(GraphicsObject): self.opts['pointMode'] = mode self.update() - def setPen(self, pen): + def setPen(self, *args, **kargs): """ | Sets the pen used to draw lines between points. | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() ` """ - self.opts['pen'] = fn.mkPen(pen) - for c in self.curves: - c.setPen(pen) - self.update() + pen = fn.mkPen(*args, **kargs) + self.opts['pen'] = pen + #self.curve.setPen(pen) + #for c in self.curves: + #c.setPen(pen) + #self.update() + self.updateItems() - def setShadowPen(self, pen): + def setShadowPen(self, *args, **kargs): """ | Sets the shadow pen used to draw lines between points (this is for enhancing contrast or emphacizing data). @@ -148,10 +158,46 @@ class PlotDataItem(GraphicsObject): and should generally be assigned greater width than the primary pen. | *pen* can be a QPen or any argument accepted by :func:`pyqtgraph.mkPen() ` """ + pen = fn.mkPen(*args, **kargs) self.opts['shadowPen'] = pen - for c in self.curves: - c.setPen(pen) - self.update() + #for c in self.curves: + #c.setPen(pen) + #self.update() + self.updateItems() + + def setBrush(self, *args, **kargs): + brush = fn.mkBrush(*args, **kargs) + self.opts['brush'] = brush + self.updateItems() + + def setFillLevel(self, level): + self.opts['fillLevel'] = level + self.updateItems() + + def setSymbol(self, symbol): + self.opts['symbol'] = symbol + #self.scatter.setSymbol(symbol) + self.updateItems() + + def setSymbolPen(self, *args, **kargs): + pen = fn.mkPen(*args, **kargs) + self.opts['symbolPen'] = pen + #self.scatter.setSymbolPen(pen) + self.updateItems() + + + + def setSymbolBrush(self, *args, **kargs): + brush = fn.mkBrush(*args, **kargs) + self.opts['symbolBrush'] = brush + #self.scatter.setSymbolBrush(brush) + self.updateItems() + + + def setSymbolSize(self, size): + self.opts['symbolSize'] = size + #self.scatter.setSymbolSize(symbolSize) + self.updateItems() def setDownsampling(self, ds): if self.opts['downsample'] != ds: @@ -165,7 +211,7 @@ class PlotDataItem(GraphicsObject): See :func:`__init__() ` for details; it accepts the same arguments. """ - self.clear() + #self.clear() y = None x = None @@ -219,7 +265,7 @@ class PlotDataItem(GraphicsObject): ## if symbol pen/brush are given with no symbol, then assume symbol is 'o' - if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs): + if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs or 'symbolSize' in kargs): kargs['symbol'] = 'o' for k in self.opts.keys(): @@ -251,6 +297,8 @@ class PlotDataItem(GraphicsObject): self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by self.yData = y.view(np.ndarray) + self.xDisp = None + self.yDisp = None self.updateItems() view = self.getViewBox() @@ -260,29 +308,37 @@ class PlotDataItem(GraphicsObject): def updateItems(self): - for c in self.curves+self.scatters: - if c.scene() is not None: - c.scene().removeItem(c) + #for c in self.curves+self.scatters: + #if c.scene() is not None: + #c.scene().removeItem(c) curveArgs = {} for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: curveArgs[k] = self.opts[k] scatterArgs = {} - for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol')]: + for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size')]: scatterArgs[v] = self.opts[k] x,y = self.getData() + self.curve.setData(x=x, y=y, **curveArgs) if curveArgs['pen'] is not None or curveArgs['brush'] is not None: - curve = PlotCurveItem(x=x, y=y, **curveArgs) - curve.setParentItem(self) - self.curves.append(curve) + self.curve.show() + else: + self.curve.hide() + #curve = PlotCurveItem(x=x, y=y, **curveArgs) + #curve.setParentItem(self) + #self.curves.append(curve) + self.scatter.setData(x=x, y=y, **scatterArgs) if scatterArgs['symbol'] is not None: - sp = ScatterPlotItem(x=x, y=y, **scatterArgs) - sp.setParentItem(self) - self.scatters.append(sp) + self.scatter.show() + else: + self.scatter.hide() + #sp = ScatterPlotItem(x=x, y=y, **scatterArgs) + #sp.setParentItem(self) + #self.scatters.append(sp) def getData(self): @@ -335,15 +391,17 @@ class PlotDataItem(GraphicsObject): def clear(self): - for i in self.curves+self.scatters: - if i.scene() is not None: - i.scene().removeItem(i) - self.curves = [] - self.scatters = [] + #for i in self.curves+self.scatters: + #if i.scene() is not None: + #i.scene().removeItem(i) + #self.curves = [] + #self.scatters = [] self.xData = None self.yData = None self.xDisp = None self.yDisp = None + self.curve.setData([]) + self.scatter.setData([]) def appendData(self, *args, **kargs): pass diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index 85427eb4..f40db2fa 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -2,6 +2,8 @@ from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Point import Point import pyqtgraph.functions as fn from GraphicsObject import GraphicsObject +import numpy as np +import scipy.stats __all__ = ['ScatterPlotItem', 'SpotItem'] class ScatterPlotItem(GraphicsObject): @@ -10,74 +12,265 @@ class ScatterPlotItem(GraphicsObject): sigClicked = QtCore.Signal(object, object) ## self, points sigPlotChanged = QtCore.Signal(object) - def __init__(self, spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=7, - symbol=None, identical=False, data=None): - + def __init__(self, *args, **kargs): """ - Arguments: - spots: list of dicts. Each dict specifies parameters for a single spot: - {'pos': (x,y), 'size', 'pen', 'brush', 'symbol'} - x,y: array of x,y values. Alternatively, specify spots['pos'] = (x,y) - pxMode: If True, spots are always the same size regardless of scaling, and size is given in px. + Accepts the same arguments as setData() + """ + + GraphicsObject.__init__(self) + self.data = None + self.spots = [] + self.bounds = [None, None] + self.opts = {} + self.spotsValid = False + self._spotPixmap = None + + self.setPen(200,200,200) + self.setBrush(100,100,150) + self.setSymbol('o') + self.setSize(7) + self.setPxMode(True) + self.setIdentical(False) + + self.setData(*args, **kargs) + + + def setData(self, *args, **kargs): + """ + Ordered Arguments: + If there is only one unnamed argument, it will be interpreted like the 'spots' argument. + + If there are two unnamed arguments, they will be interpreted as sequences of x and y values. + + Keyword Arguments: + *spots*: Optional list of dicts. Each dict specifies parameters for a single spot: + {'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method + of passing in data for the corresponding arguments. + *x*,*y*: 1D arrays of x,y values. + *pos*: 2D structure of x,y pairs (such as Nx2 array or list of tuples) + *pxMode*: If True, spots are always the same size regardless of scaling, and size is given in px. Otherwise, size is in scene coordinates and the spots scale with the view. - identical: If True, all spots are forced to look identical. + Default is True + *identical*: If True, all spots are forced to look identical. This can result in performance enhancement. - - symbol can be one of: - 'o' circle + Default is False + *symbol* can be one (or a list) of: + 'o' circle (default) 's' square 't' triangle 'd' diamond '+' plus + + *pen*: The pen (or list of pens) to use for drawing spot outlines. + *brush*: The brush (or list of brushes) to use for filling spots. + *size*: The size (or list of sizes) of spots. If *pxMode* is True, this value is in pixels. Otherwise, + it is in the item's local coordinate system. + *data*: a list of python objects used to uniquely identify each spot. """ + self.clear() + - GraphicsObject.__init__(self) - self.spots = [] - self.range = [[0,0], [0,0]] - self.identical = identical - self._spotPixmap = None + ## deal with non-keyword arguments + if len(args) == 1: + kargs['spots'] = args[0] + elif len(args) == 2: + kargs['x'] = args[0] + kargs['y'] = args[1] + elif len(args) > 2: + raise Exception('Only accepts up to two non-keyword arguments.') - if brush == 'default': - self.brush = QtGui.QBrush(QtGui.QColor(100, 100, 150)) + ## convert 'pos' argument to 'x' and 'y' + if 'pos' in kargs: + pos = kargs['pos'] + if isinstance(pos, np.ndarray): + kargs['x'] = pos[:,0] + kargs['y'] = pos[:,1] + else: + x = [] + y = [] + for p in pos: + if isinstance(p, QtCore.QPointF): + x.append(p.x()) + y.append(p.y()) + else: + x.append(p[0]) + y.append(p[1]) + kargs['x'] = x + kargs['y'] = y + + ## determine how many spots we have + if 'spots' in kargs: + numPts = len(kargs['spots']) + elif 'y' in kargs and kargs['y'] is not None: + numPts = len(kargs['y']) else: - self.brush = fn.mkBrush(brush) + kargs['x'] = [] + kargs['y'] = [] + numPts = 0 - if pen == 'default': - self.pen = QtGui.QPen(QtGui.QColor(200, 200, 200)) - else: - self.pen = fn.mkPen(pen) + ## create empty record array + self.data = np.empty(numPts, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('data', object), ('spot', object)]) + self.data['size'] = -1 ## indicates use default size + self.data['symbol'] = '' + self.data['pen'] = None + self.data['brush'] = None + self.data['data'] = None - self.symbol = symbol - self.size = size + if 'spots' in kargs: + spots = kargs['spots'] + for i in xrange(len(spots)): + spot = spots[i] + for k in spot: + if k == 'pen': + self.data[i][k] = fn.mkPen(spot[k]) + elif k == 'brush': + self.data[i][k] = fn.mkBrush(spot[k]) + elif k == 'pos': + pos = spot[k] + if isinstance(pos, QtCore.QPointF): + x,y = pos.x(), pos.y() + else: + x,y = pos[0], pos[1] + self.data[i]['x'] = x + self.data[i]['y'] = y + elif k in ['x', 'y', 'size', 'symbol', 'data']: + self.data[i][k] = spot[k] + else: + raise Exception("Unknown spot parameter: %s" % k) + elif 'y' in kargs: + self.data['x'] = kargs['x'] + self.data['y'] = kargs['y'] - self.pxMode = pxMode - if spots is not None or x is not None: - self.setPoints(spots, x, y, data) - - #self.optimize = optimize - #if optimize: - #self.spotImage = QtGui.QImage(size, size, QtGui.QImage.Format_ARGB32_Premultiplied) - #self.spotImage.fill(0) - #p = QtGui.QPainter(self.spotImage) - #p.setRenderHint(p.Antialiasing) - #p.setBrush(brush) - #p.setPen(pen) - #p.drawEllipse(0, 0, size, size) - #p.end() - #self.optimizePixmap = QtGui.QPixmap(self.spotImage) - #self.optimizeFragments = [] - #self.setFlags(self.flags() | self.ItemIgnoresTransformations) + + ## Set any extra parameters provided in keyword arguments + for k in ['pxMode', 'identical', 'pen', 'brush', 'symbol', 'size']: + if k in kargs: + setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) + setMethod(kargs[k]) + self.updateSpots() + + + + + + + + + #pen = kargs.get('pen', (200,200,200)) + #brush = kargs.get('pen', (100,100,150)) + + #if hasattr(pen, '__len__'): + #pen = map(pg.mkPen(pen)) + #self.data['pen'] = pen + + #if hasattr(pen, '__len__'): + #brush = map(pg.mkPen(pen)) + #self.data['brush'] = pen + + #self.data['size'] = kargs.get('size', 7) + #self.data['symbol'] = kargs.get('symbol', 'o') + + + + #if spots is not None and len(spots) > 0: + #spot = spots[0] + #for k in spot: + #self.data[k] = [] + #for spot in spots: + #for k,v in spot.iteritems(): + #self.data[k].append(v) + + def setPoints(self, *args, **kargs): + """Deprecated; use setData""" + return self.setData(*args, **kargs) + + #def setPoints(self, spots=None, x=None, y=None, data=None): + #""" + #Remove all existing points in the scatter plot and add a new set. + #Arguments: + #spots - list of dicts specifying parameters for each spot + #[ {'pos': (x,y), 'pen': 'r', ...}, ...] + #x, y - arrays specifying location of spots to add. + #all other parameters (pen, symbol, etc.) will be set to the default + #values for this scatter plot. + #these arguments are IGNORED if 'spots' is specified + #data - list of arbitrary objects to be assigned to spot.data for each spot + #(this is useful for identifying spots that are clicked on) + #""" + #self.clear() + #self.bounds = [[0,0],[0,0]] + #self.addPoints(spots, x, y, data) + def implements(self, interface=None): ints = ['plotData'] if interface is None: return ints return interface in ints + def setPen(self, *args, **kargs): + if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): + pens = args[0] + if self.data is None: + raise Exception("Must set data before setting multiple pens.") + if len(pens) != len(self.data): + raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(self.data))) + for i in xrange(len(pens)): + self.data[i]['pen'] = fn.mkPen(pens[i]) + else: + self.opts['pen'] = fn.mkPen(*args, **kargs) + self.updateSpots() + + def setBrush(self, *args, **kargs): + if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): + brushes = args[0] + if self.data is None: + raise Exception("Must set data before setting multiple brushes.") + if len(brushes) != len(self.data): + raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(self.data))) + for i in xrange(len(brushes)): + self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs) + else: + self.opts['brush'] = fn.mkBrush(*args, **kargs) + self.updateSpots() + + def setSymbol(self, symbol): + if isinstance(symbol, np.ndarray) or isinstance(symbol, list): + symbols = symbol + if self.data is None: + raise Exception("Must set data before setting multiple symbols.") + if len(symbols) != len(self.data): + raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(self.data))) + self.data['symbol'] = symbols + else: + self.opts['symbol'] = symbol + self.updateSpots() + + def setSize(self, size): + if isinstance(size, np.ndarray) or isinstance(size, list): + sizes = size + if self.data is None: + raise Exception("Must set data before setting multiple sizes.") + if len(sizes) != len(self.data): + raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(self.data))) + self.data['size'] = sizes + else: + self.opts['size'] = size + self.updateSpots() + + def setIdentical(self, ident): + self.opts['identical'] = ident + self.updateSpots() + def setPxMode(self, mode): - self.pxMode = mode - + self.opts['pxMode'] = mode + self.updateSpots() + + def updateSpots(self): + self.spotsValid = False + self.update() + def clear(self): for i in self.spots: i.setParentItem(None) @@ -85,73 +278,113 @@ class ScatterPlotItem(GraphicsObject): if s is not None: s.removeItem(i) self.spots = [] + self.data = None + self.spotsValid = False + self.bounds = [None, None] - def getRange(self, ax, percent): - return self.range[ax] + def dataBounds(self, ax, frac=1.0): + if frac >= 1.0 and self.bounds[ax] is not None: + return self.bounds[ax] - def setPoints(self, spots=None, x=None, y=None, data=None): - """ - Remove all existing points in the scatter plot and add a new set. - Arguments: - spots - list of dicts specifying parameters for each spot - [ {'pos': (x,y), 'pen': 'r', ...}, ...] - x, y - arrays specifying location of spots to add. - all other parameters (pen, symbol, etc.) will be set to the default - values for this scatter plot. - these arguments are IGNORED if 'spots' is specified - data - list of arbitrary objects to be assigned to spot.data for each spot - (this is useful for identifying spots that are clicked on) - """ - self.clear() - self.range = [[0,0],[0,0]] - self.addPoints(spots, x, y, data) - - def addPoints(self, spots=None, x=None, y=None, data=None): - xmn = ymn = xmx = ymx = None - if spots is not None: - n = len(spots) + if self.data is None or len(self.data) == 0: + return (None, None) + + if ax == 0: + d = self.data['x'] + elif ax == 1: + d = self.data['y'] + + if frac >= 1.0: + minIndex = np.argmin(d) + maxIndex = np.argmax(d) + minVal = d[minIndex] + maxVal = d[maxIndex] + if not self.opts['pxMode']: + minVal -= self.data[minIndex]['size'] + maxVal += self.data[maxIndex]['size'] + self.bounds[ax] = (minVal, maxVal) + return self.bounds[ax] + elif frac <= 0.0: + raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: - n = len(x) + return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) + + + + + def addPoints(self, *args, **kargs): + """ + Add new points to the scatter plot. + Arguments are the same as setData() + Note: this is expensive; plenty of room for optimization here. + """ + if self.data is None: + self.setData(*args, **kargs) + return + - for i in range(n): - if spots is not None: - s = spots[i] - pos = Point(s['pos']) - else: - s = {} - pos = Point(x[i], y[i]) - if data is not None: - s['data'] = data[i] - - size = s.get('size', self.size) - if self.pxMode: + data1 = self.data[:] + #range1 = [self.bounds[0][:], self.bounds[1][:]] + self.setData(*args, **kargs) + newData = np.empty(len(self.data) + len(data1), dtype=self.data.dtype) + newData[:len(data1)] = data1 + newData[len(data1):] = self.data + #self.bounds = [ + #[min(self.bounds[0][0], range1[0][0]), max(self.bounds[0][1], range1[0][1])], + #[min(self.bounds[1][0], range1[1][0]), max(self.bounds[1][1], range1[1][1])], + #] + self.data = newData + self.sigPlotChanged.emit(self) + + + def generateSpots(self): + xmn = ymn = xmx = ymx = None + + ## apply defaults + size = self.data['size'].copy() + size[size<0] = self.opts['size'] + + pen = self.data['pen'].copy() + pen[pen<0] = self.opts['pen'] ## note pen<0 checks for pen==None + + brush = self.data['brush'].copy() + brush[brush<0] = self.opts['brush'] + + symbol = self.data['symbol'].copy() + symbol[symbol==''] = self.opts['symbol'] + + for i in xrange(len(self.data)): + s = self.data[i] + pos = Point(s['x'], s['y']) + if self.opts['pxMode']: psize = 0 else: - psize = size - if xmn is None: - xmn = pos[0]-psize - xmx = pos[0]+psize - ymn = pos[1]-psize - ymx = pos[1]+psize - else: - xmn = min(xmn, pos[0]-psize) - xmx = max(xmx, pos[0]+psize) - ymn = min(ymn, pos[1]-psize) - ymx = max(ymx, pos[1]+psize) - #print pos, xmn, xmx, ymn, ymx - brush = s.get('brush', self.brush) - pen = s.get('pen', self.pen) - pen.setCosmetic(True) - symbol = s.get('symbol', self.symbol) - data2 = s.get('data', None) - item = self.mkSpot(pos, size, self.pxMode, brush, pen, data2, symbol=symbol, index=len(self.spots)) + psize = size[i] + + #if xmn is None: + #xmn = pos[0]-psize + #xmx = pos[0]+psize + #ymn = pos[1]-psize + #ymx = pos[1]+psize + #else: + #xmn = min(xmn, pos[0]-psize) + #xmx = max(xmx, pos[0]+psize) + #ymn = min(ymn, pos[1]-psize) + #ymx = max(ymx, pos[1]+psize) + + item = self.mkSpot(pos, size[i], self.opts['pxMode'], brush[i], pen[i], s['data'], symbol=symbol[i], index=len(self.spots)) self.spots.append(item) + self.data[i]['spot'] = item #if self.optimize: #item.hide() #frag = QtGui.QPainter.PixmapFragment.create(pos, QtCore.QRectF(0, 0, size, size)) #self.optimizeFragments.append(frag) - self.range = [[xmn, xmx], [ymn, ymx]] + + #self.bounds = [[xmn, xmx], [ymn, ymx]] + self.spotsValid = True + self.sigPlotChanged.emit(self) + #def setPointSize(self, size): #for s in self.spots: @@ -166,24 +399,22 @@ class ScatterPlotItem(GraphicsObject): #p.drawPixmapFragments(self.optimizeFragments, self.optimizePixmap) def paint(self, *args): - pass + if not self.spotsValid: + self.generateSpots() def spotPixmap(self): ## If all spots are identical, return the pixmap to use for all spots ## Otherwise return None - if not self.identical: + if not self.opts['identical']: return None if self._spotPixmap is None: - #print 'spotPixmap' - spot = SpotItem(size=self.size, pxMode=True, brush=self.brush, pen=self.pen, symbol=self.symbol) - #self._spotPixmap = PixmapSpotItem.makeSpotImage(self.size, self.pen, self.brush, self.symbol) + spot = SpotItem(size=self.opts['size'], pxMode=True, brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol']) self._spotPixmap = spot.pixmap return self._spotPixmap def mkSpot(self, pos, size, pxMode, brush, pen, data, symbol=None, index=None): ## Make and return a SpotItem (or PixmapSpotItem if in pxMode) - brush = fn.mkBrush(brush) pen = fn.mkPen(pen) if pxMode: @@ -198,10 +429,19 @@ class ScatterPlotItem(GraphicsObject): return item def boundingRect(self): - ((xmn, xmx), (ymn, ymx)) = self.range - if xmn is None or xmx is None or ymn is None or ymx is None: - return QtCore.QRectF() + (xmn, xmx) = self.dataBounds(ax=0) + (ymn, ymx) = self.dataBounds(ax=1) + if xmn is None or xmx is None: + xmn = 0 + xmx = 0 + if ymn is None or ymx is None: + ymn = 0 + ymx = 0 return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) + + #if xmn is None or xmx is None or ymn is None or ymx is None: + #return QtCore.QRectF() + #return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) #return QtCore.QRectF(xmn-1, ymn-1, xmx-xmn+2, ymx-ymn+2) #def pointClicked(self, point): @@ -222,7 +462,7 @@ class ScatterPlotItem(GraphicsObject): sx = sp.x() sy = sp.y() s2x = s2y = ss * 0.5 - if self.pxMode: + if self.opts['pxMode']: s2x *= pw s2y *= ph if x > sx-s2x and x < sx+s2x and y > sy-s2y and y < sy+s2y: @@ -281,10 +521,18 @@ class SpotItem(GraphicsObject): GraphicsObject.__init__(self) self.pxMode = pxMode + try: + symbol = int(symbol) + except: + pass + if symbol is None: symbol = 'o' ## circle by default elif isinstance(symbol, int): ## allow symbols specified by integer for easy iteration symbol = ['o', 's', 't', 'd', '+'][symbol] + + + ####print 'SpotItem symbol: ', symbol self.data = data self.pen = pen @@ -294,11 +542,12 @@ class SpotItem(GraphicsObject): self.symbol = symbol #s2 = size/2. self.path = QtGui.QPainterPath() + if symbol == 'o': self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) elif symbol == 's': self.path.addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) - elif symbol is 't' or symbol is '^': + elif symbol == 't' or symbol == '^': self.path.moveTo(-0.5, -0.5) self.path.lineTo(0, 0.5) self.path.lineTo(0.5, -0.5) @@ -328,7 +577,7 @@ class SpotItem(GraphicsObject): #self.path.connectPath(self.path) #elif symbol == 'x': else: - raise Exception("Unknown spot symbol '%s'" % symbol) + raise Exception("Unknown spot symbol '%s' (type=%s)" % (str(symbol), str(type(symbol)))) #self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) if pxMode: From f80b73b173e81c4a6beb6b2d6ce2d8b3a7c7358f Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 18 Mar 2012 19:48:40 -0400 Subject: [PATCH 025/238] Buxfixes - initialization error in plotcurveitem - performance fix for plotdataitem --- graphicsItems/PlotCurveItem.py | 3 ++- graphicsItems/PlotDataItem.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index 8f6bacd7..fcd03448 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -195,7 +195,8 @@ class PlotCurveItem(GraphicsObject): for k in ['x', 'y']: data = kargs[k] if isinstance(data, list): - kargs['k'] = np.array(data) + data = np.array(data) + kargs[k] = data if not isinstance(data, np.ndarray) or data.ndim > 1: raise Exception("Plot data must be 1D ndarray.") if 'complex' in str(data.dtype): diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index abb197eb..a8a46c4f 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -322,8 +322,8 @@ class PlotDataItem(GraphicsObject): x,y = self.getData() - self.curve.setData(x=x, y=y, **curveArgs) - if curveArgs['pen'] is not None or curveArgs['brush'] is not None: + if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None): + self.curve.setData(x=x, y=y, **curveArgs) self.curve.show() else: self.curve.hide() @@ -331,8 +331,8 @@ class PlotDataItem(GraphicsObject): #curve.setParentItem(self) #self.curves.append(curve) - self.scatter.setData(x=x, y=y, **scatterArgs) if scatterArgs['symbol'] is not None: + self.scatter.setData(x=x, y=y, **scatterArgs) self.scatter.show() else: self.scatter.hide() From 48929a2aa673e8574680f04b5928614e21797691 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 19 Mar 2012 23:02:29 -0400 Subject: [PATCH 026/238] Minor updates: GraphicsObject - corrected bug in viewPos() method WidgetGroup - allow bound methods in interfaces parametertree - fixed crash when calling remove from context menu --- WidgetGroup.py | 19 +++++++++++++++++-- graphicsItems/GraphicsItemMethods.py | 2 +- parametertree/Parameter.py | 4 ++++ parametertree/ParameterItem.py | 8 ++++++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/WidgetGroup.py b/WidgetGroup.py index 32b952e6..48a3778e 100644 --- a/WidgetGroup.py +++ b/WidgetGroup.py @@ -255,7 +255,14 @@ class WidgetGroup(QtCore.QObject): if getFunc is None: return None - val = getFunc(w) + ## if the getter function provided in the interface is a bound method, + ## then just call the method directly. Otherwise, pass in the widget as the first arg + ## to the function. + if inspect.ismethod(getFunc) and getFunc.im_self is not None: + val = getFunc() + else: + val = getFunc(w) + if self.scales[w] is not None: val /= self.scales[w] #if isinstance(val, QtCore.QString): @@ -273,7 +280,15 @@ class WidgetGroup(QtCore.QObject): setFunc = WidgetGroup.classes[type(w)][2] else: setFunc = w.widgetGroupInterface()[2] - setFunc(w, v) + + ## if the setter function provided in the interface is a bound method, + ## then just call the method directly. Otherwise, pass in the widget as the first arg + ## to the function. + if inspect.ismethod(setFunc) and setFunc.im_self is not None: + setFunc(v) + else: + setFunc(w, v) + #name = self.widgetList[w] #if name in self.cache and (self.cache[name] != v1): #print "%s: Cached value %s != set value %s" % (name, str(self.cache[name]), str(v1)) diff --git a/graphicsItems/GraphicsItemMethods.py b/graphicsItems/GraphicsItemMethods.py index 0d8e9fca..ca9c7a43 100644 --- a/graphicsItems/GraphicsItemMethods.py +++ b/graphicsItems/GraphicsItemMethods.py @@ -229,7 +229,7 @@ class GraphicsItemMethods(object): return Point(QtGui.QGraphicsObject.pos(self)) def viewPos(self): - return self.mapToView(self.pos()) + return self.mapToView(self.mapFromParent(self.pos())) #def itemChange(self, change, value): #ret = QtGui.QGraphicsObject.itemChange(self, change, value) diff --git a/parametertree/Parameter.py b/parametertree/Parameter.py index 39edb880..e10147fd 100644 --- a/parametertree/Parameter.py +++ b/parametertree/Parameter.py @@ -284,6 +284,10 @@ class Parameter(QtCore.QObject): for ch in self.childs[:]: self.removeChild(ch) + def children(self): + ## warning -- this overrides QObject.children + return self.childs[:] + def parentChanged(self, parent): self._parent = parent self.sigParentChanged.emit(self, parent) diff --git a/parametertree/ParameterItem.py b/parametertree/ParameterItem.py index 605e6317..7c0db075 100644 --- a/parametertree/ParameterItem.py +++ b/parametertree/ParameterItem.py @@ -38,7 +38,7 @@ class ParameterItem(QtGui.QTreeWidgetItem): flags |= QtCore.Qt.ItemIsEditable self.contextMenu.addAction('Rename').triggered.connect(self.editName) if opts.get('removable', False): - self.contextMenu.addAction("Remove").triggered.connect(self.param.remove) + self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) ## handle movable / dropEnabled options if opts.get('movable', False): @@ -144,5 +144,9 @@ class ParameterItem(QtGui.QTreeWidgetItem): """Called when this item has been selected (sel=True) OR deselected (sel=False)""" pass - + def requestRemove(self): + ## called when remove is selected from the context menu. + ## we need to delay removal until the action is complete + ## since destroying the menu in mid-action will cause a crash. + QtCore.QTimer.singleShot(0, self.param.remove) From d1521dc7ed4c08656715a860f4c23be9ab8e1dd8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 20 Mar 2012 00:33:58 -0400 Subject: [PATCH 027/238] ROI fix - filled in missing rotate() method --- graphicsItems/ROI.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index 833baa94..1fc38c55 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -62,11 +62,11 @@ class ROI(GraphicsObject): self.handlePen = QtGui.QPen(QtGui.QColor(150, 255, 255)) self.handles = [] - self.state = {'pos': pos, 'size': size, 'angle': angle} ## angle is in degrees for ease of Qt integration + self.state = {'pos': Point(0,0), 'size': Point(1,1), 'angle': 0} ## angle is in degrees for ease of Qt integration self.lastState = None self.setPos(pos) - #self.rotate(-angle * 180. / np.pi) - self.rotate(angle) + self.setAngle(angle) + self.setSize(size) self.setZValue(10) self.isMoving = False @@ -237,9 +237,8 @@ class ROI(GraphicsObject): #if 'update' not in kargs or kargs['update'] is True: #self.stateChanged() - def rotate(self, angle, center=(0,0), angleSnap=False, update=True, finish=True): - pass - #self.setAngle(self.angle()+angle, update=update, finish=finish) + def rotate(self, angle, update=True, finish=True): + self.setAngle(self.angle()+angle, update=update, finish=finish) def addTranslateHandle(self, pos, axes=None, item=None, name=None): From 97740c2376c6238dcc1ee52d8f561c0ef9d5d428 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 20 Mar 2012 22:39:37 -0400 Subject: [PATCH 028/238] bugfix for linking ViewBoxes --- graphicsItems/ViewBox/ViewBoxMenu.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/graphicsItems/ViewBox/ViewBoxMenu.py b/graphicsItems/ViewBox/ViewBoxMenu.py index c837c1c3..09027029 100644 --- a/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/graphicsItems/ViewBox/ViewBoxMenu.py @@ -118,7 +118,12 @@ class ViewBoxMenu(QtGui.QMenu): view = state['linkedViews'][i] if view is None: view = '' - ind = c.findText(view) + + if isinstance(view, basestring): + ind = c.findText(view) + else: + ind = c.findText(view.name) + if ind == -1: ind = 0 c.setCurrentIndex(ind) From 7c94b5a7026d5ce241b48adc30f38b09e8ec12b7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 20 Mar 2012 23:38:04 -0400 Subject: [PATCH 029/238] Bugfixes and example for view linking --- examples/__main__.py | 1 + examples/linkedViews.py | 48 ++++++++++++++++++++++++++++++++ graphicsItems/GraphicsLayout.py | 7 ++++- graphicsItems/LabelItem.py | 6 ++-- graphicsItems/ViewBox/ViewBox.py | 28 ++++++++++++++----- widgets/GraphicsLayoutWidget.py | 2 +- 6 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 examples/linkedViews.py diff --git a/examples/__main__.py b/examples/__main__.py index 628b93fd..95c0ebae 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -19,6 +19,7 @@ examples = OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), ('Text Item', 'text.py'), ('ViewBox', 'ViewBox.py'), + ('Linked Views', 'linkedViews.py'), ('Arrow', 'Arrow.py'), ])), ('Widgets', OrderedDict([ diff --git a/examples/linkedViews.py b/examples/linkedViews.py new file mode 100644 index 00000000..c1777aab --- /dev/null +++ b/examples/linkedViews.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +## This example demonstrates the ability to link the axes of views together +## Views can be linked manually using the context menu, but only if they are given names. + + +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg + +#QtGui.QApplication.setGraphicsSystem('raster') +app = QtGui.QApplication([]) +#mw = QtGui.QMainWindow() +#mw.resize(800,800) + +x = np.linspace(-50, 50, 1000) +y = np.sin(x) / x + +win = pg.GraphicsWindow(title="View Linking Examples") +win.resize(800,600) + +win.addLabel("Views linked at runtime:", colspan=2) +win.nextRow() + +p1 = win.addPlot(x=x, y=y, name="Plot1", title="Plot1") +p2 = win.addPlot(x=x, y=y, name="Plot2", title="Plot2 - Y linked with Plot1") +p2.setLabel('bottom', "Label to test offset") +p2.setYLink(p1) + +win.nextRow() + +p3 = win.addPlot(x=x, y=y, name="Plot3", title="Plot3 - X linked with Plot1") +p4 = win.addPlot(x=x, y=y, name="Plot4", title="Plot4 - X and Y linked with Plot1") +p3.setLabel('left', "Label to test offset") +QtGui.QApplication.processEvents() +p3.setXLink(p1) +p4.setXLink(p1) +p4.setYLink(p1) + + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() + diff --git a/graphicsItems/GraphicsLayout.py b/graphicsItems/GraphicsLayout.py index c1d28c28..9bd9f5f9 100644 --- a/graphicsItems/GraphicsLayout.py +++ b/graphicsItems/GraphicsLayout.py @@ -33,7 +33,6 @@ class GraphicsLayout(GraphicsWidget): return self.currentCol-colspan def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs): - from PlotItem import PlotItem plot = PlotItem(**kargs) self.addItem(plot, row, col, rowspan, colspan) return plot @@ -43,6 +42,11 @@ class GraphicsLayout(GraphicsWidget): self.addItem(vb, row, col, rowspan, colspan) return vb + def addLabel(self, text, row=None, col=None, rowspan=1, colspan=1, **kargs): + text = LabelItem(text, **kargs) + self.addItem(text, row, col, rowspan, colspan) + return text + def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): if row is None: @@ -95,3 +99,4 @@ class GraphicsLayout(GraphicsWidget): ## Must be imported at the end to avoid cyclic-dependency hell: from ViewBox import ViewBox from PlotItem import PlotItem +from LabelItem import LabelItem \ No newline at end of file diff --git a/graphicsItems/LabelItem.py b/graphicsItems/LabelItem.py index c9d88dd6..6a2f6b48 100644 --- a/graphicsItems/LabelItem.py +++ b/graphicsItems/LabelItem.py @@ -10,12 +10,11 @@ class LabelItem(GraphicsWidget): GraphicsWidget displaying text. Used mainly as axis labels, titles, etc. - Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use QGraphicsTextItem - with the flag ItemIgnoresTransformations set. + Note: To display text inside a scaled view (ViewBox, PlotWidget, etc) use TextItem """ - def __init__(self, text, parent=None, **args): + def __init__(self, text, parent=None, angle=0, **args): GraphicsWidget.__init__(self, parent) self.item = QtGui.QGraphicsTextItem(self) self.opts = args @@ -26,6 +25,7 @@ class LabelItem(GraphicsWidget): self.opts['color'] = fn.colorStr(args['color'])[:6] self.sizeHint = {} self.setText(text) + self.setAngle(angle) def setAttr(self, attr, value): diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 0bc84c2c..2bddf2fe 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -480,11 +480,13 @@ class ViewBox(GraphicsWidget): self.linksBlocked = b ## prevents recursive plot-change propagation def linkedXChanged(self): + ## called when x range of linked view has changed view = self.state['linkedViews'][0] self.linkedViewChanged(view, ViewBox.XAxis) def linkedYChanged(self): - view = self.state['linkedViews'][0] + ## called when y range of linked view has changed + view = self.state['linkedViews'][1] self.linkedViewChanged(view, ViewBox.YAxis) @@ -502,15 +504,27 @@ class ViewBox(GraphicsWidget): view.blockLink(True) try: if axis == ViewBox.XAxis: - upp = float(vr.width()) / vg.width() - x1 = vr.left() + (sg.x()-vg.x()) * upp - x2 = x1 + sg.width() * upp + overlap = min(sg.right(), vg.right()) - max(sg.left(), vg.left()) + if overlap < min(vg.width()/3, sg.width()/3): ## if less than 1/3 of views overlap, + ## then just replicate the view + x1 = vr.left() + x2 = vr.right() + else: ## views overlap; line them up + upp = float(vr.width()) / vg.width() + x1 = vr.left() + (sg.x()-vg.x()) * upp + x2 = x1 + sg.width() * upp self.enableAutoRange(ViewBox.XAxis, False) self.setXRange(x1, x2, padding=0) else: - upp = float(vr.height()) / vg.height() - x1 = vr.bottom() + (sg.y()-vg.y()) * upp - x2 = x1 + sg.height() * upp + overlap = min(sg.bottom(), vg.bottom()) - max(sg.top(), vg.top()) + if overlap < min(vg.height()/3, sg.height()/3): ## if less than 1/3 of views overlap, + ## then just replicate the view + x1 = vr.top() + x2 = vr.bottom() + else: ## views overlap; line them up + upp = float(vr.height()) / vg.height() + x1 = vr.top() + (sg.y()-vg.y()) * upp + x2 = x1 + sg.height() * upp self.enableAutoRange(ViewBox.YAxis, False) self.setYRange(x1, x2, padding=0) finally: diff --git a/widgets/GraphicsLayoutWidget.py b/widgets/GraphicsLayoutWidget.py index 937c880a..287fbbb9 100644 --- a/widgets/GraphicsLayoutWidget.py +++ b/widgets/GraphicsLayoutWidget.py @@ -7,6 +7,6 @@ class GraphicsLayoutWidget(GraphicsView): def __init__(self, parent=None, **kargs): GraphicsView.__init__(self, parent) self.ci = GraphicsLayout(**kargs) - for n in ['nextRow', 'nextCol', 'addPlot', 'addViewBox', 'addItem', 'getItem']: + for n in ['nextRow', 'nextCol', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLabel']: setattr(self, n, getattr(self.ci, n)) self.setCentralItem(self.ci) From b78662c33e64dc86b723ef96bf6b5765f4c596bf Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 02:41:10 -0400 Subject: [PATCH 030/238] Minor updates for exporting - curves enable antialiasing when exporting to image - plotitems hide button during export --- GraphicsScene/GraphicsScene.py | 4 +- GraphicsScene/exportDialogTemplate.py | 4 +- GraphicsScene/exportDialogTemplate.ui | 2 +- exporters/Exporter.py | 68 ++++++++++++++++++++++- exporters/ImageExporter.py | 10 +++- exporters/PrintExporter.py | 59 ++++++++++++++++++++ exporters/SVGExporter.py | 6 +- exporters/__init__.py | 24 +++++++- graphicsItems/AxisItem.py | 3 + graphicsItems/PlotCurveItem.py | 18 +++++- graphicsItems/PlotItem/PlotItem.py | 56 ++++++++++++++----- graphicsItems/ViewBox/axisCtrlTemplate.py | 26 +++++---- graphicsItems/ViewBox/axisCtrlTemplate.ui | 6 +- 13 files changed, 245 insertions(+), 41 deletions(-) create mode 100644 exporters/PrintExporter.py diff --git a/GraphicsScene/GraphicsScene.py b/GraphicsScene/GraphicsScene.py index e6679c2d..9cc2491a 100644 --- a/GraphicsScene/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -59,12 +59,12 @@ class GraphicsScene(QtGui.QGraphicsScene): move in a drag. """ - - _addressCache = weakref.WeakValueDictionary() sigMouseHover = QtCore.Signal(object) ## emits a list of objects hovered over sigMouseMoved = QtCore.Signal(object) ## emits position of mouse on every move sigMouseClicked = QtCore.Signal(object) ## emitted when MouseClickEvent is not accepted by any items under the click. + _addressCache = weakref.WeakValueDictionary() + ExportDirectory = None @classmethod diff --git a/GraphicsScene/exportDialogTemplate.py b/GraphicsScene/exportDialogTemplate.py index ae90100d..60f18d0d 100644 --- a/GraphicsScene/exportDialogTemplate.py +++ b/GraphicsScene/exportDialogTemplate.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'exportDialogTemplate.ui' # -# Created: Sat Mar 10 17:54:53 2012 +# Created: Thu Mar 22 13:13:06 2012 # by: PyQt4 UI code generator 4.8.5 # # WARNING! All changes made in this file will be lost! @@ -18,7 +18,7 @@ class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) Form.resize(241, 367) - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8)) self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout.setSpacing(0) self.gridLayout.setObjectName(_fromUtf8("gridLayout")) diff --git a/GraphicsScene/exportDialogTemplate.ui b/GraphicsScene/exportDialogTemplate.ui index 0d840253..c81c8831 100644 --- a/GraphicsScene/exportDialogTemplate.ui +++ b/GraphicsScene/exportDialogTemplate.ui @@ -11,7 +11,7 @@ - Form + Export diff --git a/exporters/Exporter.py b/exporters/Exporter.py index 25ae367c..abcddd28 100644 --- a/exporters/Exporter.py +++ b/exporters/Exporter.py @@ -75,8 +75,74 @@ class Exporter(object): else: return self.item.mapRectToDevice(self.item.boundingRect()) + def setExportMode(self, export, opts=None): + """ + Call setExportMode(export, opts) on all items that will + be painted during the export. This informs the item + that it is about to be painted for export, allowing it to + alter its appearance temporarily - + + *export* - bool; must be True before exporting and False afterward + *opts* - dict; common parameters are 'antialias' and 'background' + """ + if opts is None: + opts = {} + for item in self.getPaintItems(): + if hasattr(item, 'setExportMode'): + item.setExportMode(export, opts) + + def getPaintItems(self, root=None): + """Return a list of all items that should be painted in the correct order.""" + if root is None: + root = self.item + preItems = [] + postItems = [] + if isinstance(root, QtGui.QGraphicsScene): + childs = [i for i in root.items() if i.parentItem() is None] + rootItem = [] + else: + childs = root.childItems() + rootItem = [root] + childs.sort(lambda a,b: cmp(a.zValue(), b.zValue())) + while len(childs) > 0: + ch = childs.pop(0) + tree = self.getPaintItems(ch) + if int(ch.flags() & ch.ItemStacksBehindParent) > 0 or (ch.zValue() < 0 and int(ch.flags() & ch.ItemNegativeZStacksBehindParent) > 0): + preItems.extend(tree) + else: + postItems.extend(tree) + + return preItems + rootItem + postItems + + def render(self, painter, sourcRect, targetRect, item=None) + + #if item is None: + #item = self.item + #preItems = [] + #postItems = [] + #if isinstance(item, QtGui.QGraphicsScene): + #childs = [i for i in item.items() if i.parentItem() is None] + #rootItem = [] + #else: + #childs = item.childItems() + #rootItem = [item] + #childs.sort(lambda a,b: cmp(a.zValue(), b.zValue())) + #while len(childs) > 0: + #ch = childs.pop(0) + #if int(ch.flags() & ch.ItemStacksBehindParent) > 0 or (ch.zValue() < 0 and int(ch.flags() & ch.ItemNegativeZStacksBehindParent) > 0): + #preItems.extend(tree) + #else: + #postItems.extend(tree) + + #for ch in preItems: + #self.render(painter, sourceRect, targetRect, item=ch) + ### paint root here + #for ch in postItems: + #self.render(painter, sourceRect, targetRect, item=ch) + + + self.getScene().render(painter, QtCore.QRectF(targetRect), QtCore.QRectF(sourceRect)) #def writePs(self, fileName=None, item=None): #if fileName is None: diff --git a/exporters/ImageExporter.py b/exporters/ImageExporter.py index 138ddabb..fa9b6f6d 100644 --- a/exporters/ImageExporter.py +++ b/exporters/ImageExporter.py @@ -57,6 +57,12 @@ class ImageExporter(Exporter): bg[:,:,3] = color.alpha() self.png = pg.makeQImage(bg, alpha=True) painter = QtGui.QPainter(self.png) - self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect) + try: + self.setExportMode(True, {'antialias': self.params['antialias'], 'background': self.params['background']}) + self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect) + finally: + self.setExportMode(False) self.png.save(fileName) - painter.end() \ No newline at end of file + painter.end() + + \ No newline at end of file diff --git a/exporters/PrintExporter.py b/exporters/PrintExporter.py new file mode 100644 index 00000000..12468e9c --- /dev/null +++ b/exporters/PrintExporter.py @@ -0,0 +1,59 @@ +from Exporter import Exporter +from pyqtgraph.parametertree import Parameter +from pyqtgraph.Qt import QtGui, QtCore, QtSvg +import re + +#__all__ = ['PrintExporter'] +__all__ = [] ## Printer is disabled for now--does not work very well. + +class PrintExporter(Exporter): + Name = "Printer" + def __init__(self, item): + Exporter.__init__(self, item) + tr = self.getTargetRect() + self.params = Parameter(name='params', type='group', children=[ + {'name': 'width', 'type': 'float', 'value': 0.1, 'limits': (0, None), 'suffix': 'm', 'siPrefix': True}, + {'name': 'height', 'type': 'float', 'value': (0.1 * tr.height()) / tr.width(), 'limits': (0, None), 'suffix': 'm', 'siPrefix': True}, + ]) + self.params.param('width').sigValueChanged.connect(self.widthChanged) + self.params.param('height').sigValueChanged.connect(self.heightChanged) + + def widthChanged(self): + sr = self.getSourceRect() + ar = sr.height() / sr.width() + self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged) + + def heightChanged(self): + sr = self.getSourceRect() + ar = sr.width() / sr.height() + self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged) + + def parameters(self): + return self.params + + def export(self, fileName=None): + printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) + dialog = QtGui.QPrintDialog(printer) + dialog.setWindowTitle("Print Document") + if dialog.exec_() != QtGui.QDialog.Accepted: + return; + + #self.svg.setSize(QtCore.QSize(100,100)) + #self.svg.setResolution(600) + res = printer.resolution() + rect = printer.pageRect() + center = rect.center() + h = self.params['height'] * res * 100. / 2.54 + w = self.params['width'] * res * 100. / 2.54 + x = center.x() - w/2. + y = center.y() - h/2. + + targetRect = QtCore.QRect(x, y, w, h) + sourceRect = self.getSourceRect() + painter = QtGui.QPainter(printer) + try: + self.setExportMode(True) + self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect) + finally: + self.setExportMode(False) + painter.end() diff --git a/exporters/SVGExporter.py b/exporters/SVGExporter.py index 40489628..9158d00c 100644 --- a/exporters/SVGExporter.py +++ b/exporters/SVGExporter.py @@ -42,7 +42,11 @@ class SVGExporter(Exporter): targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) sourceRect = self.getSourceRect() painter = QtGui.QPainter(self.svg) - self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect) + try: + self.setExportMode(True) + self.render(painter, QtCore.QRectF(targetRect), sourceRect) + finally: + self.setExportMode(False) painter.end() ## Workaround to set pen widths correctly diff --git a/exporters/__init__.py b/exporters/__init__.py index a73ef2c2..e6ac379c 100644 --- a/exporters/__init__.py +++ b/exporters/__init__.py @@ -1,6 +1,24 @@ -from SVGExporter import * -from ImageExporter import * -Exporters = [SVGExporter, ImageExporter] +Exporters = [] + +import os, sys +d = os.path.split(__file__)[0] +files = [] +for f in os.listdir(d): + if os.path.isdir(os.path.join(d, f)): + files.append(f) + elif f[-3:] == '.py' and f not in ['__init__.py', 'Exporter.py']: + files.append(f[:-3]) + +for modName in files: + mod = __import__(modName, globals(), locals(), fromlist=['*']) + if hasattr(mod, '__all__'): + names = mod.__all__ + else: + names = [n for n in dir(mod) if n[0] != '_'] + for k in names: + if hasattr(mod, k): + Exporters.append(getattr(mod, k)) + def listExporters(): return Exporters[:] diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 31e43b75..4015f8fe 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -261,6 +261,9 @@ class AxisItem(GraphicsWidget): def drawPicture(self, p): + p.setRenderHint(p.Antialiasing, False) + p.setRenderHint(p.TextAntialiasing, True) + prof = debug.Profiler("AxisItem.paint", disabled=True) p.setPen(self.pen) diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index fcd03448..12ac044c 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -27,6 +27,8 @@ class PlotCurveItem(GraphicsObject): self.clear() self.path = None self.fillPath = None + self.exportOpts = False + self.antialias = False if y is not None: self.updateData(y, x) @@ -364,6 +366,14 @@ class PlotCurveItem(GraphicsObject): #pen.setColor(c) ##pen.setCosmetic(True) + if self.exportOpts is not False: + aa = self.exportOpts['antialias'] + else: + aa = self.antialias + + p.setRenderHint(p.Antialiasing, aa) + + if sp is not None: p.setPen(sp) p.drawPath(path) @@ -410,7 +420,13 @@ class PlotCurveItem(GraphicsObject): ev.accept() self.sigClicked.emit(self) - + def setExportMode(self, export, opts): + if export: + self.exportOpts = opts + if 'antialias' not in opts: + self.exportOpts['antialias'] = True + else: + self.exportOpts = False class ROIPlotItem(PlotCurveItem): """Plot curve that monitors an ROI and image for changes to automatically replot.""" diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index e6e76cb4..78e213d7 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -64,6 +64,20 @@ class PlotItem(GraphicsWidget): managers = {} def __init__(self, parent=None, name=None, labels=None, title=None, **kargs): + """ + Create a new PlotItem. All arguments are optional. + Any extra keyword arguments are passed to PlotItem.plot(). + + Arguments: + *title* - Title to display at the top of the item. Html is allowed. + *labels* - A dictionary specifying the axis labels to display. + {'left': (args), 'bottom': (args), ...} + The name of each axis and the corresponding arguments are passed to PlotItem.setLabel() + Optionally, PlotItem my also be initialized with the keyword arguments left, + right, top, or bottom to achieve the same effect. + *name* - Registers a name for this view so that others may link to it + """ + GraphicsWidget.__init__(self, parent) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) @@ -250,12 +264,16 @@ class PlotItem(GraphicsWidget): #if name is not None: #self.registerPlot(name) - - if labels is not None: - for k in labels: - if isinstance(labels[k], basestring): - labels[k] = (labels[k],) - self.setLabel(k, *labels[k]) + if labels is None: + labels = {} + for label in self.scales.keys(): + if label in kargs: + labels[label] = kargs[label] + del kargs[label] + for k in labels: + if isinstance(labels[k], basestring): + labels[k] = (labels[k],) + self.setLabel(k, *labels[k]) if title is not None: self.setTitle(title) @@ -263,7 +281,7 @@ class PlotItem(GraphicsWidget): if len(kargs) > 0: self.plot(**kargs) - self.enableAutoRange() + #self.enableAutoRange() def implements(self, interface=None): return interface in ['ViewBoxWrapper'] @@ -365,6 +383,8 @@ class PlotItem(GraphicsWidget): #print " Referrers are:", refs #raise + + def updateGrid(self, *args): g = self.ctrl.gridGroup.isChecked() if g: @@ -1313,18 +1333,24 @@ class PlotItem(GraphicsWidget): return c - def saveSvgClicked(self): - self.writeSvg() + #def saveSvgClicked(self): + #self.writeSvg() - def saveSvgCurvesClicked(self): - self.writeSvgCurves() + #def saveSvgCurvesClicked(self): + #self.writeSvgCurves() - def saveImgClicked(self): - self.writeImage() + #def saveImgClicked(self): + #self.writeImage() - def saveCsvClicked(self): - self.writeCsv() + #def saveCsvClicked(self): + #self.writeCsv() + def setExportMode(self, export, opts): + if export: + self.autoBtn.hide() + else: + self.autoBtn.show() + #class PlotWidgetManager(QtCore.QObject): diff --git a/graphicsItems/ViewBox/axisCtrlTemplate.py b/graphicsItems/ViewBox/axisCtrlTemplate.py index b229bf3d..20e2a8c9 100644 --- a/graphicsItems/ViewBox/axisCtrlTemplate.py +++ b/graphicsItems/ViewBox/axisCtrlTemplate.py @@ -2,8 +2,8 @@ # Form implementation generated from reading ui file 'axisCtrlTemplate.ui' # -# Created: Fri Jan 20 12:41:24 2012 -# by: PyQt4 UI code generator 4.8.3 +# Created: Thu Mar 22 13:13:14 2012 +# by: PyQt4 UI code generator 4.8.5 # # WARNING! All changes made in this file will be lost! @@ -19,41 +19,51 @@ class Ui_Form(object): Form.setObjectName(_fromUtf8("Form")) Form.resize(182, 120) Form.setMaximumSize(QtCore.QSize(200, 16777215)) + Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout.setSpacing(0) self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.mouseCheck = QtGui.QCheckBox(Form) + self.mouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse Enabled", None, QtGui.QApplication.UnicodeUTF8)) self.mouseCheck.setChecked(True) self.mouseCheck.setObjectName(_fromUtf8("mouseCheck")) self.gridLayout.addWidget(self.mouseCheck, 0, 1, 1, 2) self.manualRadio = QtGui.QRadioButton(Form) + self.manualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) self.manualRadio.setObjectName(_fromUtf8("manualRadio")) self.gridLayout.addWidget(self.manualRadio, 1, 0, 1, 1) self.minText = QtGui.QLineEdit(Form) + self.minText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) self.minText.setObjectName(_fromUtf8("minText")) self.gridLayout.addWidget(self.minText, 1, 1, 1, 1) self.maxText = QtGui.QLineEdit(Form) + self.maxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) self.maxText.setObjectName(_fromUtf8("maxText")) self.gridLayout.addWidget(self.maxText, 1, 2, 1, 1) self.autoRadio = QtGui.QRadioButton(Form) + self.autoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) self.autoRadio.setChecked(True) self.autoRadio.setObjectName(_fromUtf8("autoRadio")) self.gridLayout.addWidget(self.autoRadio, 2, 0, 1, 1) self.autoPercentSpin = QtGui.QSpinBox(Form) self.autoPercentSpin.setEnabled(True) + self.autoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) self.autoPercentSpin.setMinimum(1) self.autoPercentSpin.setMaximum(100) self.autoPercentSpin.setSingleStep(1) - self.autoPercentSpin.setProperty(_fromUtf8("value"), 100) + self.autoPercentSpin.setProperty("value", 100) self.autoPercentSpin.setObjectName(_fromUtf8("autoPercentSpin")) self.gridLayout.addWidget(self.autoPercentSpin, 2, 1, 1, 2) self.autoPanCheck = QtGui.QCheckBox(Form) + self.autoPanCheck.setText(QtGui.QApplication.translate("Form", "Auto Pan Only", None, QtGui.QApplication.UnicodeUTF8)) self.autoPanCheck.setObjectName(_fromUtf8("autoPanCheck")) self.gridLayout.addWidget(self.autoPanCheck, 3, 1, 1, 2) self.linkCombo = QtGui.QComboBox(Form) + self.linkCombo.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) self.linkCombo.setObjectName(_fromUtf8("linkCombo")) self.gridLayout.addWidget(self.linkCombo, 4, 1, 1, 2) self.label = QtGui.QLabel(Form) + self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8)) self.label.setObjectName(_fromUtf8("label")) self.gridLayout.addWidget(self.label, 4, 0, 1, 1) @@ -61,13 +71,5 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.mouseCheck.setText(QtGui.QApplication.translate("Form", "Mouse Enabled", None, QtGui.QApplication.UnicodeUTF8)) - self.manualRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8)) - self.minText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.maxText.setText(QtGui.QApplication.translate("Form", "0", None, QtGui.QApplication.UnicodeUTF8)) - self.autoRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPercentSpin.setSuffix(QtGui.QApplication.translate("Form", "%", None, QtGui.QApplication.UnicodeUTF8)) - self.autoPanCheck.setText(QtGui.QApplication.translate("Form", "Auto Pan Only", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("Form", "Link Axis:", None, QtGui.QApplication.UnicodeUTF8)) + pass diff --git a/graphicsItems/ViewBox/axisCtrlTemplate.ui b/graphicsItems/ViewBox/axisCtrlTemplate.ui index f01a3f80..b463923a 100644 --- a/graphicsItems/ViewBox/axisCtrlTemplate.ui +++ b/graphicsItems/ViewBox/axisCtrlTemplate.ui @@ -94,7 +94,11 @@ - + + + QComboBox::AdjustToContents + + From 1489643a3074d990ef4f0ecb70424df608e15c22 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 02:42:02 -0400 Subject: [PATCH 031/238] GraphicsLayout: added convenience method for creating sub-layouts --- graphicsItems/GraphicsLayout.py | 11 +++++++++-- widgets/GraphicsLayoutWidget.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/graphicsItems/GraphicsLayout.py b/graphicsItems/GraphicsLayout.py index 9bd9f5f9..a35c79e3 100644 --- a/graphicsItems/GraphicsLayout.py +++ b/graphicsItems/GraphicsLayout.py @@ -26,12 +26,15 @@ class GraphicsLayout(GraphicsWidget): self.currentRow += 1 self.currentCol = 0 - def nextCol(self, colspan=1): + def nextColumn(self, colspan=1): """Advance to next column, while returning the current column number (generally only for internal use--called by addItem)""" self.currentCol += colspan return self.currentCol-colspan + def nextCol(self, *args, **kargs): + return self.nextColumn(*args, **kargs) + def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs): plot = PlotItem(**kargs) self.addItem(plot, row, col, rowspan, colspan) @@ -47,7 +50,11 @@ class GraphicsLayout(GraphicsWidget): self.addItem(text, row, col, rowspan, colspan) return text - + def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + layout = GraphicsLayout(**kargs) + self.addItem(layout, row, col, rowspan, colspan) + return layout + def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): if row is None: row = self.currentRow diff --git a/widgets/GraphicsLayoutWidget.py b/widgets/GraphicsLayoutWidget.py index 287fbbb9..b08e4383 100644 --- a/widgets/GraphicsLayoutWidget.py +++ b/widgets/GraphicsLayoutWidget.py @@ -7,6 +7,6 @@ class GraphicsLayoutWidget(GraphicsView): def __init__(self, parent=None, **kargs): GraphicsView.__init__(self, parent) self.ci = GraphicsLayout(**kargs) - for n in ['nextRow', 'nextCol', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLabel']: + for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLabel', 'addLayout']: setattr(self, n, getattr(self.ci, n)) self.setCentralItem(self.ci) From 3b2ef160711efc9694ba83ac8b637c76eaad5504 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 02:42:32 -0400 Subject: [PATCH 032/238] bugfix for view linking, example update --- examples/linkedViews.py | 2 +- graphicsItems/ViewBox/ViewBox.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/linkedViews.py b/examples/linkedViews.py index c1777aab..25e14d54 100644 --- a/examples/linkedViews.py +++ b/examples/linkedViews.py @@ -35,7 +35,7 @@ win.nextRow() p3 = win.addPlot(x=x, y=y, name="Plot3", title="Plot3 - X linked with Plot1") p4 = win.addPlot(x=x, y=y, name="Plot4", title="Plot4 - X and Y linked with Plot1") p3.setLabel('left', "Label to test offset") -QtGui.QApplication.processEvents() +#QtGui.QApplication.processEvents() p3.setXLink(p1) p4.setXLink(p1) p4.setYLink(p1) diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 2bddf2fe..26b5ae00 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -101,8 +101,8 @@ class ViewBox(GraphicsWidget): self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox.setPen(fn.mkPen((255,0,0), width=1)) self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100)) - self.addItem(self.rbScaleBox) self.rbScaleBox.hide() + self.addItem(self.rbScaleBox) self.axHistory = [] # maintain a history of zoom locations self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo" @@ -218,6 +218,8 @@ class ViewBox(GraphicsWidget): self.updateAutoRange() self.updateMatrix() self.sigStateChanged.emit(self) + #self.linkedXChanged() + #self.linkedYChanged() def viewRange(self): return [x[:] for x in self.state['viewRange']] ## return copy @@ -467,7 +469,7 @@ class ViewBox(GraphicsWidget): if view is not None: getattr(view, signal).connect(slot) - if view.autoRangeEnabled()[axis] is True: + if view.autoRangeEnabled()[axis] is not False: self.enableAutoRange(axis, False) slot() else: @@ -491,7 +493,7 @@ class ViewBox(GraphicsWidget): def linkedViewChanged(self, view, axis): - if self.linksBlocked: + if self.linksBlocked or view is None: return vr = view.viewRect() From b0d3e9a50b96a061db55577bdb62477eeafb7027 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 02:45:11 -0400 Subject: [PATCH 033/238] update to plot() function - all arguments are now passed through to PlotWindow. (this *should* be a backward-compatible change) --- __init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/__init__.py b/__init__.py index 86953817..a8ae98de 100644 --- a/__init__.py +++ b/__init__.py @@ -105,13 +105,14 @@ def plot(*args, **kargs): | All other arguments are used to plot data. (see :func:`PlotItem.plot() `) """ mkQApp() - if 'title' in kargs: - w = PlotWindow(title=kargs['title']) - del kargs['title'] - else: - w = PlotWindow() - if len(args)+len(kargs) > 0: - w.plot(*args, **kargs) + #if 'title' in kargs: + #w = PlotWindow(title=kargs['title']) + #del kargs['title'] + #else: + #w = PlotWindow() + #if len(args)+len(kargs) > 0: + #w.plot(*args, **kargs) + w = PlotWindow(*args, **kargs) plots.append(w) w.show() return w From 04291a9300627dccbbe5498ae05e033181417c31 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 02:46:59 -0400 Subject: [PATCH 034/238] reconnected viewbox range-change signals through plotitem. --- graphicsItems/PlotItem/PlotItem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 78e213d7..cef568c5 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -99,8 +99,9 @@ class PlotItem(GraphicsWidget): self.layout.setVerticalSpacing(0) self.vb = ViewBox(name=name) - #self.vb.sigXRangeChanged.connect(self.xRangeChanged) - #self.vb.sigYRangeChanged.connect(self.yRangeChanged) + self.vb.sigRangeChanged.connect(self.sigRangeChanged) + self.vb.sigXRangeChanged.connect(self.sigXRangeChanged) + self.vb.sigYRangeChanged.connect(self.sigYRangeChanged) #self.vb.sigRangeChangedManually.connect(self.enableManualScale) #self.vb.sigRangeChanged.connect(self.viewRangeChanged) From 1d66063dbedb1327da9bafc64e37ab576bf97744 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 02:49:20 -0400 Subject: [PATCH 035/238] bugfix --- exporters/Exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exporters/Exporter.py b/exporters/Exporter.py index abcddd28..1215e187 100644 --- a/exporters/Exporter.py +++ b/exporters/Exporter.py @@ -115,7 +115,7 @@ class Exporter(object): return preItems + rootItem + postItems - def render(self, painter, sourcRect, targetRect, item=None) + def render(self, painter, sourcRect, targetRect, item=None): #if item is None: #item = self.item From 2a2f19b2d59d6f0fc6d32f4be5e446200eb822c9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 03:21:04 -0400 Subject: [PATCH 036/238] bugfixes --- __init__.py | 13 ++++++++++++- examples/Arrow.py | 1 + examples/Draw.py | 1 + examples/__main__.py | 12 ++++++------ exporters/Exporter.py | 2 +- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/__init__.py b/__init__.py index a8ae98de..360e217f 100644 --- a/__init__.py +++ b/__init__.py @@ -112,7 +112,18 @@ def plot(*args, **kargs): #w = PlotWindow() #if len(args)+len(kargs) > 0: #w.plot(*args, **kargs) - w = PlotWindow(*args, **kargs) + + pwArgList = ['title', 'label', 'name', 'left', 'right', 'top', 'bottom'] + pwArgs = {} + dataArgs = {} + for k in kargs: + if k in pwArgList: + pwArgs[k] = kargs[k] + else: + dataArgs[k] = kargs[k] + + w = PlotWindow(**pwArgs) + w.plot(*args, **dataArgs) plots.append(w) w.show() return w diff --git a/examples/Arrow.py b/examples/Arrow.py index 446e243e..86f5c8c7 100755 --- a/examples/Arrow.py +++ b/examples/Arrow.py @@ -32,5 +32,6 @@ anim = a.makeAnimation(loop=-1) anim.start() ## Start Qt event loop unless running in interactive mode or using pyside. +import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() diff --git a/examples/Draw.py b/examples/Draw.py index 6a9b1323..e64b76b6 100644 --- a/examples/Draw.py +++ b/examples/Draw.py @@ -38,5 +38,6 @@ img.setDrawKernel(kern, mask=kern, center=(1,1), mode='add') img.setLevels([0, 10]) ## Start Qt event loop unless running in interactive mode or using pyside. +import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() diff --git a/examples/__main__.py b/examples/__main__.py index 95c0ebae..95e948ff 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -24,22 +24,22 @@ examples = OrderedDict([ ])), ('Widgets', OrderedDict([ ('PlotWidget', 'PlotWidget.py'), - ('SpinBox', '../widgets/SpinBox.py'), + #('SpinBox', '../widgets/SpinBox.py'), ('TreeWidget', '../widgets/TreeWidget.py'), ('DataTreeWidget', '../widgets/DataTreeWidget.py'), ('GradientWidget', '../widgets/GradientWidget.py'), - ('TableWidget', '../widgets/TableWidget.py'), + #('TableWidget', '../widgets/TableWidget.py'), ('ColorButton', '../widgets/ColorButton.py'), - ('CheckTable', '../widgets/CheckTable.py'), - ('VerticalLabel', '../widgets/VerticalLabel.py'), + #('CheckTable', '../widgets/CheckTable.py'), + #('VerticalLabel', '../widgets/VerticalLabel.py'), ('JoystickButton', '../widgets/JoystickButton.py'), ])), ('ImageView', 'ImageView.py'), ('GraphicsScene', 'GraphicsScene.py'), ('Flowcharts', 'Flowchart.py'), ('ParameterTree', '../parametertree'), - ('Canvas', '../canvas'), - ('MultiPlotWidget', 'MultiPlotWidget.py'), + #('Canvas', '../canvas'), + #('MultiPlotWidget', 'MultiPlotWidget.py'), ]) path = os.path.abspath(os.path.dirname(__file__)) diff --git a/exporters/Exporter.py b/exporters/Exporter.py index 1215e187..709926d4 100644 --- a/exporters/Exporter.py +++ b/exporters/Exporter.py @@ -115,7 +115,7 @@ class Exporter(object): return preItems + rootItem + postItems - def render(self, painter, sourcRect, targetRect, item=None): + def render(self, painter, targetRect, sourceRect, item=None): #if item is None: #item = self.item From 7e926ba13668fd2754051babf28df33a80849f92 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 04:04:04 -0400 Subject: [PATCH 037/238] Bugfix for plot linking --- examples/__main__.py | 3 +-- examples/linkedViews.py | 2 +- graphicsItems/ViewBox/ViewBox.py | 8 +++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 95e948ff..cdfabab2 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -100,8 +100,7 @@ def run(): app = QtGui.QApplication([]) loader = ExampleLoader() - if sys.flags.interactive != 1: - app.exec_() + app.exec_() if __name__ == '__main__': run() diff --git a/examples/linkedViews.py b/examples/linkedViews.py index 25e14d54..8abe7413 100644 --- a/examples/linkedViews.py +++ b/examples/linkedViews.py @@ -22,7 +22,7 @@ y = np.sin(x) / x win = pg.GraphicsWindow(title="View Linking Examples") win.resize(800,600) -win.addLabel("Views linked at runtime:", colspan=2) +win.addLabel("Linked Views", colspan=2) win.nextRow() p1 = win.addPlot(x=x, y=y, name="Plot1", title="Plot1") diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 26b5ae00..e83ff9e6 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -215,7 +215,7 @@ class ViewBox(GraphicsWidget): def resizeEvent(self, ev): #self.setRange(self.range, padding=0) - self.updateAutoRange() + #self.updateAutoRange() self.updateMatrix() self.sigStateChanged.emit(self) #self.linkedXChanged() @@ -609,10 +609,12 @@ class ViewBox(GraphicsWidget): return self.mapToScene(self.mapFromView(obj)) def mapFromItemToView(self, item, obj): - return self.mapSceneToView(item.mapToScene(obj)) + return self.childGroup.mapFromItem(item, obj) + #return self.mapSceneToView(item.mapToScene(obj)) def mapFromViewToItem(self, item, obj): - return item.mapFromScene(self.mapViewToScene(obj)) + return self.childGroup.mapToItem(item, obj) + #return item.mapFromScene(self.mapViewToScene(obj)) def itemBoundingRect(self, item): """Return the bounding rect of the item in view coordinates""" From c814499beebc869a7b13b04110357700fcf538bd Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 13:38:53 -0400 Subject: [PATCH 038/238] Added features from meganbkratz: - isocurves - array processing through gradientwidget --- flowchart/library/Filters.py | 2 +- functions.py | 91 +++++++++++++++++++++++++++++++++-- graphicsItems/IsocurveItem.py | 38 +++++++++++++++ widgets/GradientWidget.py | 1 + 4 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 graphicsItems/IsocurveItem.py diff --git a/flowchart/library/Filters.py b/flowchart/library/Filters.py index 1819b01e..a88ea40e 100644 --- a/flowchart/library/Filters.py +++ b/flowchart/library/Filters.py @@ -187,7 +187,7 @@ class HistogramDetrend(CtrlNode): """Removes baseline from data by computing mode (from histogram) of beginning and end of data.""" nodeName = 'HistogramDetrend' uiTemplate = [ - ('windowSize', 'intSpin', {'value': 500, 'min': 10, 'max': 1000000}), + ('windowSize', 'intSpin', {'value': 500, 'min': 10, 'max': 1000000, 'suffix': 'pts'}), ('numBins', 'intSpin', {'value': 50, 'min': 3, 'max': 1000000}) ] diff --git a/functions.py b/functions.py index ab60e63e..624e90b4 100644 --- a/functions.py +++ b/functions.py @@ -409,12 +409,12 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): -def makeARGB(data, lut=None, levels=None): +def makeARGB(data, lut=None, levels=None, useRGBA=False): """ Convert a 2D or 3D array into an ARGB array suitable for building QImages Will optionally do scaling and/or table lookups to determine final colors. - Returns the ARGB array and a boolean indicating whether there is alpha channel data. + Returns the ARGB array (values 0-255) and a boolean indicating whether there is alpha channel data. Arguments: data - 2D or 3D numpy array of int/float types @@ -433,6 +433,8 @@ def makeARGB(data, lut=None, levels=None): Lookup tables can be built using GradientWidget. levels - List [min, max]; optionally rescale data before converting through the lookup table. rescaled = (data-min) * len(lut) / (max-min) + useRGBA - If True, the data is returned in RGBA order. The default is + False, which returns in BGRA order for use with QImage. """ @@ -580,8 +582,11 @@ def makeARGB(data, lut=None, levels=None): prof.mark('4') - - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. + if useRGBA: + order = [0,1,2,3] ## array comes out RGBA + else: + order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. + if data.shape[2] == 1: for i in xrange(3): imgData[..., order[i]] = data[..., 0] @@ -732,7 +737,85 @@ def rescaleData(data, scale, offset): #return facets +def isocurve(data, level): + """ + Generate isocurve from 2D data using marching squares algorithm. + + *data* 2D numpy array of scalar values + *level* The level at which to generate an isosurface + + This function is SLOW; plenty of room for optimization here. + """ + sideTable = [ + [], + [0,1], + [1,2], + [0,2], + [0,3], + [1,3], + [0,1,2,3], + [2,3], + [2,3], + [0,1,2,3], + [1,3], + [0,3], + [0,2], + [1,2], + [0,1], + [] + ] + + edgeKey=[ + [(0,1),(0,0)], + [(0,0), (1,0)], + [(1,0), (1,1)], + [(1,1), (0,1)] + ] + + + lines = [] + + ## mark everything below the isosurface level + mask = data < level + + ### make four sub-fields and compute indexes for grid cells + index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) + fields = np.empty((2,2), dtype=object) + slices = [slice(0,-1), slice(1,None)] + for i in [0,1]: + for j in [0,1]: + fields[i,j] = mask[slices[i], slices[j]] + #vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme + vertIndex = i+2*j + #print i,j,k," : ", fields[i,j,k], 2**vertIndex + index += fields[i,j] * 2**vertIndex + #print index + #print index + + ## add lines + for i in xrange(index.shape[0]): # data x-axis + for j in xrange(index.shape[1]): # data y-axis + sides = sideTable[index[i,j]] + for l in range(0, len(sides), 2): ## faces for this grid cell + edges = sides[l:l+2] + pts = [] + for m in [0,1]: # points in this face + p1 = edgeKey[edges[m]][0] # p1, p2 are points at either side of an edge + p2 = edgeKey[edges[m]][1] + v1 = data[i+p1[0], j+p1[1]] # v1 and v2 are the values at p1 and p2 + v2 = data[i+p2[0], j+p2[1]] + f = (level-v1) / (v2-v1) + fi = 1.0 - f + p = ( ## interpolate between corners + p1[0]*fi + p2[0]*f + i + 0.5, + p1[1]*fi + p2[1]*f + j + 0.5 + ) + pts.append(p) + lines.append(pts) + + return lines ## a list of pairs of points + def isosurface(data, level): """ diff --git a/graphicsItems/IsocurveItem.py b/graphicsItems/IsocurveItem.py new file mode 100644 index 00000000..62e582fc --- /dev/null +++ b/graphicsItems/IsocurveItem.py @@ -0,0 +1,38 @@ + + +from GraphicsObject import * +import pyqtgraph.functions as fn +from pyqtgraph.Qt import QtGui + + +class IsocurveItem(GraphicsObject): + """ + Item displaying an isocurve of a 2D array. + + To align this item correctly with an ImageItem, + call isocurve.setParentItem(image) + """ + + def __init__(self, data, level, pen='w'): + GraphicsObject.__init__(self) + + lines = fn.isocurve(data, level) + + self.path = QtGui.QPainterPath() + self.setPen(pen) + + for line in lines: + self.path.moveTo(*line[0]) + self.path.lineTo(*line[1]) + + def setPen(self, *args, **kwargs): + self.pen = fn.mkPen(*args, **kwargs) + self.update() + + def boundingRect(self): + return self.path.boundingRect() + + def paint(self, p, *args): + p.setPen(self.pen) + p.drawPath(self.path) + \ No newline at end of file diff --git a/widgets/GradientWidget.py b/widgets/GradientWidget.py index 47d6ab45..bcc50b69 100644 --- a/widgets/GradientWidget.py +++ b/widgets/GradientWidget.py @@ -55,6 +55,7 @@ class GradientWidget(GraphicsView): self.setMaximumHeight(16777215) def __getattr__(self, attr): + ### wrap methods from GradientEditorItem return getattr(self.item, attr) From f6da6e2fd0ff22adcac8dbbb101d209dd4bb1d4b Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 23 Mar 2012 22:13:41 -0400 Subject: [PATCH 039/238] Added matplotlib exporter Updates to MeshData class (this is still not tested) --- GraphicsScene/exportDialog.py | 7 +- __init__.py | 6 +- exporters/Matplotlib.py | 74 ++++++++++++ functions.py | 179 ++++++++--------------------- graphicsItems/PlotDataItem.py | 16 ++- graphicsItems/PlotItem/PlotItem.py | 12 +- opengl/MeshData.py | 111 +++++++++++++++++- widgets/MatplotlibWidget.py | 37 ++++++ 8 files changed, 292 insertions(+), 150 deletions(-) create mode 100644 exporters/Matplotlib.py create mode 100644 widgets/MatplotlibWidget.py diff --git a/GraphicsScene/exportDialog.py b/GraphicsScene/exportDialog.py index f9ed5763..72809d44 100644 --- a/GraphicsScene/exportDialog.py +++ b/GraphicsScene/exportDialog.py @@ -72,6 +72,8 @@ class ExportDialog(QtGui.QWidget): def exportItemChanged(self, item, prev): + if item is None: + return if item.gitem is self.scene: newBounds = self.scene.views()[0].viewRect() else: @@ -105,7 +107,10 @@ class ExportDialog(QtGui.QWidget): expClass = self.exporterClasses[str(item.text())] exp = expClass(item=self.ui.itemTree.currentItem().gitem) params = exp.parameters() - self.ui.paramTree.setParameters(params) + if params is None: + self.ui.paramTree.clear() + else: + self.ui.paramTree.setParameters(params) self.currentExporter = exp def exportClicked(self): diff --git a/__init__.py b/__init__.py index 360e217f..4256c0e3 100644 --- a/__init__.py +++ b/__init__.py @@ -57,7 +57,7 @@ renamePyc(path) ## don't import the more complex systems--canvas, parametertree, flowchart, dockarea ## these must be imported separately. -def importAll(path): +def importAll(path, excludes=()): d = os.path.join(os.path.split(__file__)[0], path) files = [] for f in os.listdir(d): @@ -67,6 +67,8 @@ def importAll(path): files.append(f[:-3]) for modName in files: + if modName in excludes: + continue mod = __import__(path+"."+modName, globals(), locals(), fromlist=['*']) if hasattr(mod, '__all__'): names = mod.__all__ @@ -77,7 +79,7 @@ def importAll(path): globals()[k] = getattr(mod, k) importAll('graphicsItems') -importAll('widgets') +importAll('widgets', excludes=['MatplotlibWidget']) from imageview import * from WidgetGroup import * diff --git a/exporters/Matplotlib.py b/exporters/Matplotlib.py new file mode 100644 index 00000000..71164b8e --- /dev/null +++ b/exporters/Matplotlib.py @@ -0,0 +1,74 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from Exporter import Exporter + + +__all__ = ['MatplotlibExporter'] + + +class MatplotlibExporter(Exporter): + Name = "Matplotlib Window" + windows = [] + def __init__(self, item): + Exporter.__init__(self, item) + + def parameters(self): + return None + + def export(self, fileName=None): + + if isinstance(self.item, pg.PlotItem): + mpw = MatplotlibWindow() + MatplotlibExporter.windows.append(mpw) + fig = mpw.getFigure() + + ax = fig.add_subplot(111) + ax.clear() + #ax.grid(True) + + for item in self.item.curves: + x, y = item.getData() + opts = item.opts + pen = pg.mkPen(opts['pen']) + if pen.style() == QtCore.Qt.NoPen: + linestyle = '' + else: + linestyle = '-' + color = tuple([c/255. for c in pg.colorTuple(pen.color())]) + symbol = opts['symbol'] + if symbol == 't': + symbol = '^' + symbolPen = pg.mkPen(opts['symbolPen']) + symbolBrush = pg.mkBrush(opts['symbolBrush']) + markeredgecolor = tuple([c/255. for c in pg.colorTuple(symbolPen.color())]) + markerfacecolor = tuple([c/255. for c in pg.colorTuple(symbolBrush.color())]) + + if opts['fillLevel'] is not None and opts['fillBrush'] is not None: + fillBrush = pg.mkBrush(opts['fillBrush']) + fillcolor = tuple([c/255. for c in pg.colorTuple(fillBrush.color())]) + ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) + + ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor) + + xr, yr = self.item.viewRange() + ax.set_xbound(*xr) + ax.set_ybound(*yr) + mpw.draw() + else: + raise Exception("Matplotlib export currently only works with plot items") + + + +class MatplotlibWindow(QtGui.QMainWindow): + def __init__(self): + import pyqtgraph.widgets.MatplotlibWidget + QtGui.QMainWindow.__init__(self) + self.mpl = pyqtgraph.widgets.MatplotlibWidget.MatplotlibWidget() + self.setCentralWidget(self.mpl) + self.show() + + def __getattr__(self, attr): + return getattr(self.mpl, attr) + + def closeEvent(self, ev): + MatplotlibExporter.windows.remove(self) diff --git a/functions.py b/functions.py index 624e90b4..7a582c4a 100644 --- a/functions.py +++ b/functions.py @@ -409,12 +409,12 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): -def makeARGB(data, lut=None, levels=None, useRGBA=False): +def makeARGB(data, lut=None, levels=None): """ Convert a 2D or 3D array into an ARGB array suitable for building QImages Will optionally do scaling and/or table lookups to determine final colors. - Returns the ARGB array (values 0-255) and a boolean indicating whether there is alpha channel data. + Returns the ARGB array and a boolean indicating whether there is alpha channel data. Arguments: data - 2D or 3D numpy array of int/float types @@ -433,8 +433,6 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False): Lookup tables can be built using GradientWidget. levels - List [min, max]; optionally rescale data before converting through the lookup table. rescaled = (data-min) * len(lut) / (max-min) - useRGBA - If True, the data is returned in RGBA order. The default is - False, which returns in BGRA order for use with QImage. """ @@ -582,11 +580,8 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False): prof.mark('4') - if useRGBA: - order = [0,1,2,3] ## array comes out RGBA - else: - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - + + order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. if data.shape[2] == 1: for i in xrange(3): imgData[..., order[i]] = data[..., 0] @@ -737,85 +732,7 @@ def rescaleData(data, scale, offset): #return facets -def isocurve(data, level): - """ - Generate isocurve from 2D data using marching squares algorithm. - - *data* 2D numpy array of scalar values - *level* The level at which to generate an isosurface - - This function is SLOW; plenty of room for optimization here. - """ - sideTable = [ - [], - [0,1], - [1,2], - [0,2], - [0,3], - [1,3], - [0,1,2,3], - [2,3], - [2,3], - [0,1,2,3], - [1,3], - [0,3], - [0,2], - [1,2], - [0,1], - [] - ] - - edgeKey=[ - [(0,1),(0,0)], - [(0,0), (1,0)], - [(1,0), (1,1)], - [(1,1), (0,1)] - ] - - - lines = [] - - ## mark everything below the isosurface level - mask = data < level - - ### make four sub-fields and compute indexes for grid cells - index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) - fields = np.empty((2,2), dtype=object) - slices = [slice(0,-1), slice(1,None)] - for i in [0,1]: - for j in [0,1]: - fields[i,j] = mask[slices[i], slices[j]] - #vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme - vertIndex = i+2*j - #print i,j,k," : ", fields[i,j,k], 2**vertIndex - index += fields[i,j] * 2**vertIndex - #print index - #print index - - ## add lines - for i in xrange(index.shape[0]): # data x-axis - for j in xrange(index.shape[1]): # data y-axis - sides = sideTable[index[i,j]] - for l in range(0, len(sides), 2): ## faces for this grid cell - edges = sides[l:l+2] - pts = [] - for m in [0,1]: # points in this face - p1 = edgeKey[edges[m]][0] # p1, p2 are points at either side of an edge - p2 = edgeKey[edges[m]][1] - v1 = data[i+p1[0], j+p1[1]] # v1 and v2 are the values at p1 and p2 - v2 = data[i+p2[0], j+p2[1]] - f = (level-v1) / (v2-v1) - fi = 1.0 - f - p = ( ## interpolate between corners - p1[0]*fi + p2[0]*f + i + 0.5, - p1[1]*fi + p2[1]*f + j + 0.5 - ) - pts.append(p) - lines.append(pts) - - return lines ## a list of pairs of points - def isosurface(data, level): """ @@ -1193,55 +1110,55 @@ def isosurface(data, level): return facets +## code has moved to opengl/MeshData.py +#def meshNormals(data): + #""" + #Return list of normal vectors and list of faces which reference the normals + #data must be list of triangles; each triangle is a list of three points + #[ [(x,y,z), (x,y,z), (x,y,z)], ...] + #Return values are + #normals: [(x,y,z), ...] + #faces: [(n1, n2, n3), ...] + #""" -def meshNormals(data): - """ - Return list of normal vectors and list of faces which reference the normals - data must be list of triangles; each triangle is a list of three points - [ [(x,y,z), (x,y,z), (x,y,z)], ...] - Return values are - normals: [(x,y,z), ...] - faces: [(n1, n2, n3), ...] - """ - - normals = [] - points = {} - for i, face in enumerate(data): - ## compute face normal - pts = [QtGui.QVector3D(*x) for x in face] - norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) - normals.append(norm) + #normals = [] + #points = {} + #for i, face in enumerate(data): + ### compute face normal + #pts = [QtGui.QVector3D(*x) for x in face] + #norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + #normals.append(norm) - ## remember each point was associated with this normal - for p in face: - p = tuple(map(lambda x: np.round(x, 8), p)) - if p not in points: - points[p] = [] - points[p].append(i) + ### remember each point was associated with this normal + #for p in face: + #p = tuple(map(lambda x: np.round(x, 8), p)) + #if p not in points: + #points[p] = [] + #points[p].append(i) - ## compute averages - avgLookup = {} - avgNorms = [] - for k,v in points.iteritems(): - norms = [normals[i] for i in v] - a = norms[0] - if len(v) > 1: - for n in norms[1:]: - a = a + n - a = a / len(v) - avgLookup[k] = len(avgNorms) - avgNorms.append(a) + ### compute averages + #avgLookup = {} + #avgNorms = [] + #for k,v in points.iteritems(): + #norms = [normals[i] for i in v] + #a = norms[0] + #if len(v) > 1: + #for n in norms[1:]: + #a = a + n + #a = a / len(v) + #avgLookup[k] = len(avgNorms) + #avgNorms.append(a) - ## generate return array - faces = [] - for i, face in enumerate(data): - f = [] - for p in face: - p = tuple(map(lambda x: np.round(x, 8), p)) - f.append(avgLookup[p]) - faces.append(tuple(f)) + ### generate return array + #faces = [] + #for i, face in enumerate(data): + #f = [] + #for p in face: + #p = tuple(map(lambda x: np.round(x, 8), p)) + #f.append(avgLookup[p]) + #faces.append(tuple(f)) - return avgNorms, faces + #return avgNorms, faces diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index a8a46c4f..1938cd50 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -98,7 +98,7 @@ class PlotDataItem(GraphicsObject): 'pen': (200,200,200), 'shadowPen': None, 'fillLevel': None, - 'brush': None, + 'fillBrush': None, 'symbol': None, 'symbolSize': 10, @@ -165,10 +165,13 @@ class PlotDataItem(GraphicsObject): #self.update() self.updateItems() - def setBrush(self, *args, **kargs): + def setFillBrush(self, *args, **kargs): brush = fn.mkBrush(*args, **kargs) - self.opts['brush'] = brush + self.opts['fillBrush'] = brush self.updateItems() + + def setBrush(self, *args, **kargs): + return self.setFillBrush(*args, **kargs) def setFillLevel(self, level): self.opts['fillLevel'] = level @@ -268,6 +271,9 @@ class PlotDataItem(GraphicsObject): if 'symbol' not in kargs and ('symbolPen' in kargs or 'symbolBrush' in kargs or 'symbolSize' in kargs): kargs['symbol'] = 'o' + if 'brush' in kargs: + kargs['fillBrush'] = kargs['brush'] + for k in self.opts.keys(): if k in kargs: self.opts[k] = kargs[k] @@ -313,8 +319,8 @@ class PlotDataItem(GraphicsObject): #c.scene().removeItem(c) curveArgs = {} - for k in ['pen', 'shadowPen', 'fillLevel', 'brush']: - curveArgs[k] = self.opts[k] + for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush')]: + curveArgs[v] = self.opts[k] scatterArgs = {} for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size')]: diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index cef568c5..333516dc 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -146,11 +146,11 @@ class PlotItem(GraphicsWidget): ## Wrap a few methods from viewBox - for m in [ - 'setXRange', 'setYRange', 'setXLink', 'setYLink', - 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', - 'enableAutoRange', 'disableAutoRange', 'setAspectLocked']: - setattr(self, m, getattr(self.vb, m)) + #for m in [ + #'setXRange', 'setYRange', 'setXLink', 'setYLink', + #'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', + #'enableAutoRange', 'disableAutoRange', 'setAspectLocked']: + #setattr(self, m, getattr(self.vb, m)) self.items = [] self.curves = [] @@ -296,6 +296,8 @@ class PlotItem(GraphicsWidget): #QtGui.QGraphicsWidget.paint(self, *args) #prof.finish() + def __getattr__(self, attr): ## wrap ms + return getattr(self.vb, attr) def close(self): #print "delete", self diff --git a/opengl/MeshData.py b/opengl/MeshData.py index f6d0ae7c..15139bc1 100644 --- a/opengl/MeshData.py +++ b/opengl/MeshData.py @@ -8,18 +8,117 @@ class MeshData(object): - normals per vertex or tri """ - def __init__(self ...): - - - def generateFaceNormals(self): + def __init__(self): + self.vertexes = [] + self.edges = None + self.faces = [] + self.vertexFaces = None ## maps vertex ID to a list of face IDs + self.vertexNormals = None + self.faceNormals = None + self.vertexColors = None + self.edgeColors = None + self.faceColors = None + def setFaces(self, faces, vertexes=None): + """ + Set the faces in this data set. + Data may be provided either as an Nx3x3 list of floats (9 float coordinate values per face) + *faces* = [ [(x, y, z), (x, y, z), (x, y, z)], ... ] + or as an Nx3 list of ints (vertex integers) AND an Mx3 list of floats (3 float coordinate values per vertex) + *faces* = [ (p1, p2, p3), ... ] + *vertexes* = [ (x, y, z), ... ] + """ + + if vertexes is None: + self._setUnindexedFaces(self, faces) + else: + self._setIndexedFaces(self, faces) + + def _setUnindexedFaces(self, faces): + verts = {} + self.faces = [] + self.vertexes = [] + self.vertexFaces = [] + self.faceNormals = None + self.vertexNormals = None + for face in faces: + inds = [] + for pt in face: + pt2 = tuple([int(x*1e14) for x in pt]) ## quantize to be sure that nearly-identical points will be merged + index = verts.get(pt2, None) + if index is None: + self.vertexes.append(tuple(pt)) + self.vertexFaces.append([]) + index = len(self.vertexes)-1 + verts[pt2] = index + self.vertexFaces[index].append(face) + inds.append(index) + self.faces.append(tuple(inds)) - def generateVertexNormals(self): + def _setIndexedFaces(self, faces, vertexes): + self.vertexes = vertexes + self.faces = faces + self.edges = None + self.vertexFaces = None + self.faceNormals = None + self.vertexNormals = None + + def getVertexFaces(self): + """ + Return list mapping each vertex index to a list of face indexes that use the vertex. + """ + if self.vertexFaces is None: + self.vertexFaces = [[]] * len(self.vertexes) + for i, face in enumerate(self.faces): + for ind in face: + if len(self.vertexFaces[ind]) == 0: + self.vertexFaces[ind] = [] ## need a unique/empty list to fill + self.vertexFaces[ind].append(i) + return self.vertexFaces + + + def getFaceNormals(self): + """ + Computes and stores normal of each face. + """ + if self.faceNormals is None: + self.faceNormals = [] + for i, face in enumerate(self.faces): + ## compute face normal + pts = [QtGui.QVector3D(*self.vertexes[vind]) for vind in face] + norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + self.faceNormals.append(norm) + return self.faceNormals + + def getVertexNormals(self): """ Assigns each vertex the average of its connected face normals. If face normals have not been computed yet, then generateFaceNormals will be called. """ + if self.vertexNormals is None: + faceNorms = self.getFaceNormals() + vertFaces = self.getVertexFaces() + self.vertexNormals = [] + for vindex in xrange(len(self.vertexes)): + norms = [faceNorms[findex] for findex in vertFaces[vindex]] + if len(norms) == 0: + norm = QtGui.QVector3D() + else: + norm = reduce(QtGui.QVector3D.__add__, facenorms) / float(len(norms)) + self.vertexNormals.append(norm) + return self.vertexNormals def reverseNormals(self): - \ No newline at end of file + """ + Reverses the direction of all normal vectors. + """ + pass + + def generateEdgesFromFaces(self): + """ + Generate a set of edges by listing all the edges of faces and removing any duplicates. + Useful for displaying wireframe meshes. + """ + pass + diff --git a/widgets/MatplotlibWidget.py b/widgets/MatplotlibWidget.py new file mode 100644 index 00000000..25e058f9 --- /dev/null +++ b/widgets/MatplotlibWidget.py @@ -0,0 +1,37 @@ +from pyqtgraph.Qt import QtGui, QtCore +import matplotlib +from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar +from matplotlib.figure import Figure + +class MatplotlibWidget(QtGui.QWidget): + """ + Implements a Matplotlib figure inside a QWidget. + Use getFigure() and redraw() to interact with matplotlib. + + Example:: + + mw = MatplotlibWidget() + subplot = mw.getFigure().add_subplot(111) + subplot.plot(x,y) + mw.draw() + """ + + def __init__(self, size=(5.0, 4.0), dpi=100): + QtGui.QWidget.__init__(self) + self.fig = Figure(size, dpi=dpi) + self.canvas = FigureCanvas(self.fig) + self.canvas.setParent(self) + self.toolbar = NavigationToolbar(self.canvas, self) + + self.vbox = QtGui.QVBoxLayout() + self.vbox.addWidget(self.toolbar) + self.vbox.addWidget(self.canvas) + + self.setLayout(self.vbox) + + def getFigure(self): + return self.fig + + def draw(self): + self.canvas.draw() From d2d812c86ecaf67ba336d4333097328d53c48543 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 24 Mar 2012 12:17:48 -0400 Subject: [PATCH 040/238] Fixed up MeshData and GLMeshItem classes for surface display --- opengl/MeshData.py | 146 ++++++++++++++++++++++--------------- opengl/items/GLMeshItem.py | 90 +++++++++-------------- 2 files changed, 124 insertions(+), 112 deletions(-) diff --git a/opengl/MeshData.py b/opengl/MeshData.py index 15139bc1..b9f8f1ec 100644 --- a/opengl/MeshData.py +++ b/opengl/MeshData.py @@ -1,3 +1,5 @@ +from pyqtgraph.Qt import QtGui + class MeshData(object): """ Class for storing 3D mesh data. May contain: @@ -9,15 +11,16 @@ class MeshData(object): """ def __init__(self): - self.vertexes = [] - self.edges = None - self.faces = [] - self.vertexFaces = None ## maps vertex ID to a list of face IDs - self.vertexNormals = None - self.faceNormals = None - self.vertexColors = None - self.edgeColors = None - self.faceColors = None + self._vertexes = [] + self._edges = None + self._faces = [] + self._vertexFaces = None ## maps vertex ID to a list of face IDs + self._vertexNormals = None + self._faceNormals = None + self._vertexColors = None + self._edgeColors = None + self._faceColors = None + self._meshColor = (1, 1, 1, 0.1) # default color to use if no face/edge/vertex colors are given def setFaces(self, faces, vertexes=None): """ @@ -30,84 +33,111 @@ class MeshData(object): """ if vertexes is None: - self._setUnindexedFaces(self, faces) + self._setUnindexedFaces(faces) else: - self._setIndexedFaces(self, faces) - + self._setIndexedFaces(faces, vertexes) + + def _setUnindexedFaces(self, faces): verts = {} - self.faces = [] - self.vertexes = [] - self.vertexFaces = [] - self.faceNormals = None - self.vertexNormals = None + self._faces = [] + self._vertexes = [] + self._vertexFaces = [] + self._faceNormals = None + self._vertexNormals = None for face in faces: inds = [] for pt in face: - pt2 = tuple([int(x*1e14) for x in pt]) ## quantize to be sure that nearly-identical points will be merged + pt2 = tuple([round(x*1e14) for x in pt]) ## quantize to be sure that nearly-identical points will be merged index = verts.get(pt2, None) if index is None: - self.vertexes.append(tuple(pt)) - self.vertexFaces.append([]) - index = len(self.vertexes)-1 + self._vertexes.append(QtGui.QVector3D(*pt)) + self._vertexFaces.append([]) + index = len(self._vertexes)-1 verts[pt2] = index - self.vertexFaces[index].append(face) + self._vertexFaces[index].append(len(self._faces)) inds.append(index) - self.faces.append(tuple(inds)) + self._faces.append(tuple(inds)) def _setIndexedFaces(self, faces, vertexes): - self.vertexes = vertexes - self.faces = faces - self.edges = None - self.vertexFaces = None - self.faceNormals = None - self.vertexNormals = None + self._vertexes = [QtGui.QVector3D(*v) for v in vertexes] + self._faces = faces + self._edges = None + self._vertexFaces = None + self._faceNormals = None + self._vertexNormals = None - def getVertexFaces(self): + def vertexFaces(self): """ Return list mapping each vertex index to a list of face indexes that use the vertex. """ - if self.vertexFaces is None: - self.vertexFaces = [[]] * len(self.vertexes) - for i, face in enumerate(self.faces): + if self._vertexFaces is None: + self._vertexFaces = [[]] * len(self._vertexes) + for i, face in enumerate(self._faces): for ind in face: - if len(self.vertexFaces[ind]) == 0: - self.vertexFaces[ind] = [] ## need a unique/empty list to fill - self.vertexFaces[ind].append(i) - return self.vertexFaces + if len(self._vertexFaces[ind]) == 0: + self._vertexFaces[ind] = [] ## need a unique/empty list to fill + self._vertexFaces[ind].append(i) + return self._vertexFaces - - def getFaceNormals(self): + def __iter__(self): + """Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face.""" + vnorms = self.vertexNormals() + vcolors = self.vertexColors() + for i in range(len(self._faces)): + face = [] + for j in [0,1,2]: + vind = self._faces[i][j] + pos = self._vertexes[vind] + norm = vnorms[vind] + if vcolors is None: + color = self._meshColor + else: + color = vcolors[vind] + face.append((pos, norm, color)) + yield face + + + def faceNormals(self): """ Computes and stores normal of each face. """ - if self.faceNormals is None: - self.faceNormals = [] - for i, face in enumerate(self.faces): + if self._faceNormals is None: + self._faceNormals = [] + for i, face in enumerate(self._faces): ## compute face normal - pts = [QtGui.QVector3D(*self.vertexes[vind]) for vind in face] - norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) - self.faceNormals.append(norm) - return self.faceNormals + pts = [self._vertexes[vind] for vind in face] + norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]).normalized() + self._faceNormals.append(norm) + return self._faceNormals - def getVertexNormals(self): + def vertexNormals(self): """ Assigns each vertex the average of its connected face normals. If face normals have not been computed yet, then generateFaceNormals will be called. """ - if self.vertexNormals is None: - faceNorms = self.getFaceNormals() - vertFaces = self.getVertexFaces() - self.vertexNormals = [] - for vindex in xrange(len(self.vertexes)): + if self._vertexNormals is None: + faceNorms = self.faceNormals() + vertFaces = self.vertexFaces() + self._vertexNormals = [] + for vindex in xrange(len(self._vertexes)): + #print vertFaces[vindex] norms = [faceNorms[findex] for findex in vertFaces[vindex]] - if len(norms) == 0: - norm = QtGui.QVector3D() - else: - norm = reduce(QtGui.QVector3D.__add__, facenorms) / float(len(norms)) - self.vertexNormals.append(norm) - return self.vertexNormals + norm = QtGui.QVector3D() + for fn in norms: + norm += fn + norm.normalize() + self._vertexNormals.append(norm) + return self._vertexNormals + def vertexColors(self): + return self._vertexColors + + def faceColors(self): + return self._faceColors + + def edgeColors(self): + return self._edgeColors def reverseNormals(self): """ diff --git a/opengl/items/GLMeshItem.py b/opengl/items/GLMeshItem.py index 1efc3ffe..b82c4d3d 100644 --- a/opengl/items/GLMeshItem.py +++ b/opengl/items/GLMeshItem.py @@ -1,5 +1,6 @@ from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem +from .. MeshData import MeshData from pyqtgraph.Qt import QtGui import pyqtgraph as pg from .. import shaders @@ -10,33 +11,20 @@ import numpy as np __all__ = ['GLMeshItem'] class GLMeshItem(GLGraphicsItem): - def __init__(self, faces): - self.faces = faces - self.normals, self.faceNormals = pg.meshNormals(faces) + """ + Displays a 3D triangle mesh. + + """ + def __init__(self, faces, vertexes=None): + """ + See MeshData for initialization arguments. + """ + self.data = MeshData() + self.data.setFaces(faces, vertexes) GLGraphicsItem.__init__(self) def initializeGL(self): - - #balloonVertexShader = shaders.compileShader(""" - #varying vec3 normal; - #void main() { - #normal = normalize(gl_NormalMatrix * gl_Normal); - #//vec4 color = normal; - #//normal.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 2.0), 1.0); - #gl_FrontColor = gl_Color; - #gl_BackColor = gl_Color; - #gl_Position = ftransform(); - #}""", GL_VERTEX_SHADER) - #balloonFragmentShader = shaders.compileShader(""" - #varying vec3 normal; - #void main() { - #vec4 color = gl_Color; - #color.w = min(color.w + 2.0 * color.w * pow(normal.x*normal.x + normal.y*normal.y, 5.0), 1.0); - #gl_FragColor = color; - #}""", GL_FRAGMENT_SHADER) - #self.shader = shaders.compileProgram(balloonVertexShader, balloonFragmentShader) - self.shader = shaders.getShader('balloon') l = glGenLists(1) @@ -51,39 +39,33 @@ class GLMeshItem(GLGraphicsItem): glDisable( GL_DEPTH_TEST ) glColor4f(1, 1, 1, .1) glBegin( GL_TRIANGLES ) - for i, f in enumerate(self.faces): - pts = [QtGui.QVector3D(*x) for x in f] - if pts[0] is None: - print f - continue - #norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) - for j in [0,1,2]: - norm = self.normals[self.faceNormals[i][j]] + for face in self.data: + for (pos, norm, color) in face: + glColor4f(*color) glNormal3f(norm.x(), norm.y(), norm.z()) + glVertex3f(pos.x(), pos.y(), pos.z()) + glEnd() + glEndList() + + + #l = glGenLists(1) + #self.meshList = l + #glNewList(l, GL_COMPILE) + #glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) + #glEnable( GL_BLEND ) + #glEnable( GL_ALPHA_TEST ) + ##glAlphaFunc( GL_ALWAYS,0.5 ) + #glEnable( GL_POINT_SMOOTH ) + #glEnable( GL_DEPTH_TEST ) + #glColor4f(1, 1, 1, .3) + #glBegin( GL_LINES ) + #for f in self.faces: + #for i in [0,1,2]: #j = (i+1) % 3 - glVertex3f(*f[j]) - glEnd() - glEndList() - - - l = glGenLists(1) - self.meshList = l - glNewList(l, GL_COMPILE) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glEnable( GL_BLEND ) - glEnable( GL_ALPHA_TEST ) - #glAlphaFunc( GL_ALWAYS,0.5 ) - glEnable( GL_POINT_SMOOTH ) - glEnable( GL_DEPTH_TEST ) - glColor4f(1, 1, 1, .3) - glBegin( GL_LINES ) - for f in self.faces: - for i in [0,1,2]: - j = (i+1) % 3 - glVertex3f(*f[i]) - glVertex3f(*f[j]) - glEnd() - glEndList() + #glVertex3f(*f[i]) + #glVertex3f(*f[j]) + #glEnd() + #glEndList() def paint(self): From 96d126722340c0084bd3ba9896fc1765dd26bf14 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 24 Mar 2012 12:32:53 -0400 Subject: [PATCH 041/238] Added 3D examples to menu --- examples/__main__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index cdfabab2..11c9785d 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -10,17 +10,23 @@ from collections import OrderedDict examples = OrderedDict([ ('Command-line usage', 'CLIexample.py'), ('Basic Plotting', 'Plotting.py'), + ('ImageView', 'ImageView.py'), + ('ParameterTree', '../parametertree'), ('GraphicsItems', OrderedDict([ + ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), ('ImageItem - video', 'ImageItem.py'), ('ImageItem - draw', 'Draw.py'), ('Region-of-Interest', 'ROItypes.py'), ('GraphicsLayout', 'GraphicsLayout.py'), - ('Scatter Plot', 'ScatterPlot.py'), ('Text Item', 'text.py'), - ('ViewBox', 'ViewBox.py'), ('Linked Views', 'linkedViews.py'), ('Arrow', 'Arrow.py'), + ('ViewBox', 'ViewBox.py'), + ])), + ('3D Graphics', OrderedDict([ + ('Volumetric', 'GLVolumeItem.py'), + ('Isosurface', 'GLMeshItem.py'), ])), ('Widgets', OrderedDict([ ('PlotWidget', 'PlotWidget.py'), @@ -34,10 +40,9 @@ examples = OrderedDict([ #('VerticalLabel', '../widgets/VerticalLabel.py'), ('JoystickButton', '../widgets/JoystickButton.py'), ])), - ('ImageView', 'ImageView.py'), + ('GraphicsScene', 'GraphicsScene.py'), ('Flowcharts', 'Flowchart.py'), - ('ParameterTree', '../parametertree'), #('Canvas', '../canvas'), #('MultiPlotWidget', 'MultiPlotWidget.py'), ]) @@ -93,6 +98,8 @@ class ExampleLoader(QtGui.QMainWindow): if fn is None: self.ui.codeView.clear() return + if os.path.isdir(fn): + fn = os.path.join(fn, '__main__.py') text = open(fn).read() self.ui.codeView.setPlainText(text) From 543d56f0a6c0b64ba27ab0d311359715b5ca3b8d Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 25 Mar 2012 02:25:02 -0400 Subject: [PATCH 042/238] reverted plotitem bug --- graphicsItems/PlotItem/PlotItem.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 333516dc..5f95a6dd 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -146,11 +146,12 @@ class PlotItem(GraphicsWidget): ## Wrap a few methods from viewBox - #for m in [ - #'setXRange', 'setYRange', 'setXLink', 'setYLink', - #'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', - #'enableAutoRange', 'disableAutoRange', 'setAspectLocked']: - #setattr(self, m, getattr(self.vb, m)) + for m in [ + 'setXRange', 'setYRange', 'setXLink', 'setYLink', + 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', + 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', + 'register', 'unregister']: + setattr(self, m, getattr(self.vb, m)) self.items = [] self.curves = [] @@ -296,8 +297,9 @@ class PlotItem(GraphicsWidget): #QtGui.QGraphicsWidget.paint(self, *args) #prof.finish() - def __getattr__(self, attr): ## wrap ms - return getattr(self.vb, attr) + ## bad idea. + #def __getattr__(self, attr): ## wrap ms + #return getattr(self.vb, attr) def close(self): #print "delete", self From ad232ff79bfdbb943e53134ae7fb8192c4380b1a Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 27 Mar 2012 12:30:51 -0400 Subject: [PATCH 043/238] - re-merged isocurve code - re-enabled OpenGL on windows, added a config option for enabling/disabling OpenGL - minor bug fixes --- __init__.py | 12 +++++ functions.py | 93 ++++++++++++++++++++++++++++++-- graphicsItems/VTickGroup.py | 3 ++ graphicsItems/ViewBox/ViewBox.py | 6 +-- parametertree/parameterTypes.py | 2 +- widgets/GraphicsView.py | 13 ++--- 6 files changed, 111 insertions(+), 18 deletions(-) diff --git a/__init__.py b/__init__.py index 4256c0e3..398aa020 100644 --- a/__init__.py +++ b/__init__.py @@ -9,7 +9,19 @@ from Qt import QtGui #if QtGui.QApplication.instance() is None: #app = QtGui.QApplication([]) +## in general openGL is poorly supported in Qt. +## we only enable it where the performance benefit is critical. +## Note this only applies to 2D graphics; 3D graphics always use OpenGL. +import sys +if 'linux' in sys.platform: ## linux has numerous bugs in opengl implementation + useOpenGL = False +elif 'darwin' in sys.platform: ## openGL greatly speeds up display on mac + useOpenGL = True +else: + useOpenGL = True ## on windows there's a more even performance / bugginess tradeoff. + CONFIG_OPTIONS = { + 'useOpenGL': None, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. 'leftButtonPan': True ## if false, left button drags a rubber band for zooming in viewbox } diff --git a/functions.py b/functions.py index 7a582c4a..de94295b 100644 --- a/functions.py +++ b/functions.py @@ -409,12 +409,12 @@ def affineSlice(data, shape, origin, vectors, axes, **kargs): -def makeARGB(data, lut=None, levels=None): +def makeARGB(data, lut=None, levels=None, useRGBA=False): """ Convert a 2D or 3D array into an ARGB array suitable for building QImages Will optionally do scaling and/or table lookups to determine final colors. - Returns the ARGB array and a boolean indicating whether there is alpha channel data. + Returns the ARGB array (values 0-255) and a boolean indicating whether there is alpha channel data. Arguments: data - 2D or 3D numpy array of int/float types @@ -433,9 +433,10 @@ def makeARGB(data, lut=None, levels=None): Lookup tables can be built using GradientWidget. levels - List [min, max]; optionally rescale data before converting through the lookup table. rescaled = (data-min) * len(lut) / (max-min) + useRGBA - If True, the data is returned in RGBA order. The default is + False, which returns in BGRA order for use with QImage. - """ - + """ prof = debug.Profiler('functions.makeARGB', disabled=True) ## sanity checks @@ -580,7 +581,11 @@ def makeARGB(data, lut=None, levels=None): prof.mark('4') - + if useRGBA: + order = [0,1,2,3] ## array comes out RGBA + else: + order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. + order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. if data.shape[2] == 1: for i in xrange(3): @@ -732,6 +737,84 @@ def rescaleData(data, scale, offset): #return facets +def isocurve(data, level): + """ + Generate isocurve from 2D data using marching squares algorithm. + + *data* 2D numpy array of scalar values + *level* The level at which to generate an isosurface + + This function is SLOW; plenty of room for optimization here. + """ + + sideTable = [ + [], + [0,1], + [1,2], + [0,2], + [0,3], + [1,3], + [0,1,2,3], + [2,3], + [2,3], + [0,1,2,3], + [1,3], + [0,3], + [0,2], + [1,2], + [0,1], + [] + ] + + edgeKey=[ + [(0,1),(0,0)], + [(0,0), (1,0)], + [(1,0), (1,1)], + [(1,1), (0,1)] + ] + + + lines = [] + + ## mark everything below the isosurface level + mask = data < level + + ### make four sub-fields and compute indexes for grid cells + index = np.zeros([x-1 for x in data.shape], dtype=np.ubyte) + fields = np.empty((2,2), dtype=object) + slices = [slice(0,-1), slice(1,None)] + for i in [0,1]: + for j in [0,1]: + fields[i,j] = mask[slices[i], slices[j]] + #vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme + vertIndex = i+2*j + #print i,j,k," : ", fields[i,j,k], 2**vertIndex + index += fields[i,j] * 2**vertIndex + #print index + #print index + + ## add lines + for i in xrange(index.shape[0]): # data x-axis + for j in xrange(index.shape[1]): # data y-axis + sides = sideTable[index[i,j]] + for l in range(0, len(sides), 2): ## faces for this grid cell + edges = sides[l:l+2] + pts = [] + for m in [0,1]: # points in this face + p1 = edgeKey[edges[m]][0] # p1, p2 are points at either side of an edge + p2 = edgeKey[edges[m]][1] + v1 = data[i+p1[0], j+p1[1]] # v1 and v2 are the values at p1 and p2 + v2 = data[i+p2[0], j+p2[1]] + f = (level-v1) / (v2-v1) + fi = 1.0 - f + p = ( ## interpolate between corners + p1[0]*fi + p2[0]*f + i + 0.5, + p1[1]*fi + p2[1]*f + j + 0.5 + ) + pts.append(p) + lines.append(pts) + + return lines ## a list of pairs of points def isosurface(data, level): diff --git a/graphicsItems/VTickGroup.py b/graphicsItems/VTickGroup.py index 214c57b0..bec74633 100644 --- a/graphicsItems/VTickGroup.py +++ b/graphicsItems/VTickGroup.py @@ -63,6 +63,9 @@ class VTickGroup(UIGraphicsItem): #pass self.rebuildTicks() #self.valid = False + + def dataBounds(self, *args, **kargs): + return None ## item should never affect view autoscaling #def viewRangeChanged(self): ### called when the view is scaled diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index e83ff9e6..2a7d58a2 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -820,13 +820,13 @@ class ViewBox(GraphicsWidget): frac = (1.0, 1.0) xr = item.dataBounds(0, frac=frac[0]) yr = item.dataBounds(1, frac=frac[1]) - if xr is None: + if xr is None or xr == (None, None): useX = False xr = (0,0) - if yr is None: + if yr is None or yr == (None, None): useY = False yr = (0,0) - + bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) #print " item real:", bounds else: diff --git a/parametertree/parameterTypes.py b/parametertree/parameterTypes.py index a286f4f0..6d93470a 100644 --- a/parametertree/parameterTypes.py +++ b/parametertree/parameterTypes.py @@ -405,7 +405,7 @@ class ListParameterItem(WidgetParameterItem): #return vals[key] #else: #return key - print key, self.forward + #print key, self.forward return self.forward[key] def setValue(self, val): diff --git a/widgets/GraphicsView.py b/widgets/GraphicsView.py index 22c5736a..3d3be2da 100644 --- a/widgets/GraphicsView.py +++ b/widgets/GraphicsView.py @@ -16,6 +16,7 @@ from FileDialog import FileDialog from pyqtgraph.GraphicsScene import GraphicsScene import numpy as np import pyqtgraph.functions as fn +import pyqtgraph __all__ = ['GraphicsView'] @@ -38,20 +39,14 @@ class GraphicsView(QtGui.QGraphicsView): autoPixelRange=False. The exact visible range can be set with setRange(). The view can be panned using the middle mouse button and scaled using the right mouse button if - enabled via enableMouse().""" + enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality).""" self.closed = False QtGui.QGraphicsView.__init__(self, parent) - ## in general openGL is poorly supported in Qt. - ## we only enable it where the performance benefit is critical. if useOpenGL is None: - if 'linux' in sys.platform: ## linux has numerous bugs in opengl implementation - useOpenGL = False - elif 'darwin' in sys.platform: ## openGL greatly speeds up display on mac - useOpenGL = True - else: - useOpenGL = False + useOpenGL = pyqtgraph.getConfigOption('useOpenGL') + self.useOpenGL(useOpenGL) self.setCacheMode(self.CacheBackground) From 9c268b07282324f6549f55ede280e8e79967dc17 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 27 Mar 2012 12:33:02 -0400 Subject: [PATCH 044/238] fix from last rev --- functions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/functions.py b/functions.py index de94295b..a2edcb07 100644 --- a/functions.py +++ b/functions.py @@ -586,7 +586,6 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False): else: order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. if data.shape[2] == 1: for i in xrange(3): imgData[..., order[i]] = data[..., 0] @@ -1249,4 +1248,4 @@ def isosurface(data, level): - \ No newline at end of file + From 668640b42483bf5bbc6acb8b900cd1ee6a8c4c00 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 28 Mar 2012 14:16:42 -0400 Subject: [PATCH 045/238] Updated README to reflect REALITY --- README | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/README b/README index 0072a82f..3e12369a 100644 --- a/README +++ b/README @@ -1,5 +1,6 @@ PyQtGraph - A pure-Python graphics library for PyQt/PySide -Copyright 2011 University of North Carolina at Chapel Hill +Copyright 2011 Luke Campagnola, University of North Carolina at Chapel Hill +http://http://luke.campagnola.me/code/pyqtgraph Authors: Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') @@ -7,12 +8,33 @@ Authors: Ingo Breßler Requirements: - PyQt 4.5+ or (coming soon) PySide - python 2.6+ + PyQt 4.7+ or PySide + python 2.7+ (no python 3 support yet) numpy, scipy Known to run on Windows, Linux, and Mac. +Support: + Post at the mailing list / forum: + https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph + +Installation: + Pyqtgraph currently does not have (or really require) any installation + scripts. All that is needed is for the pyqtgraph folder to be placed + someplace importable. Most people will prefer to simply place this folder + within a larger project folder. If you want to make pyqtgraph available + system-wide, copy the folder to one of the directories listed in python's + sys.path list. + Documentation: - None. - You can look around in the examples directory or pester Luke to write some. + There are many examples; run "python -m pyqtgraph.examples" for a menu + Some (incomplete) documentation exists at this time. + - Easiest place to get documentation is at + http://http://luke.campagnola.me/code/pyqtgraph/documentation + - If you acquired this code as a .tar.gz file from the website, then you can also look in + pyqtgraph/documentation/html. + - If you acquired this code via BZR, then you can build the documentation using sphinx. + From the documentation directory, run: + $ make html + Please feel free to pester Luke or post to the forum if you need a specific + section of documentation. From bdef8dc4c73276a13719638ea447a82632a085de Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Fri, 30 Mar 2012 15:53:10 -0400 Subject: [PATCH 046/238] fixed example menu on windows --- examples/__main__.py | 2 +- graphicsItems/ROI.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 11c9785d..b58e50ee 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -90,7 +90,7 @@ class ExampleLoader(QtGui.QMainWindow): fn = self.currentFile() if fn is None: return - os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn) + os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, '"' + fn + '"') def showFile(self): diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index 1fc38c55..a81b4c13 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -28,7 +28,7 @@ from UIGraphicsItem import UIGraphicsItem __all__ = [ 'ROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', - 'LineROI', 'MultiLineROI', 'LineSegmentROI', 'SpiralROI' + 'LineROI', 'MultiLineROI', 'LineSegmentROI', 'SpiralROI', ] From 5a357ddb2a1c7be20f4877cde1f51116a4ac2d40 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 3 Apr 2012 01:01:33 -0400 Subject: [PATCH 047/238] Several minor bugfixes and features - Added rate-limited mode to SignalProxy - Added basic text justification to LabelItem - ViewBox.addItem now has ignoreBounds option, which causes the item to be ignored when autoscaling - Added ValueLabel widget - Fixed some autoscaling bugs - InfiniteLine fix - no hilight if movable=False --- SignalProxy.py | 29 ++++++++++--- Transform.py | 13 +++++- functions.py | 8 ++-- graphicsItems/GraphicsLayout.py | 2 +- graphicsItems/InfiniteLine.py | 2 +- graphicsItems/LabelItem.py | 60 +++++++++++++++++--------- graphicsItems/PlotItem/PlotItem.py | 5 ++- graphicsItems/ViewBox/ViewBox.py | 5 ++- widgets/ValueLabel.py | 67 ++++++++++++++++++++++++++++++ 9 files changed, 155 insertions(+), 36 deletions(-) create mode 100644 widgets/ValueLabel.py diff --git a/SignalProxy.py b/SignalProxy.py index e3719fcf..a4ace84f 100644 --- a/SignalProxy.py +++ b/SignalProxy.py @@ -7,31 +7,35 @@ __all__ = ['SignalProxy'] class SignalProxy(QtCore.QObject): """Object which collects rapid-fire signals and condenses them - into a single signal. Used, for example, to prevent a SpinBox - from generating multiple signals when the mouse wheel is rolled - over it. + into a single signal or a rate-limited stream of signals. + Used, for example, to prevent a SpinBox from generating multiple + signals when the mouse wheel is rolled over it. Emits sigDelayed after input signals have stopped for a certain period of time. """ sigDelayed = QtCore.Signal(object) - def __init__(self, signal, delay=0.3, slot=None): + def __init__(self, signal, delay=0.3, rateLimit=0, slot=None): """Initialization arguments: signal - a bound Signal or pyqtSignal instance delay - Time (in seconds) to wait for signals to stop before emitting (default 0.3s) slot - Optional function to connect sigDelayed to. + rateLimit - (signals/sec) if greater than 0, this allows signals to stream out at a + steady rate while they are being received. """ QtCore.QObject.__init__(self) signal.connect(self.signalReceived) self.signal = signal self.delay = delay + self.rateLimit = rateLimit self.args = None self.timer = ThreadsafeTimer.ThreadsafeTimer() self.timer.timeout.connect(self.flush) self.block = False self.slot = slot + self.lastFlushTime = None if slot is not None: self.sigDelayed.connect(slot) @@ -43,8 +47,20 @@ class SignalProxy(QtCore.QObject): if self.block: return self.args = args - self.timer.stop() - self.timer.start((self.delay*1000)+1) + if self.rateLimit == 0: + self.timer.stop() + self.timer.start((self.delay*1000)+1) + else: + now = time() + if self.lastFlushTime is None: + leakTime = 0 + else: + lastFlush = self.lastFlushTime + leakTime = max(0, (lastFlush + (1.0 / self.rateLimit)) - now) + + self.timer.stop() + self.timer.start((min(leakTime, self.delay)*1000)+1) + def flush(self): """If there is a signal queued up, send it now.""" @@ -54,6 +70,7 @@ class SignalProxy(QtCore.QObject): self.sigDelayed.emit(self.args) self.args = None self.timer.stop() + self.lastFlushTime = time() return True def disconnect(self): diff --git a/Transform.py b/Transform.py index 91614f1d..a7776696 100644 --- a/Transform.py +++ b/Transform.py @@ -5,8 +5,7 @@ import numpy as np class Transform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate - - This transform always has 0 shear. + This transform has no shear; angles are always preserved. """ def __init__(self, init=None): QtGui.QTransform.__init__(self) @@ -24,6 +23,16 @@ class Transform(QtGui.QTransform): elif isinstance(init, QtGui.QTransform): self.setFromQTransform(init) + + def getScale(self): + return self._state['scale'] + + def getAngle(self): + return self._state['angle'] + + def getTranslation(self): + return self._state['pos'] + def reset(self): self._state = { 'pos': Point(0,0), diff --git a/functions.py b/functions.py index a2edcb07..70ade4a5 100644 --- a/functions.py +++ b/functions.py @@ -72,7 +72,6 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al Return the number x formatted in engineering notation with SI prefix. Example:: - siFormat(0.0001, suffix='V') # returns "100 μV" """ @@ -90,8 +89,11 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al fmt = "%." + str(precision) + "g%s%s" return fmt % (x*p, pref, suffix) else: - plusminus = space + u"±" + space - fmt = "%." + str(precision) + u"g%s%s%s%s" + if allowUnicode: + plusminus = space + u"±" + space + else: + plusminus = " +/- " + fmt = "%." + str(precision) + "g%s%s%s%s" return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) def siEval(s): diff --git a/graphicsItems/GraphicsLayout.py b/graphicsItems/GraphicsLayout.py index a35c79e3..95d98b30 100644 --- a/graphicsItems/GraphicsLayout.py +++ b/graphicsItems/GraphicsLayout.py @@ -45,7 +45,7 @@ class GraphicsLayout(GraphicsWidget): self.addItem(vb, row, col, rowspan, colspan) return vb - def addLabel(self, text, row=None, col=None, rowspan=1, colspan=1, **kargs): + def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs): text = LabelItem(text, **kargs) self.addItem(text, row, col, rowspan, colspan) return text diff --git a/graphicsItems/InfiniteLine.py b/graphicsItems/InfiniteLine.py index ebf24502..cffbeb52 100644 --- a/graphicsItems/InfiniteLine.py +++ b/graphicsItems/InfiniteLine.py @@ -221,7 +221,7 @@ class InfiniteLine(UIGraphicsItem): self.sigPositionChangeFinished.emit(self) def hoverEvent(self, ev): - if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): + if (not ev.isExit()) and self.movable and ev.acceptDrags(QtCore.Qt.LeftButton): self.currentPen = fn.mkPen(255, 0,0) else: self.currentPen = self.pen diff --git a/graphicsItems/LabelItem.py b/graphicsItems/LabelItem.py index 6a2f6b48..b27e79c6 100644 --- a/graphicsItems/LabelItem.py +++ b/graphicsItems/LabelItem.py @@ -14,15 +14,14 @@ class LabelItem(GraphicsWidget): """ - def __init__(self, text, parent=None, angle=0, **args): + def __init__(self, text=' ', parent=None, angle=0, **args): GraphicsWidget.__init__(self, parent) self.item = QtGui.QGraphicsTextItem(self) - self.opts = args - if 'color' not in args: - self.opts['color'] = 'CCC' - else: - if isinstance(args['color'], QtGui.QColor): - self.opts['color'] = fn.colorStr(args['color'])[:6] + self.opts = { + 'color': 'CCC', + 'justify': 'center' + } + self.opts.update(args) self.sizeHint = {} self.setText(text) self.setAngle(angle) @@ -47,6 +46,8 @@ class LabelItem(GraphicsWidget): optlist = [] if 'color' in opts: + if isinstance(opts['color'], QtGui.QColor): + opts['color'] = fn.colorStr(opts['color'])[:6] optlist.append('color: #' + opts['color']) if 'size' in opts: optlist.append('font-size: ' + opts['size']) @@ -58,13 +59,25 @@ class LabelItem(GraphicsWidget): #print full self.item.setHtml(full) self.updateMin() + self.resizeEvent(None) + self.update() def resizeEvent(self, ev): - c1 = self.boundingRect().center() - c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos() - dif = c1 - c2 - self.item.moveBy(dif.x(), dif.y()) + #c1 = self.boundingRect().center() + #c2 = self.item.mapToParent(self.item.boundingRect().center()) # + self.item.pos() + #dif = c1 - c2 + #self.item.moveBy(dif.x(), dif.y()) #print c1, c2, dif, self.item.pos() + if self.opts['justify'] == 'left': + self.item.setPos(0,0) + elif self.opts['justify'] == 'center': + bounds = self.item.mapRectToParent(self.item.boundingRect()) + self.item.setPos(self.width()/2. - bounds.width()/2., 0) + elif self.opts['justify'] == 'right': + bounds = self.item.mapRectToParent(self.item.boundingRect()) + self.item.setPos(self.width() - bounds.width(), 0) + #if self.width() > 0: + #self.item.setTextWidth(self.width()) def setAngle(self, angle): self.angle = angle @@ -76,16 +89,23 @@ class LabelItem(GraphicsWidget): bounds = self.item.mapRectToParent(self.item.boundingRect()) self.setMinimumWidth(bounds.width()) self.setMinimumHeight(bounds.height()) - #print self.text, bounds.width(), bounds.height() - #self.sizeHint = { - #QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), - #QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), - #QtCore.Qt.MaximumSize: (bounds.width()*2, bounds.height()*2), - #QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? - #} + self.sizeHint = { + QtCore.Qt.MinimumSize: (bounds.width(), bounds.height()), + QtCore.Qt.PreferredSize: (bounds.width(), bounds.height()), + QtCore.Qt.MaximumSize: (-1, -1), #bounds.width()*2, bounds.height()*2), + QtCore.Qt.MinimumDescent: (0, 0) ##?? what is this? + } + self.update() - #def sizeHint(self, hint, constraint): - #return self.sizeHint[hint] + def sizeHint(self, hint, constraint): + if hint not in self.sizeHint: + return QtCore.QSizeF(0, 0) + return QtCore.QSizeF(*self.sizeHint[hint]) + #def paint(self, p, *args): + #p.setPen(fn.mkPen('r')) + #p.drawRect(self.rect()) + #p.drawRect(self.item.boundingRect()) + \ No newline at end of file diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 5f95a6dd..b9fcff5a 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -690,7 +690,10 @@ class PlotItem(GraphicsWidget): def addItem(self, item, *args, **kargs): self.items.append(item) - self.vb.addItem(item, *args) + vbargs = {} + if 'ignoreBounds' in kargs: + vbargs['ignoreBounds'] = kargs['ignoreBounds'] + self.vb.addItem(item, *args, **vbargs) if hasattr(item, 'implements') and item.implements('plotData'): self.dataItems.append(item) #self.plotChanged() diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 2a7d58a2..c43b9764 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -197,11 +197,12 @@ class ViewBox(GraphicsWidget): def mouseEnabled(self): return self.state['mouseEnabled'][:] - def addItem(self, item): + def addItem(self, item, ignoreBounds=False): if item.zValue() < self.zValue(): item.setZValue(self.zValue()+1) item.setParentItem(self.childGroup) - self.addedItems.append(item) + if not ignoreBounds: + self.addedItems.append(item) self.updateAutoRange() #print "addItem:", item, item.boundingRect() diff --git a/widgets/ValueLabel.py b/widgets/ValueLabel.py new file mode 100644 index 00000000..c072b62a --- /dev/null +++ b/widgets/ValueLabel.py @@ -0,0 +1,67 @@ +from pyqtgraph.Qt import QtCore, QtGui +from pyqtgraph.ptime import time +import pyqtgraph as pg + +__all__ = ['ValueLabel'] + +class ValueLabel(QtGui.QLabel): + """ + QLabel specifically for displaying numerical values. + Extends QLabel adding some extra functionality: + - displaying units with si prefix + - built-in exponential averaging + """ + + def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None): + """ + Arguments: + *suffix* (str or None) The suffix to place after the value + *siPrefix* (bool) Whether to add an SI prefix to the units and display a scaled value + *averageTime* (float) The length of time in seconds to average values. If this value + is 0, then no averaging is performed. As this value increases + the display value will appear to change more slowly and smoothly. + *formatStr* (str) Optionally, provide a format string to use when displaying text. The text will + be generated by calling formatStr.format(value=, avgValue=, suffix=) + (see Python documentation on str.format) + This option is not compatible with siPrefix + """ + QtGui.QLabel.__init__(self, parent) + self.values = [] + self.averageTime = averageTime ## no averaging by default + self.suffix = suffix + self.siPrefix = siPrefix + if formatStr is None: + formatStr = '{avgValue:0.2g} {suffix}' + self.formatStr = formatStr + + def setValue(self, value): + now = time() + self.values.append((now, value)) + cutoff = now - self.averageTime + while len(self.values) > 0 and self.values[0][0] < cutoff: + self.values.pop(0) + self.update() + + def setFormatStr(self, text): + self.formatStr = text + self.update() + + + def averageValue(self): + return reduce(lambda a,b: a+b, [v[1] for v in self.values]) / float(len(self.values)) + + + def paintEvent(self, ev): + self.setText(self.generateText()) + return QtGui.QLabel.paintEvent(self, ev) + + def generateText(self): + if len(self.values) == 0: + return '' + avg = self.averageValue() + val = self.values[-1][1] + if self.siPrefix: + return pg.siFormat(avg, suffix=self.suffix) + else: + return self.formatStr.format(value=val, avgValue=avg, suffix=self.suffix) + \ No newline at end of file From 33b09dfa235fab6bc2e0b3548d463f67fbf6a93d Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Tue, 3 Apr 2012 01:11:39 -0400 Subject: [PATCH 048/238] Added crosshair example --- examples/crosshair.py | 55 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 examples/crosshair.py diff --git a/examples/crosshair.py b/examples/crosshair.py new file mode 100644 index 00000000..13c70029 --- /dev/null +++ b/examples/crosshair.py @@ -0,0 +1,55 @@ +import initExample ## Add path to library (just for examples; you do not need this) +import numpy as np +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from pyqtgraph.Point import Point + +#genearte layout +app = QtGui.QApplication([]) +win = pg.GraphicsWindow() +label = pg.LabelItem(justify='right') +win.addItem(label) +p1 = win.addPlot(row=1, col=0) +p2 = win.addPlot(row=2, col=0) +region = pg.LinearRegionItem() +region.setZValue(10) +p2.addItem(region) + +#create numpy arrays +#make the numbers large to show that the xrange shows data from 10000 to all the way 0 +data1 = 10000 + 3000 * np.random.random(size=10000) +data2 = 15000 + 3000 * np.random.random(size=10000) + +p1.plot(data1, pen="r") +p1.plot(data2, pen="g") +p2.plot(data1, pen="w") + +def update(): + p1.setXRange(*region.getRegion()) +region.sigRegionChanged.connect(update) +region.setRegion([1000, 2000]) + +#cross hair +vLine = pg.InfiniteLine(angle=90, movable=False) +hLine = pg.InfiniteLine(angle=0, movable=False) +p1.addItem(vLine, ignoreBounds=True) +p1.addItem(hLine, ignoreBounds=True) +vb = p1.vb + +def mouseMoved(evt): + pos = evt[0] ## using signal proxy turns original arguments into a tuple + if p1.sceneBoundingRect().contains(pos): + mousePoint = vb.mapSceneToView(pos) + index = int(mousePoint.x()) + if index > 0 and index < len(data1): + label.setText("x=%0.1f, y1=%0.1f, y2=%0.1f" % (mousePoint.x(), data1[index], data2[index])) + vLine.setPos(mousePoint.x()) + hLine.setPos(mousePoint.y()) +proxy = pg.SignalProxy(p1.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved) +#p1.scene().sigMouseMoved.connect(mouseMoved) + + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() \ No newline at end of file From 78d4bc08380a335ac7f8888bb2c00c03ba30393b Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 09:29:35 -0400 Subject: [PATCH 049/238] Performance enhancements - HistogramLUTItem avoids using lookup table if possible - GradientEditorItem has a method to ask whether the gradient is trivial (can be applied without the use of a lookup table) - ROI, LinearRegionItem, InfiniteLine no longer redraw for every mouse movement --- __init__.py | 4 +-- examples/PlotSpeedTest.py | 15 +++++++--- examples/VideoSpeedTest.py | 2 ++ graphicsItems/GradientEditorItem.py | 14 ++++++++- graphicsItems/HistogramLUTItem.py | 44 +++++++++++++++++++++++------ graphicsItems/ImageItem.py | 3 ++ graphicsItems/InfiniteLine.py | 11 ++++++++ graphicsItems/LinearRegionItem.py | 16 ++++++++++- graphicsItems/PlotCurveItem.py | 2 ++ graphicsItems/PlotDataItem.py | 9 ++++-- graphicsItems/ROI.py | 15 ++++++++-- widgets/GraphicsView.py | 6 ++++ 12 files changed, 120 insertions(+), 21 deletions(-) diff --git a/__init__.py b/__init__.py index 398aa020..5c260d1a 100644 --- a/__init__.py +++ b/__init__.py @@ -18,10 +18,10 @@ if 'linux' in sys.platform: ## linux has numerous bugs in opengl implementation elif 'darwin' in sys.platform: ## openGL greatly speeds up display on mac useOpenGL = True else: - useOpenGL = True ## on windows there's a more even performance / bugginess tradeoff. + useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. CONFIG_OPTIONS = { - 'useOpenGL': None, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. + 'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. 'leftButtonPan': True ## if false, left button drags a rubber band for zooming in viewbox } diff --git a/examples/PlotSpeedTest.py b/examples/PlotSpeedTest.py index b695bd86..212734a1 100644 --- a/examples/PlotSpeedTest.py +++ b/examples/PlotSpeedTest.py @@ -1,14 +1,14 @@ #!/usr/bin/python # -*- coding: utf-8 -*- ## Add path to library (just for examples; you do not need this) -import sys, os, time +import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg - +from pyqtgraph.ptime import time #QtGui.QApplication.setGraphicsSystem('raster') app = QtGui.QApplication([]) #mw = QtGui.QMainWindow() @@ -18,15 +18,22 @@ p = pg.plot() p.setRange(QtCore.QRectF(0, -10, 5000, 20)) p.setLabel('bottom', 'Index', units='B') curve = p.plot() + +#curve.setFillBrush((0, 0, 100, 100)) +#curve.setFillLevel(0) + +#lr = pg.LinearRegionItem([100, 4900]) +#p.addItem(lr) + data = np.random.normal(size=(50,5000)) ptr = 0 -lastTime = time.time() +lastTime = time() fps = None def update(): global curve, data, ptr, p, lastTime, fps curve.setData(data[ptr%10]) ptr += 1 - now = time.time() + now = time() dt = now - lastTime lastTime = now if fps is None: diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index 49d4c715..57d8aacc 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -25,6 +25,8 @@ win.show() ui.maxSpin1.setOpts(value=255, step=1) ui.minSpin1.setOpts(value=0, step=1) +#ui.graphicsView.useOpenGL() ## buggy, but you can try it if you need extra speed. + vb = pg.ViewBox() ui.graphicsView.setCentralItem(vb) vb.setAspectLocked() diff --git a/graphicsItems/GradientEditorItem.py b/graphicsItems/GradientEditorItem.py index d3eaaf86..c06995c7 100644 --- a/graphicsItems/GradientEditorItem.py +++ b/graphicsItems/GradientEditorItem.py @@ -467,7 +467,19 @@ class GradientEditorItem(TickSliderItem): return table - + def isLookupTrivial(self): + """Return true if the gradient has exactly two stops in it: black at 0.0 and white at 1.0""" + ticks = self.listTicks() + if len(ticks) != 2: + return False + if ticks[0][1] != 0.0 or ticks[1][1] != 1.0: + return False + c1 = fn.colorTuple(ticks[0][0].color) + c2 = fn.colorTuple(ticks[1][0].color) + if c1 != (0,0,0,255) or c2 != (255,255,255,255): + return False + return True + def mouseReleaseEvent(self, ev): TickSliderItem.mouseReleaseEvent(self, ev) diff --git a/graphicsItems/HistogramLUTItem.py b/graphicsItems/HistogramLUTItem.py index 19599720..d51bbd10 100644 --- a/graphicsItems/HistogramLUTItem.py +++ b/graphicsItems/HistogramLUTItem.py @@ -15,17 +15,30 @@ from GridItem import * from pyqtgraph.Point import Point import pyqtgraph.functions as fn import numpy as np +import pyqtgraph.debug as debug __all__ = ['HistogramLUTItem'] class HistogramLUTItem(GraphicsWidget): + """ + This is a graphicsWidget which provides controls for adjusting the display of an image. + Includes: + - Image histogram + - Movable region over histogram to select black/white levels + - Gradient editor to define color lookup table for single-channel images + """ + sigLookupTableChanged = QtCore.Signal(object) sigLevelsChanged = QtCore.Signal(object) sigLevelChangeFinished = QtCore.Signal(object) - def __init__(self, image=None): + def __init__(self, image=None, fillHistogram=True): + """ + If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. + By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. + """ GraphicsWidget.__init__(self) self.lut = None self.imageItem = None @@ -61,6 +74,8 @@ class HistogramLUTItem(GraphicsWidget): self.vb.sigRangeChanged.connect(self.viewRangeChanged) self.plot = PlotDataItem() self.plot.rotate(90) + self.fillHistogram(fillHistogram) + self.vb.addItem(self.plot) self.autoHistogramRange() @@ -68,7 +83,13 @@ class HistogramLUTItem(GraphicsWidget): self.setImageItem(image) #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) - + def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): + if fill: + self.plot.setFillLevel(level) + self.plot.setFillBrush(color) + else: + self.plot.setFillLevel(None) + #def sizeHint(self, *args): #return QtCore.QSizeF(115, 200) @@ -129,21 +150,24 @@ class HistogramLUTItem(GraphicsWidget): def gradientChanged(self): if self.imageItem is not None: - self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result + if self.gradient.isLookupTrivial(): + self.imageItem.setLookupTable(None) #lambda x: x.astype(np.uint8)) + else: + self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result self.lut = None #if self.imageItem is not None: #self.imageItem.setLookupTable(self.gradient.getLookupTable(512)) self.sigLookupTableChanged.emit(self) - def getLookupTable(self, img=None, n=None): + def getLookupTable(self, img=None, n=None, alpha=False): if n is None: if img.dtype == np.uint8: n = 256 else: n = 512 if self.lut is None: - self.lut = self.gradient.getLookupTable(n) + self.lut = self.gradient.getLookupTable(n, alpha=alpha) return self.lut def regionChanged(self): @@ -159,17 +183,19 @@ class HistogramLUTItem(GraphicsWidget): self.update() def imageChanged(self, autoLevel=False, autoRange=False): + prof = debug.Profiler('HistogramLUTItem.imageChanged', disabled=True) h = self.imageItem.getHistogram() + prof.mark('get histogram') if h[0] is None: return - self.plot.setData(*h, fillLevel=0.0, brush=(100, 100, 200)) + self.plot.setData(*h) + prof.mark('set plot') if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) - #self.updateRange() - #if autoRange: - #self.updateRange() + prof.mark('set region') + prof.finish() def getLevels(self): return self.region.getRegion() diff --git a/graphicsItems/ImageItem.py b/graphicsItems/ImageItem.py index 89bd6b64..423183ab 100644 --- a/graphicsItems/ImageItem.py +++ b/graphicsItems/ImageItem.py @@ -224,10 +224,13 @@ class ImageItem(GraphicsObject): return if self.qimage is None: self.render() + prof.mark('render QImage') if self.paintMode is not None: p.setCompositionMode(self.paintMode) + prof.mark('set comp mode') p.drawImage(QtCore.QPointF(0,0), self.qimage) + prof.mark('p.drawImage') if self.border is not None: p.setPen(self.border) p.drawRect(self.boundingRect()) diff --git a/graphicsItems/InfiniteLine.py b/graphicsItems/InfiniteLine.py index cffbeb52..a886876b 100644 --- a/graphicsItems/InfiniteLine.py +++ b/graphicsItems/InfiniteLine.py @@ -35,6 +35,7 @@ class InfiniteLine(UIGraphicsItem): self.maxRange = bounds self.moving = False self.setMovable(movable) + self.mouseHovering = False self.p = [0, 0] self.setAngle(angle) if pos is None: @@ -222,6 +223,16 @@ class InfiniteLine(UIGraphicsItem): def hoverEvent(self, ev): if (not ev.isExit()) and self.movable and ev.acceptDrags(QtCore.Qt.LeftButton): + self.setMouseHover(True) + else: + self.setMouseHover(False) + + def setMouseHover(self, hover): + ## Inform the item that the mouse is(not) hovering over it + if self.mouseHovering == hover: + return + self.mouseHovering = hover + if hover: self.currentPen = fn.mkPen(255, 0,0) else: self.currentPen = self.pen diff --git a/graphicsItems/LinearRegionItem.py b/graphicsItems/LinearRegionItem.py index 1b546cb7..bdffd075 100644 --- a/graphicsItems/LinearRegionItem.py +++ b/graphicsItems/LinearRegionItem.py @@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtGui, QtCore from UIGraphicsItem import UIGraphicsItem from InfiniteLine import InfiniteLine import pyqtgraph.functions as fn +import pyqtgraph.debug as debug __all__ = ['LinearRegionItem'] @@ -24,6 +25,7 @@ class LinearRegionItem(UIGraphicsItem): self.bounds = QtCore.QRectF() self.blockLineSignal = False self.moving = False + self.mouseHovering = False if orientation == LinearRegionItem.Horizontal: self.lines = [ @@ -94,9 +96,11 @@ class LinearRegionItem(UIGraphicsItem): return br.normalized() def paint(self, p, *args): + #prof = debug.Profiler('LinearRegionItem.paint') UIGraphicsItem.paint(self, p, *args) p.setBrush(self.currentBrush) p.drawRect(self.boundingRect()) + #prof.finish() def dataBounds(self, axis, frac=1.0): if axis == self.orientation: @@ -197,13 +201,23 @@ class LinearRegionItem(UIGraphicsItem): def hoverEvent(self, ev): if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): + self.setMouseHover(True) + else: + self.setMouseHover(False) + + def setMouseHover(self, hover): + ## Inform the item that the mouse is(not) hovering over it + if self.mouseHovering == hover: + return + self.mouseHovering = hover + if hover: c = self.brush.color() c.setAlpha(c.alpha() * 2) self.currentBrush = fn.mkBrush(c) else: self.currentBrush = self.brush self.update() - + #def hoverEnterEvent(self, ev): #print "rgn hover enter" #ev.ignore() diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index 12ac044c..bc3629d2 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -352,7 +352,9 @@ class PlotCurveItem(GraphicsObject): p2.closeSubpath() self.fillPath = p2 + prof.mark('generate fill path') p.fillPath(self.fillPath, self.opts['brush']) + prof.mark('draw fill path') ## Copy pens and apply alpha adjustment diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index 1938cd50..22c62d1a 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -11,6 +11,7 @@ from ScatterPlotItem import ScatterPlotItem import numpy as np import scipy import pyqtgraph.functions as fn +import pyqtgraph.debug as debug class PlotDataItem(GraphicsObject): """GraphicsItem for displaying plot curves, scatter plots, or both.""" @@ -215,7 +216,7 @@ class PlotDataItem(GraphicsObject): """ #self.clear() - + prof = debug.Profiler('PlotDataItem.setData (0x%x)' % id(self), disabled=True) y = None x = None if len(args) == 1: @@ -262,7 +263,7 @@ class PlotDataItem(GraphicsObject): if 'y' in kargs: y = kargs['y'] - + prof.mark('interpret data') ## pull in all style arguments. ## Use self.opts to fill in anything not present in kargs. @@ -305,12 +306,16 @@ class PlotDataItem(GraphicsObject): self.yData = y.view(np.ndarray) self.xDisp = None self.yDisp = None + prof.mark('set data') self.updateItems() + prof.mark('update items') view = self.getViewBox() if view is not None: view.itemBoundsChanged(self) ## inform view so it can update its range if it wants self.sigPlotChanged.emit(self) + prof.mark('emit') + prof.finish() def updateItems(self): diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py index a81b4c13..5c5f930a 100644 --- a/graphicsItems/ROI.py +++ b/graphicsItems/ROI.py @@ -55,7 +55,7 @@ class ROI(GraphicsObject): self.rotateAllowed = True self.freeHandleMoved = False ## keep track of whether free handles have moved since last change signal was emitted. - + self.mouseHovering = False if pen is None: pen = (255, 255, 255) self.setPen(pen) @@ -334,11 +334,22 @@ class ROI(GraphicsObject): def hoverEvent(self, ev): if self.translatable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): - self.currentPen = fn.mkPen(255, 255, 0) + self.setMouseHover(True) self.sigHoverEvent.emit(self) + else: + self.setMouseHover(False) + + def setMouseHover(self, hover): + ## Inform the ROI that the mouse is(not) hovering over it + if self.mouseHovering == hover: + return + self.mouseHovering = hover + if hover: + self.currentPen = fn.mkPen(255, 255, 0) else: self.currentPen = self.pen self.update() + def mouseDragEvent(self, ev): if ev.isStart(): diff --git a/widgets/GraphicsView.py b/widgets/GraphicsView.py index 3d3be2da..92a5d5e4 100644 --- a/widgets/GraphicsView.py +++ b/widgets/GraphicsView.py @@ -16,6 +16,7 @@ from FileDialog import FileDialog from pyqtgraph.GraphicsScene import GraphicsScene import numpy as np import pyqtgraph.functions as fn +import pyqtgraph.debug as debug import pyqtgraph __all__ = ['GraphicsView'] @@ -395,6 +396,11 @@ class GraphicsView(QtGui.QGraphicsView): #self.pev = pev #self.currentItem.mouseMoveEvent(pev) + #def paintEvent(self, ev): + #prof = debug.Profiler('GraphicsView.paintEvent (0x%x)' % id(self)) + #QtGui.QGraphicsView.paintEvent(self, ev) + #prof.finish() + def pixelSize(self): """Return vector with the length and width of one view pixel in scene coordinates""" From 6aef85331e0f5d920dd418b3b87c7eaa31302c87 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 09:31:58 -0400 Subject: [PATCH 050/238] Cleanup for AxisItem - Made more extensible by breaking out tick spacing and text generating into separate methods - Text now tries harder to avoid overlapping --- graphicsItems/AxisItem.py | 314 +++++++++++++++++++++----------------- 1 file changed, 172 insertions(+), 142 deletions(-) diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 4015f8fe..563f3fb4 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -13,6 +13,7 @@ class AxisItem(GraphicsWidget): GraphicsItem showing a single plot axis with ticks, values, and label. Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. Ticks can be extended to make a grid. + If maxTickLength is negative, ticks point into the plot. """ @@ -258,6 +259,100 @@ class AxisItem(GraphicsWidget): painter.end() self.picture.play(p) + + + def tickSpacing(self, minVal, maxVal, size): + """Return values describing the desired spacing and offset of ticks. + + This method is called whenever the axis needs to be redrawn and is a + good method to override in subclasses that require control over tick locations. + + The return value must be a list of three tuples: + [ + (major tick spacing, offset), + (minor tick spacing, offset), + (sub-minor tick spacing, offset), + ... + ] + """ + dif = abs(maxVal - minVal) + if dif == 0: + return [] + + ## decide optimal minor tick spacing in pixels (this is just aesthetics) + pixelSpacing = np.log(size+10) * 5 + optimalTickCount = size / pixelSpacing + if optimalTickCount < 1: + optimalTickCount = 1 + + ## optimal minor tick spacing + optimalSpacing = dif / optimalTickCount + + ## the largest power-of-10 spacing which is smaller than optimal + p10unit = 10 ** np.floor(np.log10(optimalSpacing)) + + ## Determine major/minor tick spacings which flank the optimal spacing. + intervals = np.array([1., 2., 10., 20., 100.]) * p10unit + minorIndex = 0 + while intervals[minorIndex+1] <= optimalSpacing: + minorIndex += 1 + + return [ + (intervals[minorIndex+2], 0), + (intervals[minorIndex+1], 0), + (intervals[minorIndex], 0) + ] + + + def tickValues(self, minVal, maxVal, size): + """ + Return the values and spacing of ticks to draw + [ + (spacing, [major ticks]), + (spacing, [minor ticks]), + ... + ] + + By default, this method calls tickSpacing to determine the correct tick locations. + This is a good method to override in subclasses. + """ + ticks = [] + tickLevels = self.tickSpacing(minVal, maxVal, size) + for i in range(len(tickLevels)): + spacing, offset = tickLevels[i] + + ## determine starting tick + start = (np.ceil((minVal-offset) / spacing) * spacing) + offset + + ## determine number of ticks + num = int((maxVal-start) / spacing) + 1 + ticks.append((spacing, np.arange(num) * spacing + start)) + return ticks + + + def tickStrings(self, values, scale, spacing): + """Return the strings that should be placed next to ticks. This method is called + when redrawing the axis and is a good method to override in subclasses. + The method is called with a list of tick values, a scaling factor (see below), and the + spacing between ticks (this is required since, in some instances, there may be only + one tick and thus no other way to determine the tick spacing) + + The scale argument is used when the axis label is displaying units which may have an SI scaling prefix. + When determining the text to display, use value*scale to correctly account for this prefix. + For example, if the axis label's units are set to 'V', then a tick value of 0.001 might + be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and + thus the tick should display 0.001 * 1000 = 1. + """ + places = max(0, np.ceil(-np.log10(spacing*scale))) + strings = [] + for v in values: + vs = v * scale + if abs(vs) < .001 or abs(vs) >= 10000: + vstr = "%g" % vs + else: + vstr = ("%%0.%df" % places) % vs + strings.append(vstr) + return strings def drawPicture(self, p): @@ -272,198 +367,133 @@ class AxisItem(GraphicsWidget): linkedView = self.linkedView() if linkedView is None or self.grid is False: - tbounds = bounds + tickBounds = bounds else: - tbounds = linkedView.mapRectToItem(self, linkedView.boundingRect()) + tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect()) if self.orientation == 'left': span = (bounds.topRight(), bounds.bottomRight()) - tickStart = tbounds.right() + tickStart = tickBounds.right() tickStop = bounds.right() tickDir = -1 axis = 0 elif self.orientation == 'right': span = (bounds.topLeft(), bounds.bottomLeft()) - tickStart = tbounds.left() + tickStart = tickBounds.left() tickStop = bounds.left() tickDir = 1 axis = 0 elif self.orientation == 'top': span = (bounds.bottomLeft(), bounds.bottomRight()) - tickStart = tbounds.bottom() + tickStart = tickBounds.bottom() tickStop = bounds.bottom() tickDir = -1 axis = 1 elif self.orientation == 'bottom': span = (bounds.topLeft(), bounds.topRight()) - tickStart = tbounds.top() + tickStart = tickBounds.top() tickStop = bounds.top() tickDir = 1 axis = 1 - + #print tickStart, tickStop, span + ## draw long line along axis p.drawLine(*span) + p.translate(0.5,0) ## resolves some damn pixel ambiguity ## determine size of this item in pixels points = map(self.mapToDevice, span) lengthInPixels = Point(points[1] - points[0]).length() - - ## decide optimal tick spacing in pixels - pixelSpacing = np.log(lengthInPixels+10) * 2 - optimalTickCount = lengthInPixels / pixelSpacing - - ## Determine optimal tick spacing - #intervals = [1., 2., 5., 10., 20., 50.] - #intervals = [1., 2.5, 5., 10., 25., 50.] - intervals = np.array([0.1, 0.2, 1., 2., 10., 20., 100., 200.]) - dif = abs(self.range[1] - self.range[0]) - if dif == 0.0: + if lengthInPixels == 0: return - pw = 10 ** (np.floor(np.log10(dif))-1) - scaledIntervals = intervals * pw - scaledTickCounts = dif / scaledIntervals - try: - i1 = np.argwhere(scaledTickCounts < optimalTickCount)[0,0] - except: - print "AxisItem can't determine tick spacing:" - print "scaledTickCounts", scaledTickCounts - print "optimalTickCount", optimalTickCount - print "dif", dif - print "scaledIntervals", scaledIntervals - print "intervals", intervals - print "pw", pw - print "pixelSpacing", pixelSpacing - i1 = 1 - - distBetweenIntervals = (optimalTickCount-scaledTickCounts[i1]) / (scaledTickCounts[i1-1]-scaledTickCounts[i1]) - - #print optimalTickCount, i1, scaledIntervals, distBetweenIntervals - - #for i in range(len(intervals)): - #i1 = i - #if dif / (pw*intervals[i]) < 10: - #break + + + tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels) textLevel = 1 ## draw text at this scale level - #print "range: %s dif: %f power: %f interval: %f spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp) - - #print " start at %f, %d ticks" % (start, num) - - + ## determine mapping between tick values and local coordinates + dif = self.range[1] - self.range[0] if axis == 0: - xs = -bounds.height() / dif + xScale = -bounds.height() / dif + offset = self.range[0] * xScale - bounds.height() else: - xs = bounds.width() / dif + xScale = bounds.width() / dif + offset = self.range[0] * xScale prof.mark('init') - tickPositions = set() # remembers positions of previously drawn ticks - ## draw ticks and generate list of texts to draw + tickPositions = [] # remembers positions of previously drawn ticks + + ## draw ticks ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching) ## draw three different intervals, long ticks first - texts = [] - for i in [2,1,0]: - if i1+i >= len(intervals) or i1+i < 0: - print "AxisItem.paint error: i1=%d, i=%d, len(intervals)=%d" % (i1, i, len(intervals)) - continue - - ## spacing for this interval - sp = pw*intervals[i1+i] - - ## determine starting tick - start = np.ceil(self.range[0] / sp) * sp - - ## determine number of ticks - num = int(dif / sp) + 1 - - ## last tick value - last = start + sp * num - - ## Number of decimal places to print - maxVal = max(abs(start), abs(last)) - places = max(0, np.ceil(-np.log10(sp*self.scale))) - #print i, sp, sp*self.scale, np.log10(sp*self.scale), places + for i in range(len(tickLevels)): + tickPositions.append([]) + ticks = tickLevels[i][1] ## length of tick - #h = np.clip((self.tickLength*3 / num) - 1., min(0, self.tickLength), max(0, self.tickLength)) - if i == 0: - h = self.tickLength * distBetweenIntervals / 2. - else: - h = self.tickLength*i/2. - - ## alpha - if i == 0: - #a = min(255, (765. / num) - 1.) - a = 255 * distBetweenIntervals - else: - a = 255 - - lineAlpha = a - textAlpha = a + tickLength = self.tickLength / ((i*1.0)+1.0) + lineAlpha = 255 / (i+1) if self.grid is not False: - lineAlpha = int(lineAlpha * self.grid / 255.) + lineAlpha = self.grid - if axis == 0: - offset = self.range[0] * xs - bounds.height() - else: - offset = self.range[0] * xs - - for j in range(num): - v = start + sp * j - x = (v * xs) - offset - p1 = [0, 0] - p2 = [0, 0] + for v in ticks: + x = (v * xScale) - offset + p1 = [x, x] + p2 = [x, x] p1[axis] = tickStart p2[axis] = tickStop if self.grid is False: - p2[axis] += h*tickDir - p1[1-axis] = p2[1-axis] = x - - if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]: - continue - if p1[1-axis] < 0: - continue + p2[axis] += tickLength*tickDir p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha))) - - # draw tick only if there is none - tickPos = p1[1-axis] - - #if tickPos not in tickPositions: p.drawLine(Point(p1), Point(p2)) - #tickPositions.add(tickPos) - if i == textLevel: - if abs(v*self.scale) < .001 or abs(v*self.scale) >= 10000: - vstr = "%g" % (v * self.scale) - else: - vstr = ("%%0.%df" % places) % (v * self.scale) - #print " ", v*self.scale, places, vstr - - textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) - height = textRect.height() - self.textHeight = height - if self.orientation == 'left': - textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height) - elif self.orientation == 'right': - textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter - rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height) - elif self.orientation == 'top': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom - rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height) - elif self.orientation == 'bottom': - textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop - rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) - - #p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a))) - #p.drawText(rect, textFlags, vstr) - texts.append((rect, textFlags, vstr, textAlpha)) - + tickPositions[i].append(x) prof.mark('draw ticks') - for args in texts: - p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, args[3]))) - p.drawText(*args[:3]) + + ## determine level to draw text + best = 0 + for i in range(len(tickLevels)): + ## take a small sample of strings and measure their rendered text + spacing, values = tickLevels[i] + strings = self.tickStrings(values[:2], self.scale, spacing) + textRects = [p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings] + if axis == 0: + textSize = np.max([r.height() for r in textRects]) + else: + textSize = np.max([r.width() for r in textRects]) + + ## If these strings are not too crowded, then this level is ok + textFillRatio = float(textSize * len(values)) / lengthInPixels + if textFillRatio < 0.7: + best = i + continue + prof.mark('measure text') + + spacing, values = tickLevels[best] + strings = self.tickStrings(values, self.scale, spacing) + for j in range(len(strings)): + vstr = strings[j] + x = tickPositions[best][j] + textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr) + height = textRect.height() + self.textHeight = height + if self.orientation == 'left': + textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height) + elif self.orientation == 'right': + textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter + rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height) + elif self.orientation == 'top': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom + rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height) + elif self.orientation == 'bottom': + textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop + rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height) + + p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150))) + p.drawText(rect, textFlags, vstr) prof.mark('draw text') prof.finish() From fffbd5548e2391b022180222f8717adcf2ff49aa Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 09:32:38 -0400 Subject: [PATCH 051/238] Added CSV exporter (only for PlotItem) --- exporters/CSVExporter.py | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 exporters/CSVExporter.py diff --git a/exporters/CSVExporter.py b/exporters/CSVExporter.py new file mode 100644 index 00000000..3955174c --- /dev/null +++ b/exporters/CSVExporter.py @@ -0,0 +1,61 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore +from Exporter import Exporter +from pyqtgraph.parametertree import Parameter + + +__all__ = ['CSVExporter'] + + +class CSVExporter(Exporter): + Name = "CSV from plot data" + windows = [] + def __init__(self, item): + Exporter.__init__(self, item) + self.params = Parameter(name='params', type='group', children=[ + {'name': 'separator', 'type': 'list', 'value': 'comma', 'values': ['comma', 'tab']}, + ]) + + def parameters(self): + return self.params + + def export(self, fileName=None): + + if not isinstance(self.item, pg.PlotItem): + raise Exception("Matplotlib export currently only works with plot items") + + if fileName is None: + self.fileSaveDialog(filter=["*.csv", "*.tsv"]) + return + + fd = open(fileName, 'w') + data = [] + header = [] + for c in self.item.curves: + data.append(c.getData()) + header.extend(['x', 'y']) + + if self.params['separator'] == 'comma': + sep = ',' + else: + sep = '\t' + + fd.write(sep.join(header) + '\n') + i = 0 + while True: + done = True + for d in data: + if i < len(d[0]): + fd.write('%g%s%g%s'%(d[0][i], sep, d[1][i], sep)) + done = False + else: + fd.write(' %s %s' % (sep, sep)) + fd.write('\n') + if done: + break + i += 1 + fd.close() + + + + From bdb6ff88a2412407e38bb97bd89d15a94b610855 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 12:22:43 -0400 Subject: [PATCH 052/238] Updates to IsocurveItem, added isocurve example minor updates for other examples --- examples/Arrow.py | 2 -- examples/VideoSpeedTest.py | 18 +++++++---- examples/__main__.py | 9 +++++- examples/isocurve.py | 58 +++++++++++++++++++++++++++++++++++ graphicsItems/IsocurveItem.py | 44 ++++++++++++++++++++------ 5 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 examples/isocurve.py diff --git a/examples/Arrow.py b/examples/Arrow.py index 86f5c8c7..ae118507 100755 --- a/examples/Arrow.py +++ b/examples/Arrow.py @@ -7,8 +7,6 @@ ## To place a static arrow anywhere in a scene, use ArrowItem. ## To attach other types of item to a curve, use CurvePoint. - -## Add path to library (just for examples; you do not need this) import initExample ## Add path to library (just for examples; you do not need this) import numpy as np diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index 57d8aacc..1ec28a3c 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -1,8 +1,13 @@ -#!/usr/bin/python # -*- coding: utf-8 -*- -## Add path to library (just for examples; you do not need this) -import sys, os, time -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) +""" +Tests the speed of image updates for an ImageItem and RawImageWidget. +The speed will generally depend on the type of data being shown, whether +it is being scaled and/or converted by lookup table, and whether OpenGL +is used by the view widget +""" + + +import initExample ## Add path to library (just for examples; you do not need this) from pyqtgraph.Qt import QtGui, QtCore @@ -136,6 +141,7 @@ timer.start(0) -## Start Qt event loop unless running in interactive mode. -if sys.flags.interactive != 1: +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() diff --git a/examples/__main__.py b/examples/__main__.py index b58e50ee..8511327d 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -12,9 +12,13 @@ examples = OrderedDict([ ('Basic Plotting', 'Plotting.py'), ('ImageView', 'ImageView.py'), ('ParameterTree', '../parametertree'), + ('Crosshair / Mouse interaction', 'crosshair.py'), + ('Video speed test', 'VideoSpeedTest.py'), + ('Plot speed test', 'PlotSpeedTest.py'), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), + ('IsocurveItem', 'isocurve.py'), ('ImageItem - video', 'ImageItem.py'), ('ImageItem - draw', 'Draw.py'), ('Region-of-Interest', 'ROItypes.py'), @@ -90,7 +94,10 @@ class ExampleLoader(QtGui.QMainWindow): fn = self.currentFile() if fn is None: return - os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, '"' + fn + '"') + if sys.platform.startswith('win'): + os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, '"' + fn + '"') + else: + os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn) def showFile(self): diff --git a/examples/isocurve.py b/examples/isocurve.py new file mode 100644 index 00000000..05316373 --- /dev/null +++ b/examples/isocurve.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" +Tests use of IsoCurve item displayed with image +""" + + +import initExample ## Add path to library (just for examples; you do not need this) + + +from pyqtgraph.Qt import QtGui, QtCore +import numpy as np +import pyqtgraph as pg +import scipy.ndimage as ndi + +app = QtGui.QApplication([]) + +## make pretty looping data +frames = 200 +data = np.random.normal(size=(frames,30,30), loc=0, scale=100) +data = np.concatenate([data, data], axis=0) +data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2] + +win = pg.GraphicsWindow() +vb = win.addViewBox() +img = pg.ImageItem(data[0]) +vb.addItem(img) +vb.setAspectLocked() + +## generate empty curves +curves = [] +levels = np.linspace(data.min(), data.max(), 10) +for i in range(len(levels)): + v = levels[i] + ## generate isocurve with automatic color selection + c = pg.IsocurveItem(level=v, pen=(i, len(levels)*1.5)) + c.setParentItem(img) ## make sure isocurve is always correctly displayed over image + c.setZValue(10) + curves.append(c) + +## animate! +ptr = 0 +imgLevels = (data.min(), data.max() * 2) +def update(): + global data, curves, img, ptr, imgLevels + ptr = (ptr + 1) % data.shape[0] + data[ptr] + img.setImage(data[ptr], levels=imgLevels) + for c in curves: + c.setData(data[ptr]) + +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(50) + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() diff --git a/graphicsItems/IsocurveItem.py b/graphicsItems/IsocurveItem.py index 62e582fc..eb87418a 100644 --- a/graphicsItems/IsocurveItem.py +++ b/graphicsItems/IsocurveItem.py @@ -2,7 +2,7 @@ from GraphicsObject import * import pyqtgraph.functions as fn -from pyqtgraph.Qt import QtGui +from pyqtgraph.Qt import QtGui, QtCore class IsocurveItem(GraphicsObject): @@ -13,26 +13,50 @@ class IsocurveItem(GraphicsObject): call isocurve.setParentItem(image) """ - def __init__(self, data, level, pen='w'): + def __init__(self, data=None, level=0, pen='w'): GraphicsObject.__init__(self) - - lines = fn.isocurve(data, level) - - self.path = QtGui.QPainterPath() + self.level = 0 + self.data = None + self.path = None + self.setData(data, level) self.setPen(pen) - for line in lines: - self.path.moveTo(*line[0]) - self.path.lineTo(*line[1]) - + + def setData(self, data, level=None): + if level is None: + level = self.level + self.level = level + self.data = data + self.path = None + self.prepareGeometryChange() + self.update() + + def setLevel(self, level): + self.level = level + self.path = None + self.update() + def setPen(self, *args, **kwargs): self.pen = fn.mkPen(*args, **kwargs) self.update() def boundingRect(self): + if self.path is None: + return QtCore.QRectF() return self.path.boundingRect() + def generatePath(self): + self.path = QtGui.QPainterPath() + if self.data is None: + return + lines = fn.isocurve(self.data, self.level) + for line in lines: + self.path.moveTo(*line[0]) + self.path.lineTo(*line[1]) + def paint(self, p, *args): + if self.path is None: + self.generatePath() p.setPen(self.pen) p.drawPath(self.path) \ No newline at end of file From a330d8494393f622d15f0a2859e96cfa6abdf662 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 14:45:12 -0400 Subject: [PATCH 053/238] added check for correct python version fixes for flowchart - added missing FeedbackButton.py - corrected imports --- __init__.py | 5 + examples/Flowchart.py | 8 +- flowchart/FlowchartCtrlTemplate.py | 2 +- flowchart/FlowchartCtrlTemplate.ui | 2 +- widgets/FeedbackButton.py | 160 +++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 widgets/FeedbackButton.py diff --git a/__init__.py b/__init__.py index 5c260d1a..ddfe7d8e 100644 --- a/__init__.py +++ b/__init__.py @@ -13,6 +13,11 @@ from Qt import QtGui ## we only enable it where the performance benefit is critical. ## Note this only applies to 2D graphics; 3D graphics always use OpenGL. import sys + +## check python version +if sys.version_info[0] != 2 or sys.version_info[1] != 7: + raise Exception("Pyqtgraph requires Python version 2.7 (this is %d.%d)" % (sys.version_info[0], sys.version_info[1])) + if 'linux' in sys.platform: ## linux has numerous bugs in opengl implementation useOpenGL = False elif 'darwin' in sys.platform: ## openGL greatly speeds up display on mac diff --git a/examples/Flowchart.py b/examples/Flowchart.py index 749fd3b6..2b4613f4 100644 --- a/examples/Flowchart.py +++ b/examples/Flowchart.py @@ -24,10 +24,10 @@ w = fc.widget() w.resize(400,200) w.show() -n1 = fc.createNode('Add') -n2 = fc.createNode('Subtract') -n3 = fc.createNode('Abs') -n4 = fc.createNode('Add') +n1 = fc.createNode('Add', pos=(0,-80)) +n2 = fc.createNode('Subtract', pos=(140,-10)) +n3 = fc.createNode('Abs', pos=(0, 80)) +n4 = fc.createNode('Add', pos=(140,100)) fc.connectTerminals(fc.dataIn, n1.A) fc.connectTerminals(fc.dataIn, n1.B) diff --git a/flowchart/FlowchartCtrlTemplate.py b/flowchart/FlowchartCtrlTemplate.py index 0f2ec162..f09e64ed 100644 --- a/flowchart/FlowchartCtrlTemplate.py +++ b/flowchart/FlowchartCtrlTemplate.py @@ -67,5 +67,5 @@ class Ui_Form(object): self.reloadBtn.setText(QtGui.QApplication.translate("Form", "Reload Libs", None, QtGui.QApplication.UnicodeUTF8)) self.showChartBtn.setText(QtGui.QApplication.translate("Form", "Flowchart", None, QtGui.QApplication.UnicodeUTF8)) -from FeedbackButton import FeedbackButton +from pyqtgraph.widgets.FeedbackButton import FeedbackButton from pyqtgraph.widgets.TreeWidget import TreeWidget diff --git a/flowchart/FlowchartCtrlTemplate.ui b/flowchart/FlowchartCtrlTemplate.ui index a931fb3a..610846b6 100644 --- a/flowchart/FlowchartCtrlTemplate.ui +++ b/flowchart/FlowchartCtrlTemplate.ui @@ -112,7 +112,7 @@ FeedbackButton QPushButton -
FeedbackButton
+
pyqtgraph.widgets.FeedbackButton
diff --git a/widgets/FeedbackButton.py b/widgets/FeedbackButton.py new file mode 100644 index 00000000..5112e955 --- /dev/null +++ b/widgets/FeedbackButton.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +from pyqtgraph.Qt import QtCore, QtGui +class FeedbackButton(QtGui.QPushButton): + """ + QPushButton which flashes success/failure indication for slow or asynchronous procedures. + """ + + + ### For thread-safetyness + sigCallSuccess = QtCore.Signal(object, object, object) + sigCallFailure = QtCore.Signal(object, object, object) + sigCallProcess = QtCore.Signal(object, object, object) + sigReset = QtCore.Signal() + + def __init__(self, *args): + QtGui.QPushButton.__init__(self, *args) + self.origStyle = None + self.origText = self.text() + self.origStyle = self.styleSheet() + self.origTip = self.toolTip() + self.limitedTime = True + + + #self.textTimer = QtCore.QTimer() + #self.tipTimer = QtCore.QTimer() + #self.textTimer.timeout.connect(self.setText) + #self.tipTimer.timeout.connect(self.setToolTip) + + self.sigCallSuccess.connect(self.success) + self.sigCallFailure.connect(self.failure) + self.sigCallProcess.connect(self.processing) + self.sigReset.connect(self.reset) + + + def feedback(self, success, message=None, tip="", limitedTime=True): + """Calls success() or failure(). If you want the message to be displayed until the user takes an action, set limitedTime to False. Then call self.reset() after the desired action.Threadsafe.""" + if success: + self.success(message, tip, limitedTime=limitedTime) + else: + self.failure(message, tip, limitedTime=limitedTime) + + def success(self, message=None, tip="", limitedTime=True): + """Displays specified message on button and flashes button green to let user know action was successful. If you want the success to be displayed until the user takes an action, set limitedTime to False. Then call self.reset() after the desired action. Threadsafe.""" + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + self.setEnabled(True) + #print "success" + self.startBlink("#0F0", message, tip, limitedTime=limitedTime) + else: + self.sigCallSuccess.emit(message, tip, limitedTime) + + def failure(self, message=None, tip="", limitedTime=True): + """Displays specified message on button and flashes button red to let user know there was an error. If you want the error to be displayed until the user takes an action, set limitedTime to False. Then call self.reset() after the desired action. Threadsafe. """ + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + self.setEnabled(True) + #print "fail" + self.startBlink("#F00", message, tip, limitedTime=limitedTime) + else: + self.sigCallFailure.emit(message, tip, limitedTime) + + def processing(self, message="Processing..", tip="", processEvents=True): + """Displays specified message on button to let user know the action is in progress. Threadsafe. """ + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + self.setEnabled(False) + self.setText(message, temporary=True) + self.setToolTip(tip, temporary=True) + if processEvents: + QtGui.QApplication.processEvents() + else: + self.sigCallProcess.emit(message, tip, processEvents) + + + def reset(self): + """Resets the button to its original text and style. Threadsafe.""" + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if isGuiThread: + self.limitedTime = True + self.setText() + self.setToolTip() + self.setStyleSheet() + else: + self.sigReset.emit() + + def startBlink(self, color, message=None, tip="", limitedTime=True): + #if self.origStyle is None: + #self.origStyle = self.styleSheet() + #self.origText = self.text() + self.setFixedHeight(self.height()) + + if message is not None: + self.setText(message, temporary=True) + self.setToolTip(tip, temporary=True) + self.count = 0 + #self.indStyle = "QPushButton {border: 2px solid %s; border-radius: 5px}" % color + self.indStyle = "QPushButton {background-color: %s}" % color + self.limitedTime = limitedTime + self.borderOn() + if limitedTime: + QtCore.QTimer.singleShot(2000, self.setText) + QtCore.QTimer.singleShot(10000, self.setToolTip) + + def borderOn(self): + self.setStyleSheet(self.indStyle, temporary=True) + if self.limitedTime or self.count <=2: + QtCore.QTimer.singleShot(100, self.borderOff) + + + def borderOff(self): + self.setStyleSheet() + self.count += 1 + if self.count >= 2: + if self.limitedTime: + return + QtCore.QTimer.singleShot(30, self.borderOn) + + + def setText(self, text=None, temporary=False): + if text is None: + text = self.origText + #print text + QtGui.QPushButton.setText(self, text) + if not temporary: + self.origText = text + + def setToolTip(self, text=None, temporary=False): + if text is None: + text = self.origTip + QtGui.QPushButton.setToolTip(self, text) + if not temporary: + self.origTip = text + + def setStyleSheet(self, style=None, temporary=False): + if style is None: + style = self.origStyle + QtGui.QPushButton.setStyleSheet(self, style) + if not temporary: + self.origStyle = style + + +if __name__ == '__main__': + import time + app = QtGui.QApplication([]) + win = QtGui.QMainWindow() + btn = FeedbackButton("Button") + fail = True + def click(): + btn.processing("Hold on..") + time.sleep(2.0) + + global fail + fail = not fail + if fail: + btn.failure(message="FAIL.", tip="There was a failure. Get over it.") + else: + btn.success(message="Bueno!") + btn.clicked.connect(click) + win.setCentralWidget(btn) + win.show() \ No newline at end of file From 5a9bcc359125e8b796929553575143dbc60627af Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 14:49:09 -0400 Subject: [PATCH 054/238] Added missing reload.py --- reload.py | 506 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 reload.py diff --git a/reload.py b/reload.py new file mode 100644 index 00000000..e1a8179d --- /dev/null +++ b/reload.py @@ -0,0 +1,506 @@ +# -*- coding: utf-8 -*- +""" +Magic Reload Library +Luke Campagnola 2010 + +Python reload function that actually works (the way you expect it to) + - No re-importing necessary + - Modules can be reloaded in any order + - Replaces functions and methods with their updated code + - Changes instances to use updated classes + - Automatically decides which modules to update by comparing file modification times + +Does NOT: + - re-initialize exting instances, even if __init__ changes + - update references to any module-level objects + ie, this does not reload correctly: + from module import someObject + print someObject + ..but you can use this instead: (this works even for the builtin reload) + import module + print module.someObject +""" + + +import inspect, os, sys, __builtin__, gc, traceback +from debug import printExc + +def reloadAll(prefix=None, debug=False): + """Automatically reload everything whose __file__ begins with prefix. + - Skips reload if the file has not been updated (if .pyc is newer than .py) + - if prefix is None, checks all loaded modules + """ + for modName, mod in sys.modules.items(): ## don't use iteritems; size may change during reload + if not inspect.ismodule(mod): + continue + if modName == '__main__': + continue + + ## Ignore if the file name does not start with prefix + if not hasattr(mod, '__file__') or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']: + continue + if prefix is not None and mod.__file__[:len(prefix)] != prefix: + continue + + ## ignore if the .pyc is newer than the .py (or if there is no pyc or py) + py = os.path.splitext(mod.__file__)[0] + '.py' + pyc = py + 'c' + if os.path.isfile(pyc) and os.path.isfile(py) and os.stat(pyc).st_mtime >= os.stat(py).st_mtime: + #if debug: + #print "Ignoring module %s; unchanged" % str(mod) + continue + + try: + reload(mod, debug=debug) + except: + printExc("Error while reloading module %s, skipping\n" % mod) + + +def reload(module, debug=False, lists=False, dicts=False): + """Replacement for the builtin reload function: + - Reloads the module as usual + - Updates all old functions and class methods to use the new code + - Updates all instances of each modified class to use the new class + - Can update lists and dicts, but this is disabled by default + - Requires that class and function names have not changed + """ + if debug: + print "Reloading", module + + ## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison + oldDict = module.__dict__.copy() + __builtin__.reload(module) + newDict = module.__dict__ + + ## Allow modules access to the old dictionary after they reload + if hasattr(module, '__reload__'): + module.__reload__(oldDict) + + ## compare old and new elements from each dict; update where appropriate + for k in oldDict: + old = oldDict[k] + new = newDict.get(k, None) + if old is new or new is None: + continue + + if inspect.isclass(old): + if debug: + print " Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new)) + updateClass(old, new, debug) + + elif inspect.isfunction(old): + depth = updateFunction(old, new, debug) + if debug: + extra = "" + if depth > 0: + extra = " (and %d previous versions)" % depth + print " Updating function %s.%s%s" % (module.__name__, k, extra) + elif lists and isinstance(old, list): + l = old.len() + old.extend(new) + for i in range(l): + old.pop(0) + elif dicts and isinstance(old, dict): + old.update(new) + for k in old: + if k not in new: + del old[k] + + + +## For functions: +## 1) update the code and defaults to new versions. +## 2) keep a reference to the previous version so ALL versions get updated for every reload +def updateFunction(old, new, debug, depth=0, visited=None): + #if debug and depth > 0: + #print " -> also updating previous version", old, " -> ", new + + old.__code__ = new.__code__ + old.__defaults__ = new.__defaults__ + + if visited is None: + visited = [] + if old in visited: + return + visited.append(old) + + ## finally, update any previous versions still hanging around.. + if hasattr(old, '__previous_reload_version__'): + maxDepth = updateFunction(old.__previous_reload_version__, new, debug, depth=depth+1, visited=visited) + else: + maxDepth = depth + + ## We need to keep a pointer to the previous version so we remember to update BOTH + ## when the next reload comes around. + if depth == 0: + new.__previous_reload_version__ = old + return maxDepth + + + +## For classes: +## 1) find all instances of the old class and set instance.__class__ to the new class +## 2) update all old class methods to use code from the new class methods +def updateClass(old, new, debug): + + ## Track town all instances and subclasses of old + refs = gc.get_referrers(old) + for ref in refs: + try: + if isinstance(ref, old) and ref.__class__ is old: + ref.__class__ = new + if debug: + print " Changed class for", safeStr(ref) + elif inspect.isclass(ref) and issubclass(ref, old) and old in ref.__bases__: + ind = ref.__bases__.index(old) + + ## Does not work: + #ref.__bases__ = ref.__bases__[:ind] + (new,) + ref.__bases__[ind+1:] + ## reason: Even though we change the code on methods, they remain bound + ## to their old classes (changing im_class is not allowed). Instead, + ## we have to update the __bases__ such that this class will be allowed + ## as an argument to older methods. + + ## This seems to work. Is there any reason not to? + ## Note that every time we reload, the class hierarchy becomes more complex. + ## (and I presume this may slow things down?) + ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] + if debug: + print " Changed superclass for", safeStr(ref) + #else: + #if debug: + #print " Ignoring reference", type(ref) + except: + print "Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new)) + raise + + ## update all class methods to use new code. + ## Generally this is not needed since instances already know about the new class, + ## but it fixes a few specific cases (pyqt signals, for one) + for attr in dir(old): + oa = getattr(old, attr) + if inspect.ismethod(oa): + try: + na = getattr(new, attr) + except AttributeError: + if debug: + print " Skipping method update for %s; new class does not have this attribute" % attr + continue + + if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.im_func is not na.im_func: + depth = updateFunction(oa.im_func, na.im_func, debug) + #oa.im_class = new ## bind old method to new class ## not allowed + if debug: + extra = "" + if depth > 0: + extra = " (and %d previous versions)" % depth + print " Updating method %s%s" % (attr, extra) + + ## And copy in new functions that didn't exist previously + for attr in dir(new): + if not hasattr(old, attr): + if debug: + print " Adding missing attribute", attr + setattr(old, attr, getattr(new, attr)) + + ## finally, update any previous versions still hanging around.. + if hasattr(old, '__previous_reload_version__'): + updateClass(old.__previous_reload_version__, new, debug) + + +## It is possible to build classes for which str(obj) just causes an exception. +## Avoid thusly: +def safeStr(obj): + try: + s = str(obj) + except: + try: + s = repr(obj) + except: + s = "" % (safeStr(type(obj)), id(obj)) + return s + + + + + +## Tests: +# write modules to disk, import, then re-write and run again +if __name__ == '__main__': + doQtTest = True + try: + from PyQt4 import QtCore + if not hasattr(QtCore, 'Signal'): + QtCore.Signal = QtCore.pyqtSignal + #app = QtGui.QApplication([]) + class Btn(QtCore.QObject): + sig = QtCore.Signal() + def emit(self): + self.sig.emit() + btn = Btn() + except: + raise + print "Error; skipping Qt tests" + doQtTest = False + + + + import os + if not os.path.isdir('test1'): + os.mkdir('test1') + open('test1/__init__.py', 'w') + modFile1 = "test1/test1.py" + modCode1 = """ +import sys +class A(object): + def __init__(self, msg): + object.__init__(self) + self.msg = msg + def fn(self, pfx = ""): + print pfx+"A class:", self.__class__, id(self.__class__) + print pfx+" %%s: %d" %% self.msg + +class B(A): + def fn(self, pfx=""): + print pfx+"B class:", self.__class__, id(self.__class__) + print pfx+" %%s: %d" %% self.msg + print pfx+" calling superclass.. (%%s)" %% id(A) + A.fn(self, " ") +""" + + modFile2 = "test2.py" + modCode2 = """ +from test1.test1 import A +from test1.test1 import B + +a1 = A("ax1") +b1 = B("bx1") +class C(A): + def __init__(self, msg): + #print "| C init:" + #print "| C.__bases__ = ", map(id, C.__bases__) + #print "| A:", id(A) + #print "| A.__init__ = ", id(A.__init__.im_func), id(A.__init__.im_func.__code__), id(A.__init__.im_class) + A.__init__(self, msg + "(init from C)") + +def fn(): + print "fn: %s" +""" + + open(modFile1, 'w').write(modCode1%(1,1)) + open(modFile2, 'w').write(modCode2%"message 1") + import test1.test1 as test1 + import test2 + print "Test 1 originals:" + A1 = test1.A + B1 = test1.B + a1 = test1.A("a1") + b1 = test1.B("b1") + a1.fn() + b1.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + + from test2 import fn, C + + if doQtTest: + print "Button test before:" + btn.sig.connect(fn) + btn.sig.connect(a1.fn) + btn.emit() + #btn.sig.emit() + print "" + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + + print "Test2 before reload:" + + fn() + oldfn = fn + test2.a1.fn() + test2.b1.fn() + c1 = test2.C('c1') + c1.fn() + + os.remove(modFile1+'c') + open(modFile1, 'w').write(modCode1%(2,2)) + print "\n----RELOAD test1-----\n" + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + + print "Subclass test:" + c2 = test2.C('c2') + c2.fn() + + + os.remove(modFile2+'c') + open(modFile2, 'w').write(modCode2%"message 2") + print "\n----RELOAD test2-----\n" + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + if doQtTest: + print "Button test after:" + btn.emit() + #btn.sig.emit() + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + print "Test2 after reload:" + fn() + test2.a1.fn() + test2.b1.fn() + + print "\n==> Test 1 Old instances:" + a1.fn() + b1.fn() + c1.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + print "\n==> Test 1 New instances:" + a2 = test1.A("a2") + b2 = test1.B("b2") + a2.fn() + b2.fn() + c2 = test2.C('c2') + c2.fn() + #print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + + + + os.remove(modFile1+'c') + os.remove(modFile2+'c') + open(modFile1, 'w').write(modCode1%(3,3)) + open(modFile2, 'w').write(modCode2%"message 3") + + print "\n----RELOAD-----\n" + reloadAll(os.path.abspath(__file__)[:10], debug=True) + + if doQtTest: + print "Button test after:" + btn.emit() + #btn.sig.emit() + + #print "a1.fn referrers:", sys.getrefcount(a1.fn.im_func), gc.get_referrers(a1.fn.im_func) + + print "Test2 after reload:" + fn() + test2.a1.fn() + test2.b1.fn() + + print "\n==> Test 1 Old instances:" + a1.fn() + b1.fn() + print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + print "\n==> Test 1 New instances:" + a2 = test1.A("a2") + b2 = test1.B("b2") + a2.fn() + b2.fn() + print "function IDs a1 bound method: %d a1 func: %d a1 class: %d b1 func: %d b1 class: %d" % (id(a1.fn), id(a1.fn.im_func), id(a1.fn.im_class), id(b1.fn.im_func), id(b1.fn.im_class)) + + + os.remove(modFile1) + os.remove(modFile2) + os.remove(modFile1+'c') + os.remove(modFile2+'c') + os.system('rm -r test1') + + + + + + + + +# +# Failure graveyard ahead: +# + + +"""Reload Importer: +Hooks into import system to +1) keep a record of module dependencies as they are imported +2) make sure modules are always reloaded in correct order +3) update old classes and functions to use reloaded code""" + +#import imp, sys + +## python's import hook mechanism doesn't work since we need to be +## informed every time there is an import statement, not just for new imports +#class ReloadImporter: + #def __init__(self): + #self.depth = 0 + + #def find_module(self, name, path): + #print " "*self.depth + "find: ", name, path + ##if name == 'PyQt4' and path is None: + ##print "PyQt4 -> PySide" + ##self.modData = imp.find_module('PySide') + ##return self + ##return None ## return none to allow the import to proceed normally; return self to intercept with load_module + #self.modData = imp.find_module(name, path) + #self.depth += 1 + ##sys.path_importer_cache = {} + #return self + + #def load_module(self, name): + #mod = imp.load_module(name, *self.modData) + #self.depth -= 1 + #print " "*self.depth + "load: ", name + #return mod + +#def pathHook(path): + #print "path hook:", path + #raise ImportError +#sys.path_hooks.append(pathHook) + +#sys.meta_path.append(ReloadImporter()) + + +### replace __import__ with a wrapper that tracks module dependencies +#modDeps = {} +#reloadModule = None +#origImport = __builtins__.__import__ +#def _import(name, globals=None, locals=None, fromlist=None, level=-1, stack=[]): + ### Note that stack behaves as a static variable. + ##print " "*len(importStack) + "import %s" % args[0] + #stack.append(set()) + #mod = origImport(name, globals, locals, fromlist, level) + #deps = stack.pop() + #if len(stack) > 0: + #stack[-1].add(mod) + #elif reloadModule is not None: ## If this is the top level import AND we're inside a module reload + #modDeps[reloadModule].add(mod) + + #if mod in modDeps: + #modDeps[mod] |= deps + #else: + #modDeps[mod] = deps + + + #return mod + +#__builtins__.__import__ = _import + +### replace +#origReload = __builtins__.reload +#def _reload(mod): + #reloadModule = mod + #ret = origReload(mod) + #reloadModule = None + #return ret +#__builtins__.reload = _reload + + +#def reload(mod, visited=None): + #if visited is None: + #visited = set() + #if mod in visited: + #return + #visited.add(mod) + #for dep in modDeps.get(mod, []): + #reload(dep, visited) + #__builtins__.reload(mod) From 09995e0d1221c59f479bb9f6ca6d03251cf02b56 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 20:28:48 -0400 Subject: [PATCH 055/238] Added a few more files to get flowcharts working --- configfile.py | 194 ++++++++++++++++++++++++++++++++++ flowchart/Flowchart.py | 2 +- flowchart/library/Filters.py | 45 -------- flowchart/library/__init__.py | 2 +- units.py | 64 +++++++++++ 5 files changed, 260 insertions(+), 47 deletions(-) create mode 100644 configfile.py create mode 100644 units.py diff --git a/configfile.py b/configfile.py new file mode 100644 index 00000000..c851edb1 --- /dev/null +++ b/configfile.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +""" +configfile.py - Human-readable text configuration file library +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +Used for reading and writing dictionary objects to a python-like configuration +file format. Data structures may be nested and contain any data type as long +as it can be converted to/from a string using repr and eval. +""" + +import re, os, sys +from collections import OrderedDict +GLOBAL_PATH = None # so not thread safe. +import units + +class ParseError(Exception): + def __init__(self, message, lineNum, line, fileName=None): + self.lineNum = lineNum + self.line = line + #self.message = message + self.fileName = fileName + Exception.__init__(self, message) + + def __str__(self): + if self.fileName is None: + msg = "Error parsing string at line %d:\n" % self.lineNum + else: + msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum) + msg += "%s\n%s" % (self.line, self.message) + return msg + #raise Exception() + + +def writeConfigFile(data, fname): + s = genString(data) + fd = open(fname, 'w') + fd.write(s) + fd.close() + +def readConfigFile(fname): + #cwd = os.getcwd() + global GLOBAL_PATH + if GLOBAL_PATH is not None: + fname2 = os.path.join(GLOBAL_PATH, fname) + if os.path.exists(fname2): + fname = fname2 + + GLOBAL_PATH = os.path.dirname(os.path.abspath(fname)) + + try: + #os.chdir(newDir) ## bad. + fd = open(fname) + s = unicode(fd.read(), 'UTF-8') + fd.close() + s = s.replace("\r\n", "\n") + s = s.replace("\r", "\n") + data = parseString(s)[1] + except ParseError: + sys.exc_info()[1].fileName = fname + raise + except: + print "Error while reading config file %s:"% fname + raise + #finally: + #os.chdir(cwd) + return data + +def appendConfigFile(data, fname): + s = genString(data) + fd = open(fname, 'a') + fd.write(s) + fd.close() + + +def genString(data, indent=''): + s = '' + for k in data: + sk = str(k) + if len(sk) == 0: + print data + raise Exception('blank dict keys not allowed (see data above)') + if sk[0] == ' ' or ':' in sk: + print data + raise Exception('dict keys must not contain ":" or start with spaces [offending key is "%s"]' % sk) + if isinstance(data[k], dict): + s += indent + sk + ':\n' + s += genString(data[k], indent + ' ') + else: + s += indent + sk + ': ' + repr(data[k]) + '\n' + return s + +def parseString(lines, start=0): + + data = OrderedDict() + if isinstance(lines, basestring): + lines = lines.split('\n') + + indent = measureIndent(lines[start]) + ln = start - 1 + + try: + while True: + ln += 1 + #print ln + if ln >= len(lines): + break + + l = lines[ln] + + ## Skip blank lines or lines starting with # + if re.match(r'\s*#', l) or not re.search(r'\S', l): + continue + + ## Measure line indentation, make sure it is correct for this level + lineInd = measureIndent(l) + if lineInd < indent: + ln -= 1 + break + if lineInd > indent: + #print lineInd, indent + raise ParseError('Indentation is incorrect. Expected %d, got %d' % (indent, lineInd), ln+1, l) + + + if ':' not in l: + raise ParseError('Missing colon', ln+1, l) + + (k, p, v) = l.partition(':') + k = k.strip() + v = v.strip() + + ## set up local variables to use for eval + local = units.allUnits.copy() + local['OrderedDict'] = OrderedDict + local['readConfigFile'] = readConfigFile + if len(k) < 1: + raise ParseError('Missing name preceding colon', ln+1, l) + if k[0] == '(' and k[-1] == ')': ## If the key looks like a tuple, try evaluating it. + try: + k1 = eval(k, local) + if type(k1) is tuple: + k = k1 + except: + pass + if re.search(r'\S', v) and v[0] != '#': ## eval the value + try: + val = eval(v, local) + except: + ex = sys.exc_info()[1] + raise ParseError("Error evaluating expression '%s': [%s: %s]" % (v, ex.__class__.__name__, str(ex)), (ln+1), l) + else: + if ln+1 >= len(lines) or measureIndent(lines[ln+1]) <= indent: + #print "blank dict" + val = {} + else: + #print "Going deeper..", ln+1 + (ln, val) = parseString(lines, start=ln+1) + data[k] = val + #print k, repr(val) + except ParseError: + raise + except: + ex = sys.exc_info()[1] + raise ParseError("%s: %s" % (ex.__class__.__name__, str(ex)), ln+1, l) + #print "Returning shallower..", ln+1 + return (ln, data) + +def measureIndent(s): + n = 0 + while n < len(s) and s[n] == ' ': + n += 1 + return n + + + +if __name__ == '__main__': + import tempfile + fn = tempfile.mktemp() + tf = open(fn, 'w') + cf = """ +key: 'value' +key2: + key21: 'value' + key22: [1,2,3] + key23: 234 #comment + """ + tf.write(cf) + tf.close() + print "=== Test:===" + print cf + print "============" + data = readConfigFile(fn) + print data + os.remove(fn) \ No newline at end of file diff --git a/flowchart/Flowchart.py b/flowchart/Flowchart.py index 3e854d54..e253741f 100644 --- a/flowchart/Flowchart.py +++ b/flowchart/Flowchart.py @@ -14,7 +14,7 @@ from Terminal import Terminal from numpy import ndarray import library from pyqtgraph.debug import printExc -import configfile +import pyqtgraph.configfile as configfile import pyqtgraph.dockarea as dockarea import pyqtgraph as pg import FlowchartGraphicsView diff --git a/flowchart/library/Filters.py b/flowchart/library/Filters.py index a88ea40e..6badff83 100644 --- a/flowchart/library/Filters.py +++ b/flowchart/library/Filters.py @@ -197,49 +197,4 @@ class HistogramDetrend(CtrlNode): return functions.histogramDetrend(data, window=ws, bins=bn) -class ExpDeconvolve(CtrlNode): - """Exponential deconvolution filter.""" - nodeName = 'ExpDeconvolve' - uiTemplate = [ - ('tau', 'spin', {'value': 10e-3, 'step': 1, 'minStep': 100e-6, 'dec': True, 'range': [0.0, None], 'suffix': 's', 'siPrefix': True}) - ] - def processData(self, data): - tau = self.ctrls['tau'].value() - return functions.expDeconvolve(data, tau) - #dt = 1 - #if isinstance(data, MetaArray): - #dt = data.xvals(0)[1] - data.xvals(0)[0] - #d = data[:-1] + (self.ctrls['tau'].value() / dt) * (data[1:] - data[:-1]) - #if isinstance(data, MetaArray): - #info = data.infoCopy() - #if 'values' in info[0]: - #info[0]['values'] = info[0]['values'][:-1] - #return MetaArray(d, info=info) - #else: - #return d - -class ExpReconvolve(CtrlNode): - """Exponential reconvolution filter. Only works with MetaArrays that were previously deconvolved.""" - nodeName = 'ExpReconvolve' - #uiTemplate = [ - #('tau', 'spin', {'value': 10e-3, 'step': 1, 'minStep': 100e-6, 'dec': True, 'range': [0.0, None], 'suffix': 's', 'siPrefix': True}) - #] - - def processData(self, data): - return functions.expReconvolve(data) - -class Tauiness(CtrlNode): - """Sliding-window exponential fit""" - nodeName = 'Tauiness' - uiTemplate = [ - ('window', 'intSpin', {'value': 100, 'min': 3, 'max': 1000000}), - ('skip', 'intSpin', {'value': 10, 'min': 0, 'max': 10000000}) - ] - - def processData(self, data): - return functions.tauiness(data, self.ctrls['window'].value(), self.ctrls['skip'].value()) - - - - \ No newline at end of file diff --git a/flowchart/library/__init__.py b/flowchart/library/__init__.py index 58b5b810..1efc4ea1 100644 --- a/flowchart/library/__init__.py +++ b/flowchart/library/__init__.py @@ -3,7 +3,7 @@ from collections import OrderedDict import os, types from pyqtgraph.debug import printExc from ..Node import Node -import reload +import pyqtgraph.reload as reload NODE_LIST = OrderedDict() ## maps name:class for all registered Node subclasses diff --git a/units.py b/units.py new file mode 100644 index 00000000..6b7f3099 --- /dev/null +++ b/units.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +## Very simple unit support: +## - creates variable names like 'mV' and 'kHz' +## - the value assigned to the variable corresponds to the scale prefix +## (mV = 0.001) +## - the actual units are purely cosmetic for making code clearer: +## +## x = 20*pA is identical to x = 20*1e-12 + +## No unicode variable names (μ,Ω) allowed until python 3 + +SI_PREFIXES = 'yzafpnum kMGTPEZY' +UNITS = 'm,s,g,W,J,V,A,F,T,Hz,Ohm,S,N,C,px,b,B'.split(',') +allUnits = {} + +def addUnit(p, n): + g = globals() + v = 1000**n + for u in UNITS: + g[p+u] = v + allUnits[p+u] = v + +for p in SI_PREFIXES: + if p == ' ': + p = '' + n = 0 + elif p == 'u': + n = -2 + else: + n = SI_PREFIXES.index(p) - 8 + + addUnit(p, n) + +cm = 0.01 + + + + + + +def evalUnits(unitStr): + """ + Evaluate a unit string into ([numerators,...], [denominators,...]) + Examples: + N m/s^2 => ([N, m], [s, s]) + A*s / V => ([A, s], [V,]) + """ + pass + +def formatUnits(units): + """ + Format a unit specification ([numerators,...], [denominators,...]) + into a string (this is the inverse of evalUnits) + """ + pass + +def simplify(units): + """ + Cancel units that appear in both numerator and denominator, then attempt to replace + groups of units with single units where possible (ie, J/s => W) + """ + pass + + \ No newline at end of file From 20c40a70d5e0b0e20209972eb045436b1f800ab8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 21:03:31 -0400 Subject: [PATCH 056/238] Added in flowchart's filter functions --- examples/Flowchart.py | 95 +++++++++------ flowchart/library/functions.py | 216 +++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 34 deletions(-) create mode 100644 flowchart/library/functions.py diff --git a/examples/Flowchart.py b/examples/Flowchart.py index 2b4613f4..a80d9e74 100644 --- a/examples/Flowchart.py +++ b/examples/Flowchart.py @@ -1,61 +1,88 @@ # -*- coding: utf-8 -*- -import sys, os - -## Make sure pyqtgraph is importable -p = os.path.dirname(os.path.abspath(__file__)) -p = os.path.join(p, '..', '..') -sys.path.insert(0, p) +import initExample ## Add path to library (just for examples; you do not need this) from pyqtgraph.flowchart import Flowchart from pyqtgraph.Qt import QtGui - -#import pyqtgraph.flowchart as f +import pyqtgraph as pg +import numpy as np app = QtGui.QApplication([]) -#TETRACYCLINE = True + +win = QtGui.QMainWindow() +cw = QtGui.QWidget() +win.setCentralWidget(cw) +layout = QtGui.QGridLayout() +cw.setLayout(layout) fc = Flowchart(terminals={ 'dataIn': {'io': 'in'}, 'dataOut': {'io': 'out'} }) w = fc.widget() -w.resize(400,200) -w.show() -n1 = fc.createNode('Add', pos=(0,-80)) -n2 = fc.createNode('Subtract', pos=(140,-10)) -n3 = fc.createNode('Abs', pos=(0, 80)) -n4 = fc.createNode('Add', pos=(140,100)) +layout.addWidget(fc.widget(), 0, 0, 2, 1) -fc.connectTerminals(fc.dataIn, n1.A) -fc.connectTerminals(fc.dataIn, n1.B) -fc.connectTerminals(fc.dataIn, n2.A) -fc.connectTerminals(n1.Out, n4.A) -fc.connectTerminals(n1.Out, n2.B) -fc.connectTerminals(n2.Out, n3.In) -fc.connectTerminals(n3.Out, n4.B) -fc.connectTerminals(n4.Out, fc.dataOut) +pw1 = pg.PlotWidget() +pw2 = pg.PlotWidget() +layout.addWidget(pw1, 0, 1) +layout.addWidget(pw2, 1, 1) + +win.show() -def process(**kargs): - return fc.process(**kargs) +data = np.random.normal(size=1000) +data[200:300] += 1 +data += np.sin(np.linspace(0, 100, 1000)) + +fc.setInput(dataIn=data) + +pw1Node = fc.createNode('PlotWidget', pos=(0, -150)) +pw1Node.setPlot(pw1) + +pw2Node = fc.createNode('PlotWidget', pos=(150, -150)) +pw2Node.setPlot(pw2) + +fNode = fc.createNode('GaussianFilter', pos=(0, 0)) +fc.connectTerminals(fc.dataIn, fNode.In) +fc.connectTerminals(fc.dataIn, pw1Node.In) +fc.connectTerminals(fNode.Out, pw2Node.In) +fc.connectTerminals(fNode.Out, fc.dataOut) + + +#n1 = fc.createNode('Add', pos=(0,-80)) +#n2 = fc.createNode('Subtract', pos=(140,-10)) +#n3 = fc.createNode('Abs', pos=(0, 80)) +#n4 = fc.createNode('Add', pos=(140,100)) + +#fc.connectTerminals(fc.dataIn, n1.A) +#fc.connectTerminals(fc.dataIn, n1.B) +#fc.connectTerminals(fc.dataIn, n2.A) +#fc.connectTerminals(n1.Out, n4.A) +#fc.connectTerminals(n1.Out, n2.B) +#fc.connectTerminals(n2.Out, n3.In) +#fc.connectTerminals(n3.Out, n4.B) +#fc.connectTerminals(n4.Out, fc.dataOut) + + +#def process(**kargs): + #return fc.process(**kargs) -print process(dataIn=7) +#print process(dataIn=7) -fc.setInput(dataIn=3) +#fc.setInput(dataIn=3) -s = fc.saveState() -fc.clear() +#s = fc.saveState() +#fc.clear() -fc.restoreState(s) +#fc.restoreState(s) -fc.setInput(dataIn=3) +#fc.setInput(dataIn=3) -#f.NodeMod.TETRACYCLINE = False -if sys.flags.interactive == 0: +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): app.exec_() - diff --git a/flowchart/library/functions.py b/flowchart/library/functions.py new file mode 100644 index 00000000..66709415 --- /dev/null +++ b/flowchart/library/functions.py @@ -0,0 +1,216 @@ +def downsample(data, n, axis=0, xvals='subsample'): + """Downsample by averaging points together across axis. + If multiple axes are specified, runs once per axis. + If a metaArray is given, then the axis values can be either subsampled + or downsampled to match. + """ + ma = None + if isinstance(data, MetaArray): + ma = data + data = data.view(ndarray) + + + if hasattr(axis, '__len__'): + if not hasattr(n, '__len__'): + n = [n]*len(axis) + for i in range(len(axis)): + data = downsample(data, n[i], axis[i]) + return data + + nPts = int(data.shape[axis] / n) + s = list(data.shape) + s[axis] = nPts + s.insert(axis+1, n) + sl = [slice(None)] * data.ndim + sl[axis] = slice(0, nPts*n) + d1 = data[tuple(sl)] + #print d1.shape, s + d1.shape = tuple(s) + d2 = d1.mean(axis+1) + + if ma is None: + return d2 + else: + info = ma.infoCopy() + if 'values' in info[axis]: + if xvals == 'subsample': + info[axis]['values'] = info[axis]['values'][::n][:nPts] + elif xvals == 'downsample': + info[axis]['values'] = downsample(info[axis]['values'], n) + return MetaArray(d2, info=info) + + +def applyFilter(data, b, a, padding=100, bidir=True): + """Apply a linear filter with coefficients a, b. Optionally pad the data before filtering + and/or run the filter in both directions.""" + d1 = data.view(ndarray) + + if padding > 0: + d1 = numpy.hstack([d1[:padding], d1, d1[-padding:]]) + + if bidir: + d1 = scipy.signal.lfilter(b, a, scipy.signal.lfilter(b, a, d1)[::-1])[::-1] + else: + d1 = scipy.signal.lfilter(b, a, d1) + + if padding > 0: + d1 = d1[padding:-padding] + + if isinstance(data, MetaArray): + return MetaArray(d1, info=data.infoCopy()) + else: + return d1 + +def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True): + """return data passed through bessel filter""" + if dt is None: + try: + tvals = data.xvals('Time') + dt = (tvals[-1]-tvals[0]) / (len(tvals)-1) + except: + raise Exception('Must specify dt for this data.') + + b,a = scipy.signal.bessel(order, cutoff * dt, btype=btype) + + return applyFilter(data, b, a, bidir=bidir) + #base = data.mean() + #d1 = scipy.signal.lfilter(b, a, data.view(ndarray)-base) + base + #if isinstance(data, MetaArray): + #return MetaArray(d1, info=data.infoCopy()) + #return d1 + +def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True): + """return data passed through bessel filter""" + if dt is None: + try: + tvals = data.xvals('Time') + dt = (tvals[-1]-tvals[0]) / (len(tvals)-1) + except: + raise Exception('Must specify dt for this data.') + + if wStop is None: + wStop = wPass * 2.0 + ord, Wn = scipy.signal.buttord(wPass*dt*2., wStop*dt*2., gPass, gStop) + #print "butterworth ord %f Wn %f c %f sc %f" % (ord, Wn, cutoff, stopCutoff) + b,a = scipy.signal.butter(ord, Wn, btype=btype) + + return applyFilter(data, b, a, bidir=bidir) + + +def rollingSum(data, n): + d1 = data.copy() + d1[1:] += d1[:-1] # integrate + d2 = np.empty(len(d1) - n + 1, dtype=data.dtype) + d2[0] = d1[n-1] # copy first point + d2[1:] = d1[n:] - d1[:-n] # subtract + return d2 + + +def mode(data, bins=None): + """Returns location max value from histogram.""" + if bins is None: + bins = int(len(data)/10.) + if bins < 2: + bins = 2 + y, x = np.histogram(data, bins=bins) + ind = np.argmax(y) + mode = 0.5 * (x[ind] + x[ind+1]) + return mode + +def modeFilter(data, window=500, step=None, bins=None): + """Filter based on histogram-based mode function""" + d1 = data.view(np.ndarray) + vals = [] + l2 = int(window/2.) + if step is None: + step = l2 + i = 0 + while True: + if i > len(data)-step: + break + vals.append(mode(d1[i:i+window], bins)) + i += step + + chunks = [np.linspace(vals[0], vals[0], l2)] + for i in range(len(vals)-1): + chunks.append(np.linspace(vals[i], vals[i+1], step)) + remain = len(data) - step*(len(vals)-1) - l2 + chunks.append(np.linspace(vals[-1], vals[-1], remain)) + d2 = np.hstack(chunks) + + if isinstance(data, MetaArray): + return MetaArray(d2, info=data.infoCopy()) + return d2 + +def denoise(data, radius=2, threshold=4): + """Very simple noise removal function. Compares a point to surrounding points, + replaces with nearby values if the difference is too large.""" + + + r2 = radius * 2 + d1 = data.view(ndarray) + d2 = data[radius:] - data[:-radius] #a derivative + #d3 = data[r2:] - data[:-r2] + #d4 = d2 - d3 + stdev = d2.std() + #print "denoise: stdev of derivative:", stdev + mask1 = d2 > stdev*threshold #where derivative is large and positive + mask2 = d2 < -stdev*threshold #where derivative is large and negative + maskpos = mask1[:-radius] * mask2[radius:] #both need to be true + maskneg = mask1[radius:] * mask2[:-radius] + mask = maskpos + maskneg + d5 = np.where(mask, d1[:-r2], d1[radius:-radius]) #where both are true replace the value with the value from 2 points before + d6 = np.empty(d1.shape, dtype=d1.dtype) #add points back to the ends + d6[radius:-radius] = d5 + d6[:radius] = d1[:radius] + d6[-radius:] = d1[-radius:] + + if isinstance(data, MetaArray): + return MetaArray(d6, info=data.infoCopy()) + return d6 + +def adaptiveDetrend(data, x=None, threshold=3.0): + """Return the signal with baseline removed. Discards outliers from baseline measurement.""" + if x is None: + x = data.xvals(0) + + d = data.view(ndarray) + + d2 = scipy.signal.detrend(d) + + stdev = d2.std() + mask = abs(d2) < stdev*threshold + #d3 = where(mask, 0, d2) + #d4 = d2 - lowPass(d3, cutoffs[1], dt=dt) + + lr = stats.linregress(x[mask], d[mask]) + base = lr[1] + lr[0]*x + d4 = d - base + + if isinstance(data, MetaArray): + return MetaArray(d4, info=data.infoCopy()) + return d4 + + +def histogramDetrend(data, window=500, bins=50, threshold=3.0): + """Linear detrend. Works by finding the most common value at the beginning and end of a trace, excluding outliers.""" + + d1 = data.view(np.ndarray) + d2 = [d1[:window], d1[-window:]] + v = [0, 0] + for i in [0, 1]: + d3 = d2[i] + stdev = d3.std() + mask = abs(d3-np.median(d3)) < stdev*threshold + d4 = d3[mask] + y, x = np.histogram(d4, bins=bins) + ind = np.argmax(y) + v[i] = 0.5 * (x[ind] + x[ind+1]) + + base = np.linspace(v[0], v[1], len(data)) + d3 = data.view(np.ndarray) - base + + if isinstance(data, MetaArray): + return MetaArray(d3, info=data.infoCopy()) + return d3 + From d9c558105cf7cab1007d4c62b79702f0a99ef6a0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 21:59:37 -0400 Subject: [PATCH 057/238] Added metaarray to support flowchart Prettied up flowchart example --- examples/Flowchart.py | 2 +- flowchart/library/functions.py | 18 +- metaarray/MetaArray.py | 1322 ++++++++++++++++++++++++++++++++ metaarray/__init__.py | 1 + metaarray/license.txt | 8 + metaarray/readMeta.m | 86 +++ 6 files changed, 1429 insertions(+), 8 deletions(-) create mode 100644 metaarray/MetaArray.py create mode 100644 metaarray/__init__.py create mode 100644 metaarray/license.txt create mode 100644 metaarray/readMeta.m diff --git a/examples/Flowchart.py b/examples/Flowchart.py index a80d9e74..977854f2 100644 --- a/examples/Flowchart.py +++ b/examples/Flowchart.py @@ -3,7 +3,7 @@ import initExample ## Add path to library (just for examples; you do not need th from pyqtgraph.flowchart import Flowchart -from pyqtgraph.Qt import QtGui +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np diff --git a/flowchart/library/functions.py b/flowchart/library/functions.py index 66709415..5d72a2fb 100644 --- a/flowchart/library/functions.py +++ b/flowchart/library/functions.py @@ -1,3 +1,7 @@ +import scipy +import numpy as np +from pyqtgraph.metaarray import MetaArray + def downsample(data, n, axis=0, xvals='subsample'): """Downsample by averaging points together across axis. If multiple axes are specified, runs once per axis. @@ -7,7 +11,7 @@ def downsample(data, n, axis=0, xvals='subsample'): ma = None if isinstance(data, MetaArray): ma = data - data = data.view(ndarray) + data = data.view(np.ndarray) if hasattr(axis, '__len__'): @@ -43,10 +47,10 @@ def downsample(data, n, axis=0, xvals='subsample'): def applyFilter(data, b, a, padding=100, bidir=True): """Apply a linear filter with coefficients a, b. Optionally pad the data before filtering and/or run the filter in both directions.""" - d1 = data.view(ndarray) + d1 = data.view(np.ndarray) if padding > 0: - d1 = numpy.hstack([d1[:padding], d1, d1[-padding:]]) + d1 = np.hstack([d1[:padding], d1, d1[-padding:]]) if bidir: d1 = scipy.signal.lfilter(b, a, scipy.signal.lfilter(b, a, d1)[::-1])[::-1] @@ -68,7 +72,7 @@ def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True): tvals = data.xvals('Time') dt = (tvals[-1]-tvals[0]) / (len(tvals)-1) except: - raise Exception('Must specify dt for this data.') + dt = 1.0 b,a = scipy.signal.bessel(order, cutoff * dt, btype=btype) @@ -86,7 +90,7 @@ def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, d tvals = data.xvals('Time') dt = (tvals[-1]-tvals[0]) / (len(tvals)-1) except: - raise Exception('Must specify dt for this data.') + dt = 1.0 if wStop is None: wStop = wPass * 2.0 @@ -148,7 +152,7 @@ def denoise(data, radius=2, threshold=4): r2 = radius * 2 - d1 = data.view(ndarray) + d1 = data.view(np.ndarray) d2 = data[radius:] - data[:-radius] #a derivative #d3 = data[r2:] - data[:-r2] #d4 = d2 - d3 @@ -174,7 +178,7 @@ def adaptiveDetrend(data, x=None, threshold=3.0): if x is None: x = data.xvals(0) - d = data.view(ndarray) + d = data.view(np.ndarray) d2 = scipy.signal.detrend(d) diff --git a/metaarray/MetaArray.py b/metaarray/MetaArray.py new file mode 100644 index 00000000..5f8d32e6 --- /dev/null +++ b/metaarray/MetaArray.py @@ -0,0 +1,1322 @@ +# -*- coding: utf-8 -*- +""" +MetaArray.py - Class encapsulating ndarray with meta data +Copyright 2010 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. + +MetaArray is an extension of ndarray which allows storage of per-axis meta data +such as axis values, names, units, column names, etc. It also enables several +new methods for slicing and indexing the array based on this meta data. +More info at http://www.scipy.org/Cookbook/MetaArray +""" + +import numpy as np +import types, copy, threading, os, re +import pickle +#import traceback + +## By default, the library will use HDF5 when writing files. +## This can be overridden by setting USE_HDF5 = False +USE_HDF5 = True +try: + import h5py + HAVE_HDF5 = True +except: + USE_HDF5 = False + HAVE_HDF5 = False + + +def axis(name=None, cols=None, values=None, units=None): + """Convenience function for generating axis descriptions when defining MetaArrays""" + ax = {} + cNameOrder = ['name', 'units', 'title'] + if name is not None: + ax['name'] = name + if values is not None: + ax['values'] = values + if units is not None: + ax['units'] = units + if cols is not None: + ax['cols'] = [] + for c in cols: + if type(c) != types.ListType and type(c) != types.TupleType: + c = [c] + col = {} + for i in range(0,len(c)): + col[cNameOrder[i]] = c[i] + ax['cols'].append(col) + return ax + +class sliceGenerator: + """Just a compact way to generate tuples of slice objects.""" + def __getitem__(self, arg): + return arg + def __getslice__(self, arg): + return arg +SLICER = sliceGenerator() + + +class MetaArray(np.ndarray): + """N-dimensional array with meta data such as axis titles, units, and column names. + + May be initialized with a file name, a tuple representing the dimensions of the array, + or any arguments that could be passed on to numpy.array() + + The info argument sets the metadata for the entire array. It is composed of a list + of axis descriptions where each axis may have a name, title, units, and a list of column + descriptions. An additional dict at the end of the axis list may specify parameters + that apply to values in the entire array. + + For example: + A 2D array of altitude values for a topographical map might look like + info=[ + {'name': 'lat', 'title': 'Lattitude'}, + {'name': 'lon', 'title': 'Longitude'}, + {'title': 'Altitude', 'units': 'm'} + ] + In this case, every value in the array represents the altitude in feet at the lat, lon + position represented by the array index. All of the following return the + value at lat=10, lon=5: + array[10, 5] + array['lon':5, 'lat':10] + array['lat':10][5] + Now suppose we want to combine this data with another array of equal dimensions that + represents the average rainfall for each location. We could easily store these as two + separate arrays or combine them into a 3D array with this description: + info=[ + {'name': 'vals', 'cols': [ + {'name': 'altitude', 'units': 'm'}, + {'name': 'rainfall', 'units': 'cm/year'} + ]}, + {'name': 'lat', 'title': 'Lattitude'}, + {'name': 'lon', 'title': 'Longitude'} + ] + We can now access the altitude values with array[0] or array['altitude'], and the + rainfall values with array[1] or array['rainfall']. All of the following return + the rainfall value at lat=10, lon=5: + array[1, 10, 5] + array['lon':5, 'lat':10, 'val': 'rainfall'] + array['rainfall', 'lon':5, 'lat':10] + Notice that in the second example, there is no need for an extra (4th) axis description + since the actual values are described (name and units) in the column info for the first axis. + """ + + version = '2' + + ## Types allowed as axis or column names + nameTypes = [basestring, tuple] + @staticmethod + def isNameType(var): + return any([isinstance(var, t) for t in MetaArray.nameTypes]) + + def __new__(subtype, data=None, file=None, info=None, dtype=None, copy=False, **kwargs): + if data is not None: + if type(data) is types.TupleType: + subarr = np.empty(data, dtype=dtype) + else: + subarr = np.array(data, dtype=dtype, copy=copy) + subarr = subarr.view(subtype) + + + #### Sanity checks on info + if info is not None: + try: + info = list(info) + except: + raise Exception("Info must be a list of axis specifications") + if len(info) < subarr.ndim+1: + info.extend([{}]*(subarr.ndim+1-len(info))) + elif len(info) > subarr.ndim+1: + raise Exception("Info parameter must be list of length ndim+1 or less.") + for i in range(len(info)): + if not isinstance(info[i], dict): + if info[i] is None: + info[i] = {} + else: + raise Exception("Axis specification must be Dict or None") + if i < subarr.ndim and info[i].has_key('values'): + if type(info[i]['values']) is types.ListType: + info[i]['values'] = np.array(info[i]['values']) + elif type(info[i]['values']) is not np.ndarray: + raise Exception("Axis values must be specified as list or ndarray") + if info[i]['values'].ndim != 1 or info[i]['values'].shape[0] != subarr.shape[i]: + raise Exception("Values array for axis %d has incorrect shape. (given %s, but should be %s)" % (i, str(info[i]['values'].shape), str((subarr.shape[i],)))) + if i < subarr.ndim and info[i].has_key('cols'): + if not isinstance(info[i]['cols'], list): + info[i]['cols'] = list(info[i]['cols']) + if len(info[i]['cols']) != subarr.shape[i]: + raise Exception('Length of column list for axis %d does not match data. (given %d, but should be %d)' % (i, len(info[i]['cols']), subarr.shape[i])) + subarr._info = info + elif hasattr(data, '_info'): + subarr._info = data._info + + + + elif file is not None: + ## decide which read function to use + fd = open(file, 'rb') + magic = fd.read(8) + if magic == '\x89HDF\r\n\x1a\n': + fd.close() + return MetaArray._readHDF5(file, subtype, **kwargs) + else: + fd.seek(0) + meta = MetaArray._readMeta(fd) + if 'version' in meta: + ver = meta['version'] + else: + ver = 1 + rFuncName = '_readData%s' % str(ver) + if not hasattr(MetaArray, rFuncName): + raise Exception("This MetaArray library does not support array version '%s'" % ver) + rFunc = getattr(MetaArray, rFuncName) + subarr = rFunc(fd, meta, subtype, **kwargs) + + return subarr + + + def __array_finalize__(self,obj): + ## array_finalize is called every time a MetaArray is created + ## (whereas __new__ is not necessarily called every time) + + ## obj is the object from which this array was generated (for example, when slicing or view()ing) + + # We use the getattr method to set a default if 'obj' doesn't have the 'info' attribute + #print "Create new MA from object", str(type(obj)) + #import traceback + #traceback.print_stack() + #print "finalize", type(self), type(obj) + if not hasattr(self, '_info'): + #if isinstance(obj, MetaArray): + #print " copy info:", obj._info + self._info = getattr(obj, '_info', [{}]*(obj.ndim+1)) + self._infoOwned = False ## Do not make changes to _info until it is copied at least once + #print " self info:", self._info + + # We could have checked first whether self._info was already defined: + #if not hasattr(self, 'info'): + # self._info = getattr(obj, 'info', {}) + + + def __getitem__(self, ind): + #print "getitem:", ind + + ## should catch scalar requests as early as possible to speed things up (?) + + nInd = self._interpretIndexes(ind) + + #print "Indexes:", nInd + try: + a = np.ndarray.__getitem__(self, nInd) + except: + #print nInd, self.shape + raise + if type(a) == type(self): ## generate new info array + #print " new MA:", type(a), a.shape + a._info = [] + extraInfo = self._info[-1].copy() + for i in range(0, len(nInd)): ## iterate over all axes + #print " axis", i + if type(nInd[i]) in [slice, list] or isinstance(nInd[i], np.ndarray): ## If the axis is sliced, keep the info but chop if necessary + #print " slice axis", i, nInd[i] + #a._info[i] = self._axisSlice(i, nInd[i]) + #print " info:", a._info[i] + a._info.append(self._axisSlice(i, nInd[i])) + else: ## If the axis is indexed, then move the information from that single index to the last info dictionary + #print "indexed:", i, nInd[i], type(nInd[i]) + newInfo = self._axisSlice(i, nInd[i]) + name = None + colName = None + for k in newInfo: + if k == 'cols': + if 'cols' not in extraInfo: + extraInfo['cols'] = [] + extraInfo['cols'].append(newInfo[k]) + if 'units' in newInfo[k]: + extraInfo['units'] = newInfo[k]['units'] + if 'name' in newInfo[k]: + colName = newInfo[k]['name'] + elif k == 'name': + name = newInfo[k] + else: + if k not in extraInfo: + extraInfo[k] = newInfo[k] + extraInfo[k] = newInfo[k] + if 'name' not in extraInfo: + if name is None: + if colName is not None: + extraInfo['name'] = colName + else: + if colName is not None: + extraInfo['name'] = str(name) + ': ' + str(colName) + else: + extraInfo['name'] = name + + + #print "Lost info:", newInfo + #a._info[i] = None + #if 'name' in newInfo: + #a._info[-1][newInfo['name']] = newInfo + a._info.append(extraInfo) + + self._infoOwned = False + #while None in a._info: + #a._info.remove(None) + return a + + def __getslice__(self, *args): + return self.__getitem__(slice(*args)) + + def __setitem__(self, ind, val): + nInd = self._interpretIndexes(ind) + try: + return np.ndarray.__setitem__(self.view(np.ndarray), nInd, val) + except: + print self, nInd, val + raise + + #def __getattr__(self, attr): + #if attr in ['round']: + #return lambda *args, **kwargs: MetaArray(getattr(a.view(ndarray), attr)(*args, **kwargs) + + + def axisValues(self, axis): + """Return the list of values for an axis""" + ax = self._interpretAxis(axis) + if self._info[ax].has_key('values'): + return self._info[ax]['values'] + else: + raise Exception('Array axis %s (%d) has no associated values.' % (str(axis), ax)) + + def xvals(self, axis): + """Synonym for axisValues()""" + return self.axisValues(axis) + + def axisHasValues(self, axis): + ax = self._interpretAxis(axis) + return self._info[ax].has_key('values') + + def axisHasColumns(self, axis): + ax = self._interpretAxis(axis) + return self._info[ax].has_key('cols') + + def axisUnits(self, axis): + """Return the units for axis""" + ax = self._info[self._interpretAxis(axis)] + if ax.has_key('units'): + return ax['units'] + + def hasColumn(self, axis, col): + ax = self._info[self._interpretAxis(axis)] + if ax.has_key('cols'): + for c in ax['cols']: + if c['name'] == col: + return True + return False + + def listColumns(self, axis=None): + """Return a list of column names for axis. If axis is not specified, then return a dict of {axisName: (column names), ...}.""" + if axis is None: + ret = {} + for i in range(self.ndim): + if 'cols' in self._info[i]: + cols = [c['name'] for c in self._info[i]['cols']] + else: + cols = [] + ret[self.axisName(i)] = cols + return ret + else: + axis = self._interpretAxis(axis) + return [c['name'] for c in self._info[axis]['cols']] + + def columnName(self, axis, col): + ax = self._info[self._interpretAxis(axis)] + return ax['cols'][col]['name'] + + def axisName(self, n): + return self._info[n].get('name', n) + + def columnUnits(self, axis, column): + """Return the units for column in axis""" + ax = self._info[self._interpretAxis(axis)] + if ax.has_key('cols'): + for c in ax['cols']: + if c['name'] == column: + return c['units'] + raise Exception("Axis %s has no column named %s" % (str(axis), str(column))) + else: + raise Exception("Axis %s has no column definitions" % str(axis)) + + def rowsort(self, axis, key=0): + """Return this object with all records sorted along axis using key as the index to the values to compare. Does not yet modify meta info.""" + ## make sure _info is copied locally before modifying it! + + keyList = self[key] + order = keyList.argsort() + if type(axis) == types.IntType: + ind = [slice(None)]*axis + ind.append(order) + elif type(axis) == types.StringType: + ind = (slice(axis, order),) + return self[tuple(ind)] + + def append(self, val, axis): + """Return this object with val appended along axis. Does not yet combine meta info.""" + ## make sure _info is copied locally before modifying it! + + s = list(self.shape) + axis = self._interpretAxis(axis) + s[axis] += 1 + n = MetaArray(tuple(s), info=self._info, dtype=self.dtype) + ind = [slice(None)]*self.ndim + ind[axis] = slice(None,-1) + n[tuple(ind)] = self + ind[axis] = -1 + n[tuple(ind)] = val + return n + + def extend(self, val, axis): + """Return the concatenation along axis of this object and val. Does not yet combine meta info.""" + ## make sure _info is copied locally before modifying it! + + axis = self._interpretAxis(axis) + return MetaArray(np.concatenate(self, val, axis), info=self._info) + + def infoCopy(self, axis=None): + """Return a deep copy of the axis meta info for this object""" + if axis is None: + return copy.deepcopy(self._info) + else: + return copy.deepcopy(self._info[self._interpretAxis(axis)]) + + def copy(self): + a = np.ndarray.copy(self) + a._info = self.infoCopy() + return a + + + def _interpretIndexes(self, ind): + #print "interpret", ind + if not isinstance(ind, tuple): + ## a list of slices should be interpreted as a tuple of slices. + if isinstance(ind, list) and len(ind) > 0 and isinstance(ind[0], slice): + ind = tuple(ind) + ## everything else can just be converted to a length-1 tuple + else: + ind = (ind,) + + nInd = [slice(None)]*self.ndim + numOk = True ## Named indices not started yet; numbered sill ok + for i in range(0,len(ind)): + (axis, index, isNamed) = self._interpretIndex(ind[i], i, numOk) + #try: + nInd[axis] = index + #except: + #print "ndim:", self.ndim + #print "axis:", axis + #print "index spec:", ind[i] + #print "index num:", index + #raise + if isNamed: + numOk = False + return tuple(nInd) + + def _interpretAxis(self, axis): + if type(axis) in [types.StringType, types.TupleType]: + return self._getAxis(axis) + else: + return axis + + def _interpretIndex(self, ind, pos, numOk): + #print "Interpreting index", ind, pos, numOk + + ## should probably check for int first to speed things up.. + if type(ind) is int: + if not numOk: + raise Exception("string and integer indexes may not follow named indexes") + #print " normal numerical index" + return (pos, ind, False) + if MetaArray.isNameType(ind): + if not numOk: + raise Exception("string and integer indexes may not follow named indexes") + #print " String index, column is ", self._getIndex(pos, ind) + return (pos, self._getIndex(pos, ind), False) + elif type(ind) is slice: + #print " Slice index" + if MetaArray.isNameType(ind.start) or MetaArray.isNameType(ind.stop): ## Not an actual slice! + #print " ..not a real slice" + axis = self._interpretAxis(ind.start) + #print " axis is", axis + + ## x[Axis:Column] + if MetaArray.isNameType(ind.stop): + #print " column name, column is ", self._getIndex(axis, ind.stop) + index = self._getIndex(axis, ind.stop) + + ## x[Axis:min:max] + elif (isinstance(ind.stop, float) or isinstance(ind.step, float)) and ('values' in self._info[axis]): + #print " axis value range" + if ind.stop is None: + mask = self.xvals(axis) < ind.step + elif ind.step is None: + mask = self.xvals(axis) >= ind.stop + else: + mask = (self.xvals(axis) >= ind.stop) * (self.xvals(axis) < ind.step) + ##print "mask:", mask + index = mask + + ## x[Axis:columnIndex] + elif isinstance(ind.stop, int) or isinstance(ind.step, int): + #print " normal slice after named axis" + if ind.step is None: + index = ind.stop + else: + index = slice(ind.stop, ind.step) + + ## x[Axis: [list]] + elif type(ind.stop) is list: + #print " list of indexes from named axis" + index = [] + for i in ind.stop: + if type(i) is int: + index.append(i) + elif MetaArray.isNameType(i): + index.append(self._getIndex(axis, i)) + else: + ## unrecognized type, try just passing on to array + index = ind.stop + break + + else: + #print " other type.. forward on to array for handling", type(ind.stop) + index = ind.stop + #print "Axis %s (%s) : %s" % (ind.start, str(axis), str(type(index))) + #if type(index) is np.ndarray: + #print " ", index.shape + return (axis, index, True) + else: + #print " Looks like a real slice, passing on to array" + return (pos, ind, False) + elif type(ind) is list: + #print " List index., interpreting each element individually" + indList = [self._interpretIndex(i, pos, numOk)[1] for i in ind] + return (pos, indList, False) + else: + if not numOk: + raise Exception("string and integer indexes may not follow named indexes") + #print " normal numerical index" + return (pos, ind, False) + + def _getAxis(self, name): + for i in range(0, len(self._info)): + axis = self._info[i] + if axis.has_key('name') and axis['name'] == name: + return i + raise Exception("No axis named %s.\n info=%s" % (name, self._info)) + + def _getIndex(self, axis, name): + ax = self._info[axis] + if ax is not None and ax.has_key('cols'): + for i in range(0, len(ax['cols'])): + if ax['cols'][i].has_key('name') and ax['cols'][i]['name'] == name: + return i + raise Exception("Axis %d has no column named %s.\n info=%s" % (axis, name, self._info)) + + def _axisCopy(self, i): + return copy.deepcopy(self._info[i]) + + def _axisSlice(self, i, cols): + #print "axisSlice", i, cols + if self._info[i].has_key('cols') or self._info[i].has_key('values'): + ax = self._axisCopy(i) + if ax.has_key('cols'): + #print " slicing columns..", array(ax['cols']), cols + sl = np.array(ax['cols'])[cols] + if isinstance(sl, np.ndarray): + sl = list(sl) + ax['cols'] = sl + #print " result:", ax['cols'] + if ax.has_key('values'): + ax['values'] = np.array(ax['values'])[cols] + else: + ax = self._info[i] + #print " ", ax + return ax + + def prettyInfo(self): + s = '' + titles = [] + maxl = 0 + for i in range(len(self._info)-1): + ax = self._info[i] + axs = '' + if 'name' in ax: + axs += '"%s"' % str(ax['name']) + else: + axs += "%d" % i + if 'units' in ax: + axs += " (%s)" % str(ax['units']) + titles.append(axs) + if len(axs) > maxl: + maxl = len(axs) + + for i in range(min(self.ndim, len(self._info)-1)): + ax = self._info[i] + axs = titles[i] + axs += '%s[%d] :' % (' ' * (maxl + 2 - len(axs)), self.shape[i]) + if 'values' in ax: + v0 = ax['values'][0] + v1 = ax['values'][-1] + axs += " values: [%g ... %g] (step %g)" % (v0, v1, (v1-v0)/(self.shape[i]-1)) + if 'cols' in ax: + axs += " columns: " + colstrs = [] + for c in range(len(ax['cols'])): + col = ax['cols'][c] + cs = str(col.get('name', c)) + if 'units' in col: + cs += " (%s)" % col['units'] + colstrs.append(cs) + axs += '[' + ', '.join(colstrs) + ']' + s += axs + "\n" + s += str(self._info[-1]) + return s + + def __repr__(self): + return "%s\n-----------------------------------------------\n%s" % (self.view(np.ndarray).__repr__(), self.prettyInfo()) + + def __str__(self): + return self.__repr__() + + + def axisCollapsingFn(self, fn, axis=None, *args, **kargs): + arr = self.view(np.ndarray) + fn = getattr(arr, fn) + if axis is None: + return fn(axis, *args, **kargs) + else: + info = self.infoCopy() + axis = self._interpretAxis(axis) + info.pop(axis) + return MetaArray(fn(axis, *args, **kargs), info=info) + + def mean(self, axis=None, *args, **kargs): + return self.axisCollapsingFn('mean', axis, *args, **kargs) + + + def min(self, axis=None, *args, **kargs): + return self.axisCollapsingFn('min', axis, *args, **kargs) + + def max(self, axis=None, *args, **kargs): + return self.axisCollapsingFn('max', axis, *args, **kargs) + + def transpose(self, *args): + if len(args) == 1 and hasattr(args[0], '__iter__'): + order = args[0] + else: + order = args + + order = [self._interpretAxis(ax) for ax in order] + infoOrder = order + range(len(order), len(self._info)) + info = [self._info[i] for i in infoOrder] + order = order + range(len(order), self.ndim) + + try: + return MetaArray(self.view(np.ndarray).transpose(order), info=info) + except: + print order + raise + + #### File I/O Routines + + @staticmethod + def _readMeta(fd): + """Read meta array from the top of a file. Read lines until a blank line is reached. + This function should ideally work for ALL versions of MetaArray. + """ + meta = '' + ## Read meta information until the first blank line + while True: + line = fd.readline().strip() + if line == '': + break + meta += line + ret = eval(meta) + #print ret + return ret + + @staticmethod + def _readData1(fd, meta, subtype, mmap=False): + """Read array data from the file descriptor for MetaArray v1 files + """ + ## read in axis values for any axis that specifies a length + frameSize = 1 + for ax in meta['info']: + if ax.has_key('values_len'): + ax['values'] = np.fromstring(fd.read(ax['values_len']), dtype=ax['values_type']) + frameSize *= ax['values_len'] + del ax['values_len'] + del ax['values_type'] + ## the remaining data is the actual array + if mmap: + subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape']) + else: + subarr = np.fromstring(fd.read(), dtype=meta['type']) + subarr.shape = meta['shape'] + subarr = subarr.view(subtype) + subarr._info = meta['info'] + return subarr + + @staticmethod + def _readData2(fd, meta, subtype, mmap=False, subset=None): + ## read in axis values + dynAxis = None + frameSize = 1 + ## read in axis values for any axis that specifies a length + for i in range(len(meta['info'])): + ax = meta['info'][i] + if ax.has_key('values_len'): + if ax['values_len'] == 'dynamic': + if dynAxis is not None: + raise Exception("MetaArray has more than one dynamic axis! (this is not allowed)") + dynAxis = i + else: + ax['values'] = np.fromstring(fd.read(ax['values_len']), dtype=ax['values_type']) + frameSize *= ax['values_len'] + del ax['values_len'] + del ax['values_type'] + + ## No axes are dynamic, just read the entire array in at once + if dynAxis is None: + #if rewriteDynamic is not None: + #raise Exception("") + if meta['type'] == 'object': + if mmap: + raise Exception('memmap not supported for arrays with dtype=object') + subarr = pickle.loads(fd.read()) + else: + if mmap: + subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape']) + else: + subarr = np.fromstring(fd.read(), dtype=meta['type']) + #subarr = subarr.view(subtype) + subarr.shape = meta['shape'] + #subarr._info = meta['info'] + ## One axis is dynamic, read in a frame at a time + else: + if mmap: + raise Exception('memmap not supported for non-contiguous arrays. Use rewriteContiguous() to convert.') + ax = meta['info'][dynAxis] + xVals = [] + frames = [] + frameShape = list(meta['shape']) + frameShape[dynAxis] = 1 + frameSize = reduce(lambda a,b: a*b, frameShape) + n = 0 + while True: + ## Extract one non-blank line + while True: + line = fd.readline() + if line != '\n': + break + if line == '': + break + + ## evaluate line + inf = eval(line) + + ## read data block + #print "read %d bytes as %s" % (inf['len'], meta['type']) + if meta['type'] == 'object': + data = pickle.loads(fd.read(inf['len'])) + else: + data = np.fromstring(fd.read(inf['len']), dtype=meta['type']) + + if data.size != frameSize * inf['numFrames']: + #print data.size, frameSize, inf['numFrames'] + raise Exception("Wrong frame size in MetaArray file! (frame %d)" % n) + + ## read in data block + shape = list(frameShape) + shape[dynAxis] = inf['numFrames'] + data.shape = shape + if subset is not None: + dSlice = subset[dynAxis] + if dSlice.start is None: + dStart = 0 + else: + dStart = max(0, dSlice.start - n) + if dSlice.stop is None: + dStop = data.shape[dynAxis] + else: + dStop = min(data.shape[dynAxis], dSlice.stop - n) + newSubset = list(subset[:]) + newSubset[dynAxis] = slice(dStart, dStop) + if dStop > dStart: + #print n, data.shape, " => ", newSubset, data[tuple(newSubset)].shape + frames.append(data[tuple(newSubset)].copy()) + else: + #data = data[subset].copy() ## what's this for?? + frames.append(data) + + n += inf['numFrames'] + if 'xVals' in inf: + xVals.extend(inf['xVals']) + subarr = np.concatenate(frames, axis=dynAxis) + if len(xVals)> 0: + ax['values'] = np.array(xVals, dtype=ax['values_type']) + del ax['values_len'] + del ax['values_type'] + subarr = subarr.view(subtype) + subarr._info = meta['info'] + #raise Exception() ## stress-testing + return subarr + + @staticmethod + def _readHDF5(fileName, subtype, mmap=False, writable=False): + if not HAVE_HDF5: + raise Exception("The file '%s' is HDF5-formatted, but the HDF5 library (h5py) was not found." % fileName) + f = h5py.File(fileName, 'r') + ver = f.attrs['MetaArray'] + if ver > MetaArray.version: + print "Warning: This file was written with MetaArray version %s, but you are using version %s. (Will attempt to read anyway)" % (str(ver), str(MetaArray.version)) + meta = MetaArray.readHDF5Meta(f['info']) + + if mmap: + arr = MetaArray.mapHDF5Array(f['data'], writable=writable) + else: + arr = f['data'][:] + #meta = H5MetaList(f['info']) + subarr = arr.view(subtype) + subarr._info = meta + f.close() + return subarr + + @staticmethod + def mapHDF5Array(data, writable=False): + off = data.id.get_offset() + if writable: + mode = 'r+' + else: + mode = 'r' + if off is None: + raise Exception("This dataset uses chunked storage; it can not be memory-mapped. (store using mappable=True)") + return np.memmap(filename=data.file.filename, offset=off, dtype=data.dtype, shape=data.shape, mode=mode) + + + + + @staticmethod + def readHDF5Meta(root, mmap=False): + data = {} + + ## Pull list of values from attributes and child objects + for k in root.attrs: + val = root.attrs[k] + if isinstance(val, basestring): ## strings need to be re-evaluated to their original types + try: + val = eval(val) + except: + raise Exception('Can not evaluate string: "%s"' % val) + data[k] = val + for k in root: + obj = root[k] + if isinstance(obj, h5py.highlevel.Group): + val = MetaArray.readHDF5Meta(obj) + elif isinstance(obj, h5py.highlevel.Dataset): + if mmap: + val = MetaArray.mapHDF5Array(obj) + else: + val = obj[:] + else: + raise Exception("Don't know what to do with type '%s'" % str(type(obj))) + data[k] = val + + typ = root.attrs['_metaType_'] + del data['_metaType_'] + + if typ == 'dict': + return data + elif typ == 'list' or typ == 'tuple': + d2 = [None]*len(data) + for k in data: + d2[int(k)] = data[k] + if typ == 'tuple': + d2 = tuple(d2) + return d2 + else: + raise Exception("Don't understand metaType '%s'" % typ) + + + def write(self, fileName, **opts): + """Write this object to a file. The object can be restored by calling MetaArray(file=fileName) + opts: + appendAxis: the name (or index) of the appendable axis. Allows the array to grow. + compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. + chunks: bool or tuple specifying chunk shape + """ + + if USE_HDF5 and HAVE_HDF5: + return self.writeHDF5(fileName, **opts) + else: + return self.writeMa(fileName, **opts) + + def writeMeta(self, fileName): + """Used to re-write meta info to the given file. + This feature is only available for HDF5 files.""" + f = h5py.File(fileName, 'r+') + if f.attrs['MetaArray'] != MetaArray.version: + raise Exception("The file %s was created with a different version of MetaArray. Will not modify." % fileName) + del f['info'] + + self.writeHDF5Meta(f, 'info', self._info) + f.close() + + + def writeHDF5(self, fileName, **opts): + ## default options for writing datasets + dsOpts = { + 'compression': 'lzf', + 'chunks': True, + } + + ## if there is an appendable axis, then we can guess the desired chunk shape (optimized for appending) + appAxis = opts.get('appendAxis', None) + if appAxis is not None: + appAxis = self._interpretAxis(appAxis) + cs = [min(100000, x) for x in self.shape] + cs[appAxis] = 1 + dsOpts['chunks'] = tuple(cs) + + ## if there are columns, then we can guess a different chunk shape + ## (read one column at a time) + else: + cs = [min(100000, x) for x in self.shape] + for i in range(self.ndim): + if 'cols' in self._info[i]: + cs[i] = 1 + dsOpts['chunks'] = tuple(cs) + + ## update options if they were passed in + for k in dsOpts: + if k in opts: + dsOpts[k] = opts[k] + + + ## If mappable is in options, it disables chunking/compression + if opts.get('mappable', False): + dsOpts = { + 'chunks': None, + 'compression': None + } + + + ## set maximum shape to allow expansion along appendAxis + append = False + if appAxis is not None: + maxShape = list(self.shape) + ax = self._interpretAxis(appAxis) + maxShape[ax] = None + if os.path.exists(fileName): + append = True + dsOpts['maxshape'] = tuple(maxShape) + else: + dsOpts['maxshape'] = None + + if append: + f = h5py.File(fileName, 'r+') + if f.attrs['MetaArray'] != MetaArray.version: + raise Exception("The file %s was created with a different version of MetaArray. Will not modify." % fileName) + + ## resize data and write in new values + data = f['data'] + shape = list(data.shape) + shape[ax] += self.shape[ax] + data.resize(tuple(shape)) + sl = [slice(None)] * len(data.shape) + sl[ax] = slice(-self.shape[ax], None) + data[tuple(sl)] = self.view(np.ndarray) + + ## add axis values if they are present. + axInfo = f['info'][str(ax)] + if 'values' in axInfo: + v = axInfo['values'] + v2 = self._info[ax]['values'] + shape = list(v.shape) + shape[0] += v2.shape[0] + v.resize(shape) + v[-v2.shape[0]:] = v2 + f.close() + else: + f = h5py.File(fileName, 'w') + f.attrs['MetaArray'] = MetaArray.version + #print dsOpts + f.create_dataset('data', data=self.view(np.ndarray), **dsOpts) + + ## dsOpts is used when storing meta data whenever an array is encountered + ## however, 'chunks' will no longer be valid for these arrays if it specifies a chunk shape. + ## 'maxshape' is right-out. + if isinstance(dsOpts['chunks'], tuple): + dsOpts['chunks'] = True + if 'maxshape' in dsOpts: + del dsOpts['maxshape'] + self.writeHDF5Meta(f, 'info', self._info, **dsOpts) + f.close() + + def writeHDF5Meta(self, root, name, data, **dsOpts): + if isinstance(data, np.ndarray): + dsOpts['maxshape'] = (None,) + data.shape[1:] + root.create_dataset(name, data=data, **dsOpts) + elif isinstance(data, list) or isinstance(data, tuple): + gr = root.create_group(name) + if isinstance(data, list): + gr.attrs['_metaType_'] = 'list' + else: + gr.attrs['_metaType_'] = 'tuple' + #n = int(np.log10(len(data))) + 1 + for i in xrange(len(data)): + self.writeHDF5Meta(gr, str(i), data[i], **dsOpts) + elif isinstance(data, dict): + gr = root.create_group(name) + gr.attrs['_metaType_'] = 'dict' + for k, v in data.iteritems(): + self.writeHDF5Meta(gr, k, v, **dsOpts) + elif isinstance(data, int) or isinstance(data, float) or isinstance(data, np.integer) or isinstance(data, np.floating): + root.attrs[name] = data + else: + try: ## strings, bools, None are stored as repr() strings + root.attrs[name] = repr(data) + except: + print "Can not store meta data of type '%s' in HDF5. (key is '%s')" % (str(type(data)), str(name)) + raise + + + def writeMa(self, fileName, appendAxis=None, newFile=False): + """Write an old-style .ma file""" + meta = {'shape':self.shape, 'type':str(self.dtype), 'info':self.infoCopy(), 'version':MetaArray.version} + axstrs = [] + + ## copy out axis values for dynamic axis if requested + if appendAxis is not None: + if MetaArray.isNameType(appendAxis): + appendAxis = self._interpretAxis(appendAxis) + + + ax = meta['info'][appendAxis] + ax['values_len'] = 'dynamic' + if 'values' in ax: + ax['values_type'] = str(ax['values'].dtype) + dynXVals = ax['values'] + del ax['values'] + else: + dynXVals = None + + ## Generate axis data string, modify axis info so we know how to read it back in later + for ax in meta['info']: + if 'values' in ax: + axstrs.append(ax['values'].tostring()) + ax['values_len'] = len(axstrs[-1]) + ax['values_type'] = str(ax['values'].dtype) + del ax['values'] + + ## Decide whether to output the meta block for a new file + if not newFile: + ## If the file does not exist or its size is 0, then we must write the header + newFile = (not os.path.exists(fileName)) or (os.stat(fileName).st_size == 0) + + ## write data to file + if appendAxis is None or newFile: + fd = open(fileName, 'wb') + fd.write(str(meta) + '\n\n') + for ax in axstrs: + fd.write(ax) + else: + fd = open(fileName, 'ab') + + if self.dtype != object: + dataStr = self.view(np.ndarray).tostring() + else: + dataStr = pickle.dumps(self.view(np.ndarray)) + #print self.size, len(dataStr), self.dtype + if appendAxis is not None: + frameInfo = {'len':len(dataStr), 'numFrames':self.shape[appendAxis]} + if dynXVals is not None: + frameInfo['xVals'] = list(dynXVals) + fd.write('\n'+str(frameInfo)+'\n') + fd.write(dataStr) + fd.close() + + def writeCsv(self, fileName=None): + """Write 2D array to CSV file or return the string if no filename is given""" + if self.ndim > 2: + raise Exception("CSV Export is only for 2D arrays") + if fileName is not None: + file = open(fileName, 'w') + ret = '' + if self._info[0].has_key('cols'): + s = ','.join([x['name'] for x in self._info[0]['cols']]) + '\n' + if fileName is not None: + file.write(s) + else: + ret += s + for row in range(0, self.shape[1]): + s = ','.join(["%g" % x for x in self[:, row]]) + '\n' + if fileName is not None: + file.write(s) + else: + ret += s + if fileName is not None: + file.close() + else: + return ret + + + +#class H5MetaList(): + + +#def rewriteContiguous(fileName, newName): + #"""Rewrite a dynamic array file as contiguous""" + #def _readData2(fd, meta, subtype, mmap): + ### read in axis values + #dynAxis = None + #frameSize = 1 + ### read in axis values for any axis that specifies a length + #for i in range(len(meta['info'])): + #ax = meta['info'][i] + #if ax.has_key('values_len'): + #if ax['values_len'] == 'dynamic': + #if dynAxis is not None: + #raise Exception("MetaArray has more than one dynamic axis! (this is not allowed)") + #dynAxis = i + #else: + #ax['values'] = fromstring(fd.read(ax['values_len']), dtype=ax['values_type']) + #frameSize *= ax['values_len'] + #del ax['values_len'] + #del ax['values_type'] + + ### No axes are dynamic, just read the entire array in at once + #if dynAxis is None: + #raise Exception('Array has no dynamic axes.') + ### One axis is dynamic, read in a frame at a time + #else: + #if mmap: + #raise Exception('memmap not supported for non-contiguous arrays. Use rewriteContiguous() to convert.') + #ax = meta['info'][dynAxis] + #xVals = [] + #frames = [] + #frameShape = list(meta['shape']) + #frameShape[dynAxis] = 1 + #frameSize = reduce(lambda a,b: a*b, frameShape) + #n = 0 + #while True: + ### Extract one non-blank line + #while True: + #line = fd.readline() + #if line != '\n': + #break + #if line == '': + #break + + ### evaluate line + #inf = eval(line) + + ### read data block + ##print "read %d bytes as %s" % (inf['len'], meta['type']) + #if meta['type'] == 'object': + #data = pickle.loads(fd.read(inf['len'])) + #else: + #data = fromstring(fd.read(inf['len']), dtype=meta['type']) + + #if data.size != frameSize * inf['numFrames']: + ##print data.size, frameSize, inf['numFrames'] + #raise Exception("Wrong frame size in MetaArray file! (frame %d)" % n) + + ### read in data block + #shape = list(frameShape) + #shape[dynAxis] = inf['numFrames'] + #data.shape = shape + #frames.append(data) + + #n += inf['numFrames'] + #if 'xVals' in inf: + #xVals.extend(inf['xVals']) + #subarr = np.concatenate(frames, axis=dynAxis) + #if len(xVals)> 0: + #ax['values'] = array(xVals, dtype=ax['values_type']) + #del ax['values_len'] + #del ax['values_type'] + #subarr = subarr.view(subtype) + #subarr._info = meta['info'] + #return subarr + + + + + +if __name__ == '__main__': + ## Create an array with every option possible + + arr = np.zeros((2, 5, 3, 5), dtype=int) + for i in range(arr.shape[0]): + for j in range(arr.shape[1]): + for k in range(arr.shape[2]): + for l in range(arr.shape[3]): + arr[i,j,k,l] = (i+1)*1000 + (j+1)*100 + (k+1)*10 + (l+1) + + info = [ + axis('Axis1'), + axis('Axis2', values=[1,2,3,4,5]), + axis('Axis3', cols=[ + ('Ax3Col1'), + ('Ax3Col2', 'mV', 'Axis3 Column2'), + (('Ax3','Col3'), 'A', 'Axis3 Column3')]), + {'name': 'Axis4', 'values': np.array([1.1, 1.2, 1.3, 1.4, 1.5]), 'units': 's'}, + {'extra': 'info'} + ] + + ma = MetaArray(arr, info=info) + + print "==== Original Array =======" + print ma + print "\n\n" + + #### Tests follow: + + + #### Index/slice tests: check that all values and meta info are correct after slice + print "\n -- normal integer indexing\n" + + print "\n ma[1]" + print ma[1] + + print "\n ma[1, 2:4]" + print ma[1, 2:4] + + print "\n ma[1, 1:5:2]" + print ma[1, 1:5:2] + + print "\n -- named axis indexing\n" + + print "\n ma['Axis2':3]" + print ma['Axis2':3] + + print "\n ma['Axis2':3:5]" + print ma['Axis2':3:5] + + print "\n ma[1, 'Axis2':3]" + print ma[1, 'Axis2':3] + + print "\n ma[:, 'Axis2':3]" + print ma[:, 'Axis2':3] + + print "\n ma['Axis2':3, 'Axis4':0:2]" + print ma['Axis2':3, 'Axis4':0:2] + + + print "\n -- column name indexing\n" + + print "\n ma['Axis3':'Ax3Col1']" + print ma['Axis3':'Ax3Col1'] + + print "\n ma['Axis3':('Ax3','Col3')]" + print ma['Axis3':('Ax3','Col3')] + + print "\n ma[:, :, 'Ax3Col2']" + print ma[:, :, 'Ax3Col2'] + + print "\n ma[:, :, ('Ax3','Col3')]" + print ma[:, :, ('Ax3','Col3')] + + + print "\n -- axis value range indexing\n" + + print "\n ma['Axis2':1.5:4.5]" + print ma['Axis2':1.5:4.5] + + print "\n ma['Axis4':1.15:1.45]" + print ma['Axis4':1.15:1.45] + + print "\n ma['Axis4':1.15:1.25]" + print ma['Axis4':1.15:1.25] + + + + print "\n -- list indexing\n" + + print "\n ma[:, [0,2,4]]" + print ma[:, [0,2,4]] + + print "\n ma['Axis4':[0,2,4]]" + print ma['Axis4':[0,2,4]] + + print "\n ma['Axis3':[0, ('Ax3','Col3')]]" + print ma['Axis3':[0, ('Ax3','Col3')]] + + + + print "\n -- boolean indexing\n" + + print "\n ma[:, array([True, True, False, True, False])]" + print ma[:, np.array([True, True, False, True, False])] + + print "\n ma['Axis4':array([True, False, False, False])]" + print ma['Axis4':np.array([True, False, False, False])] + + + + + + #### Array operations + # - Concatenate + # - Append + # - Extend + # - Rowsort + + + + + #### File I/O tests + + print "\n================ File I/O Tests ===================\n" + import tempfile + tf = tempfile.mktemp() + tf = 'test.ma' + # write whole array + + print "\n -- write/read test" + ma.write(tf) + ma2 = MetaArray(file=tf) + + #print ma2 + print "\nArrays are equivalent:", (ma == ma2).all() + #print "Meta info is equivalent:", ma.infoCopy() == ma2.infoCopy() + os.remove(tf) + + # CSV write + + # append mode + + + print "\n================append test (%s)===============" % tf + ma['Axis2':0:2].write(tf, appendAxis='Axis2') + for i in range(2,ma.shape[1]): + ma['Axis2':[i]].write(tf, appendAxis='Axis2') + + ma2 = MetaArray(file=tf) + + #print ma2 + print "\nArrays are equivalent:", (ma == ma2).all() + #print "Meta info is equivalent:", ma.infoCopy() == ma2.infoCopy() + + os.remove(tf) + + + + ## memmap test + print "\n==========Memmap test============" + ma.write(tf, mappable=True) + ma2 = MetaArray(file=tf, mmap=True) + print "\nArrays are equivalent:", (ma == ma2).all() + os.remove(tf) + \ No newline at end of file diff --git a/metaarray/__init__.py b/metaarray/__init__.py new file mode 100644 index 00000000..e931109d --- /dev/null +++ b/metaarray/__init__.py @@ -0,0 +1 @@ +from MetaArray import * diff --git a/metaarray/license.txt b/metaarray/license.txt new file mode 100644 index 00000000..7ef3e5e9 --- /dev/null +++ b/metaarray/license.txt @@ -0,0 +1,8 @@ +Copyright (c) 2010 Luke Campagnola ('luke.campagnola@%s.com' % 'gmail') +The MIT License +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. + diff --git a/metaarray/readMeta.m b/metaarray/readMeta.m new file mode 100644 index 00000000..f468a75d --- /dev/null +++ b/metaarray/readMeta.m @@ -0,0 +1,86 @@ +function f = readMeta(file) +info = hdf5info(file); +f = readMetaRecursive(info.GroupHierarchy.Groups(1)); +end + + +function f = readMetaRecursive(root) +typ = 0; +for i = 1:length(root.Attributes) + if strcmp(root.Attributes(i).Shortname, '_metaType_') + typ = root.Attributes(i).Value.Data; + break + end +end +if typ == 0 + printf('group has no _metaType_') + typ = 'dict'; +end + +list = 0; +if strcmp(typ, 'list') || strcmp(typ, 'tuple') + data = {}; + list = 1; +elseif strcmp(typ, 'dict') + data = struct(); +else + printf('Unrecognized meta type %s', typ); + data = struct(); +end + +for i = 1:length(root.Attributes) + name = root.Attributes(i).Shortname; + if strcmp(name, '_metaType_') + continue + end + val = root.Attributes(i).Value; + if isa(val, 'hdf5.h5string') + val = val.Data; + end + if list + ind = str2num(name)+1; + data{ind} = val; + else + data.(name) = val; + end +end + +for i = 1:length(root.Datasets) + fullName = root.Datasets(i).Name; + name = stripName(fullName); + file = root.Datasets(i).Filename; + data2 = hdf5read(file, fullName); + if list + ind = str2num(name)+1; + data{ind} = data2; + else + data.(name) = data2; + end +end + +for i = 1:length(root.Groups) + name = stripName(root.Groups(i).Name); + data2 = readMetaRecursive(root.Groups(i)); + if list + ind = str2num(name)+1; + data{ind} = data2; + else + data.(name) = data2; + end +end +f = data; +return; +end + + +function f = stripName(str) +inds = strfind(str, '/'); +if isempty(inds) + f = str; +else + f = str(inds(length(inds))+1:length(str)); +end +end + + + From a5f3d5ac9c48d7ad217a824a6bf71dc328ac1603 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 22:20:21 -0400 Subject: [PATCH 058/238] added documentation to flowchart example --- examples/Flowchart.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/examples/Flowchart.py b/examples/Flowchart.py index 977854f2..546faffa 100644 --- a/examples/Flowchart.py +++ b/examples/Flowchart.py @@ -1,4 +1,15 @@ # -*- coding: utf-8 -*- +""" +This example demonstrates a very basic use of flowcharts: filter data, +displaying both the input and output of the filter. The behavior of +he filter can be reprogrammed by the user. + +Basic steps are: + - create a flowchart and two plots + - input noisy data to the flowchart + - flowchart connects data to the first plot, where it is displayed + - add a gaussian filter to lowpass the data, then display it in the second plot. +""" import initExample ## Add path to library (just for examples; you do not need this) @@ -45,6 +56,7 @@ pw2Node = fc.createNode('PlotWidget', pos=(150, -150)) pw2Node.setPlot(pw2) fNode = fc.createNode('GaussianFilter', pos=(0, 0)) +fNode.ctrls['sigma'].setValue(5) fc.connectTerminals(fc.dataIn, fNode.In) fc.connectTerminals(fc.dataIn, pw1Node.In) fc.connectTerminals(fNode.Out, pw2Node.In) From a336728006a15e48f6d6762fafca2e4397e440f3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 4 Apr 2012 22:54:46 -0400 Subject: [PATCH 059/238] small fixes for new modules --- configfile.py | 11 +++++++++-- widgets/FeedbackButton.py | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/configfile.py b/configfile.py index c851edb1..6b01d0cf 100644 --- a/configfile.py +++ b/configfile.py @@ -95,6 +95,7 @@ def parseString(lines, start=0): data = OrderedDict() if isinstance(lines, basestring): lines = lines.split('\n') + lines = filter(lambda l: re.search(r'\S', l) and not re.match(r'\s*#', l), lines) ## remove empty lines indent = measureIndent(lines[start]) ln = start - 1 @@ -179,14 +180,20 @@ if __name__ == '__main__': tf = open(fn, 'w') cf = """ key: 'value' -key2: - key21: 'value' +key2: ##comment + ##comment + key21: 'value' ## comment + ##comment key22: [1,2,3] key23: 234 #comment """ tf.write(cf) tf.close() print "=== Test:===" + num = 1 + for line in cf.split('\n'): + print "%02d %s" % (num, line) + num += 1 print cf print "============" data = readConfigFile(fn) diff --git a/widgets/FeedbackButton.py b/widgets/FeedbackButton.py index 5112e955..f788f4b6 100644 --- a/widgets/FeedbackButton.py +++ b/widgets/FeedbackButton.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- from pyqtgraph.Qt import QtCore, QtGui + +__all__ = ['FeedbackButton'] + class FeedbackButton(QtGui.QPushButton): """ QPushButton which flashes success/failure indication for slow or asynchronous procedures. From 41ada931d4f6a079af50ab7e2c86935f341c6bb5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 5 Apr 2012 23:04:26 -0400 Subject: [PATCH 060/238] Fixed up JoystickButton example --- examples/JoystickButton.py | 55 ++++++++++++++++++++++++++++++++++++++ examples/__main__.py | 2 +- widgets/JoystickButton.py | 5 +++- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 examples/JoystickButton.py diff --git a/examples/JoystickButton.py b/examples/JoystickButton.py new file mode 100644 index 00000000..0c733513 --- /dev/null +++ b/examples/JoystickButton.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +## Display an animated arrowhead following a curve. +## This example uses the CurveArrow class, which is a combination +## of ArrowItem and CurvePoint. +## +## To place a static arrow anywhere in a scene, use ArrowItem. +## To attach other types of item to a curve, use CurvePoint. + +import initExample ## Add path to library (just for examples; you do not need this) + +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) +mw = QtGui.QMainWindow() +mw.resize(300,50) +cw = QtGui.QWidget() +mw.setCentralWidget(cw) +layout = QtGui.QGridLayout() +cw.setLayout(layout) +mw.show() + +l1 = pg.ValueLabel(siPrefix=True, suffix='m') +l2 = pg.ValueLabel(siPrefix=True, suffix='m') +jb = pg.JoystickButton() +jb.setFixedWidth(30) +jb.setFixedHeight(30) + + +layout.addWidget(l1, 0, 0) +layout.addWidget(l2, 0, 1) +layout.addWidget(jb, 0, 2) + +x = 0 +y = 0 +def update(): + global x, y, l1, l2, jb + dx, dy = jb.getState() + x += dx * 1e-3 + y += dy * 1e-3 + l1.setValue(x) + l2.setValue(y) +timer = QtCore.QTimer() +timer.timeout.connect(update) +timer.start(30) + + + + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() diff --git a/examples/__main__.py b/examples/__main__.py index 8511327d..632f516a 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -42,7 +42,7 @@ examples = OrderedDict([ ('ColorButton', '../widgets/ColorButton.py'), #('CheckTable', '../widgets/CheckTable.py'), #('VerticalLabel', '../widgets/VerticalLabel.py'), - ('JoystickButton', '../widgets/JoystickButton.py'), + ('JoystickButton', 'JoystickButton.py'), ])), ('GraphicsScene', 'GraphicsScene.py'), diff --git a/widgets/JoystickButton.py b/widgets/JoystickButton.py index 5320563f..0ef5f2ee 100644 --- a/widgets/JoystickButton.py +++ b/widgets/JoystickButton.py @@ -4,7 +4,7 @@ from pyqtgraph.Qt import QtGui, QtCore __all__ = ['JoystickButton'] class JoystickButton(QtGui.QPushButton): - sigStateChanged = QtCore.Signal(object, object) + sigStateChanged = QtCore.Signal(object, object) ## self, state def __init__(self, parent=None): QtGui.QPushButton.__init__(self, parent) @@ -34,6 +34,9 @@ class JoystickButton(QtGui.QPushButton): def doubleClickEvent(self, ev): ev.accept() + def getState(self): + return self.state + def setState(self, *xy): xy = list(xy) d = (xy[0]**2 + xy[1]**2)**0.5 From 7287771577670342e44e02f28f046a7ab0b5bb85 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Thu, 12 Apr 2012 12:44:31 -0400 Subject: [PATCH 061/238] docstring update --- examples/JoystickButton.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/JoystickButton.py b/examples/JoystickButton.py index 0c733513..7acf2450 100644 --- a/examples/JoystickButton.py +++ b/examples/JoystickButton.py @@ -1,11 +1,8 @@ # -*- coding: utf-8 -*- - -## Display an animated arrowhead following a curve. -## This example uses the CurveArrow class, which is a combination -## of ArrowItem and CurvePoint. -## -## To place a static arrow anywhere in a scene, use ArrowItem. -## To attach other types of item to a curve, use CurvePoint. +""" +JoystickButton is a button with x/y values. When the button is depressed and the mouse dragged, the x/y values change to follow the mouse. +When the mouse button is released, the x/y values change to 0,0 (rather like litting go of the joystick). +""" import initExample ## Add path to library (just for examples; you do not need this) From 355472271bace9166f751b11c95138bece6958cb Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 15 Apr 2012 10:19:30 -0400 Subject: [PATCH 062/238] Fix to ensure that items are given the keyboard focus if they are clicked --- GraphicsScene/GraphicsScene.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/GraphicsScene/GraphicsScene.py b/GraphicsScene/GraphicsScene.py index 9cc2491a..2bcf504e 100644 --- a/GraphicsScene/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -124,6 +124,13 @@ class GraphicsScene(QtGui.QGraphicsScene): #print "mouseGrabberItem: ", self.mouseGrabberItem() if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events self.clickEvents.append(MouseClickEvent(ev)) + + ## set focus on the topmost focusable item under this click + items = self.items(ev.scenePos()) + for i in items: + if i.isEnabled() and i.isVisible() and int(i.flags() & i.ItemIsFocusable) > 0: + i.setFocus(QtCore.Qt.MouseFocusReason) + break #else: #addr = sip.unwrapinstance(sip.cast(self.mouseGrabberItem(), QtGui.QGraphicsItem)) #item = GraphicsScene._addressCache.get(addr, self.mouseGrabberItem()) @@ -134,12 +141,10 @@ class GraphicsScene(QtGui.QGraphicsScene): ## First allow QGraphicsScene to deliver hoverEnter/Move/ExitEvents QtGui.QGraphicsScene.mouseMoveEvent(self, ev) - - + ## Next deliver our own HoverEvents self.sendHoverEvents(ev) - if int(ev.buttons()) != 0: ## button is pressed; send mouseMoveEvents and mouseDragEvents QtGui.QGraphicsScene.mouseMoveEvent(self, ev) if self.mouseGrabberItem() is None: @@ -266,6 +271,8 @@ class GraphicsScene(QtGui.QGraphicsScene): #print "drag -> new item" for item in self.itemsNearEvent(event): #print "check item:", item + if not item.isVisible() or not item.isEnabled(): + continue if hasattr(item, 'mouseDragEvent'): event.currentItem = item try: @@ -275,6 +282,8 @@ class GraphicsScene(QtGui.QGraphicsScene): if event.isAccepted(): #print " --> accepted" self.dragItem = item + if int(item.flags() & item.ItemIsFocusable) > 0: + item.setFocus(QtCore.Qt.MouseFocusReason) break elif self.dragItem is not None: event.currentItem = self.dragItem @@ -309,6 +318,8 @@ class GraphicsScene(QtGui.QGraphicsScene): debug.printExc("Error sending click event:") else: for item in self.itemsNearEvent(ev): + if not item.isVisible() or not item.isEnabled(): + continue if hasattr(item, 'mouseClickEvent'): ev.currentItem = item try: @@ -317,6 +328,8 @@ class GraphicsScene(QtGui.QGraphicsScene): debug.printExc("Error sending click event:") if ev.isAccepted(): + if int(item.flags() & item.ItemIsFocusable) > 0: + item.setFocus(QtCore.Qt.MouseFocusReason) break if not ev.isAccepted() and ev.button() is QtCore.Qt.RightButton: #print "GraphicsScene emitting sigSceneContextMenu" From 727214ca45609aadf46027184bd537e2893fff44 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 15 Apr 2012 10:20:07 -0400 Subject: [PATCH 063/238] docstring updates --- graphicsItems/PlotItem/PlotItem.py | 24 ++++++++++++++++++++---- widgets/PlotWidget.py | 20 ++++++++++---------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index b9fcff5a..cc0bac8c 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -59,7 +59,20 @@ class PlotItem(GraphicsWidget): sigXRangeChanged = QtCore.Signal(object, object) sigRangeChanged = QtCore.Signal(object, object) - """Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox.""" + """ + Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox. + + Use plot(...) to create a new PlotDataItem and add it to the view. + Use addItem(...) add any QGraphicsItem to the view + + This class wraps several methods from its internal ViewBox: + setXRange, setYRange, setXLink, setYLink, + setRange, autoRange, viewRect, setMouseEnabled, + enableAutoRange, disableAutoRange, setAspectLocked, + register, unregister. + + The ViewBox itself can be accessed by calling getVewBox() + """ lastFileDir = None managers = {} @@ -76,6 +89,8 @@ class PlotItem(GraphicsWidget): Optionally, PlotItem my also be initialized with the keyword arguments left, right, top, or bottom to achieve the same effect. *name* - Registers a name for this view so that others may link to it + + """ GraphicsWidget.__init__(self, parent) @@ -289,6 +304,7 @@ class PlotItem(GraphicsWidget): return interface in ['ViewBoxWrapper'] def getViewBox(self): + """Return the ViewBox within.""" return self.vb @@ -346,7 +362,7 @@ class PlotItem(GraphicsWidget): #else: #print "no manager" - def registerPlot(self, name): + def registerPlot(self, name): ## for backward compatibility self.vb.register(name) #self.name = name #win = str(self.window()) @@ -398,7 +414,7 @@ class PlotItem(GraphicsWidget): self.scales[k]['item'].setGrid(g) def viewGeometry(self): - """return the screen geometry of the viewbox""" + """Return the screen geometry of the viewbox""" v = self.scene().views()[0] b = self.vb.mapRectToScene(self.vb.boundingRect()) wr = v.mapFromScene(b).boundingRect() @@ -514,7 +530,7 @@ class PlotItem(GraphicsWidget): self.replot() def addAvgCurve(self, curve): - """Add a single curve into the pool of curves averaged together""" + ## Add a single curve into the pool of curves averaged together ## If there are plot parameters, then we need to determine which to average together. remKeys = [] diff --git a/widgets/PlotWidget.py b/widgets/PlotWidget.py index 807e609f..fa9fcf1a 100644 --- a/widgets/PlotWidget.py +++ b/widgets/PlotWidget.py @@ -15,7 +15,13 @@ class PlotWidget(GraphicsView): #sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView - """Widget implementing a graphicsView with a single PlotItem inside.""" + """ + Widget implementing a graphicsView with a single PlotItem inside. + + The following methods are wrapped directly from PlotItem: addItem, removeItem, + clear, setXRange, setYRange, setRange, setAspectLocked, setMouseEnabled. For all + other methods, use getPlotItem. + """ def __init__(self, parent=None, **kargs): GraphicsView.__init__(self, parent) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) @@ -27,14 +33,7 @@ class PlotWidget(GraphicsView): setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged) - - #def __dtor__(self): - ##print "Called plotWidget sip destructor" - #self.quit() - - - #def quit(self): - + def close(self): self.plotItem.close() self.plotItem = None @@ -49,7 +48,7 @@ class PlotWidget(GraphicsView): if hasattr(m, '__call__'): return m raise exceptions.NameError(attr) - + def viewRangeChanged(self, view, range): #self.emit(QtCore.SIGNAL('viewChanged'), *args) self.sigRangeChanged.emit(self, range) @@ -64,6 +63,7 @@ class PlotWidget(GraphicsView): return self.plotItem.restoreState(state) def getPlotItem(self): + """Return the PlotItem contained within.""" return self.plotItem From 5c0c47cda25b5d3659528022603d92464b071763 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 15 Apr 2012 10:21:31 -0400 Subject: [PATCH 064/238] 3D mesh updates: - Can initialize GLMeshItem using MeshData instance - MeshData has save/restore methods --- functions.py | 2 ++ opengl/MeshData.py | 28 ++++++++++++++++++++++++++-- opengl/items/GLMeshItem.py | 7 +++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/functions.py b/functions.py index 70ade4a5..8cbea083 100644 --- a/functions.py +++ b/functions.py @@ -827,6 +827,8 @@ def isosurface(data, level): *data* 3D numpy array of scalar values *level* The level at which to generate an isosurface + Returns a list of faces; each face is a list of three vertexes and each vertex is a tuple of three floats. + This function is SLOW; plenty of room for optimization here. """ diff --git a/opengl/MeshData.py b/opengl/MeshData.py index b9f8f1ec..7ac24aeb 100644 --- a/opengl/MeshData.py +++ b/opengl/MeshData.py @@ -1,4 +1,5 @@ from pyqtgraph.Qt import QtGui +import pyqtgraph.functions as fn class MeshData(object): """ @@ -37,6 +38,12 @@ class MeshData(object): else: self._setIndexedFaces(faces, vertexes) + def setMeshColor(self, color): + """Set the color of the entire mesh. This removes any per-face or per-vertex colors.""" + color = fn.Color(color) + self._meshColor = color.glColor() + self._vertexColors = None + self._faceColors = None def _setUnindexedFaces(self, faces): verts = {} @@ -107,7 +114,8 @@ class MeshData(object): for i, face in enumerate(self._faces): ## compute face normal pts = [self._vertexes[vind] for vind in face] - norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]).normalized() + norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) + norm = norm / norm.length() ## don't use .normalized(); doesn't work for small values. self._faceNormals.append(norm) return self._faceNormals @@ -126,7 +134,7 @@ class MeshData(object): norm = QtGui.QVector3D() for fn in norms: norm += fn - norm.normalize() + norm = norm / norm.length() ## don't use .normalize(); doesn't work for small values. self._vertexNormals.append(norm) return self._vertexNormals @@ -152,3 +160,19 @@ class MeshData(object): """ pass + def save(self): + """Serialize this mesh to a string appropriate for disk storage""" + import pickle + names = ['_vertexes', '_edges', '_faces', '_vertexFaces', '_vertexNormals', '_faceNormals', '_vertexColors', '_edgeColors', '_faceColors', '_meshColor'] + state = {n:getattr(self, n) for n in names} + return pickle.dumps(state) + + def restore(self, state): + """Restore the state of a mesh previously saved using save()""" + import pickle + state = pickle.loads(state) + for k in state: + setattr(self, k, state[k]) + + + \ No newline at end of file diff --git a/opengl/items/GLMeshItem.py b/opengl/items/GLMeshItem.py index b82c4d3d..149baafb 100644 --- a/opengl/items/GLMeshItem.py +++ b/opengl/items/GLMeshItem.py @@ -20,8 +20,11 @@ class GLMeshItem(GLGraphicsItem): """ See MeshData for initialization arguments. """ - self.data = MeshData() - self.data.setFaces(faces, vertexes) + if isinstance(faces, MeshData): + self.data = faces + else: + self.data = MeshData() + self.data.setFaces(faces, vertexes) GLGraphicsItem.__init__(self) def initializeGL(self): From 61ea03618aa6a4de271a0553f5d2e3571ce8aaee Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 15 Apr 2012 10:56:41 -0400 Subject: [PATCH 065/238] re-enabled wheel events for PlotItem --- graphicsItems/PlotItem/PlotItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index cc0bac8c..04fd1afc 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -1221,9 +1221,9 @@ class PlotItem(GraphicsWidget): mode = False return mode - def wheelEvent(self, ev): - # disables default panning the whole scene by mousewheel - ev.accept() + #def wheelEvent(self, ev): + ## disables default panning the whole scene by mousewheel + #ev.accept() def resizeEvent(self, ev): if self.autoBtn is None: ## already closed down From dc29a9060e046df7b5686bf9284088a2891988e4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 15 Apr 2012 12:32:20 -0400 Subject: [PATCH 066/238] documentation updates --- documentation/source/functions.rst | 18 ++++++ documentation/source/internals.rst | 2 +- functions.py | 73 +++-------------------- graphicsItems/GraphicsLayout.py | 28 +++++++++ graphicsItems/PlotItem/PlotItem.py | 74 ++++++++++++++--------- graphicsItems/ViewBox/ViewBox.py | 95 ++++++++++++++++++++++++------ 6 files changed, 178 insertions(+), 112 deletions(-) diff --git a/documentation/source/functions.rst b/documentation/source/functions.rst index 3d56a4d9..ad43ca06 100644 --- a/documentation/source/functions.rst +++ b/documentation/source/functions.rst @@ -51,3 +51,21 @@ SI Unit Conversion Functions .. autofunction:: pyqtgraph.siEval + +Image Preparation Functions +--------------------------- + +.. autofunction:: pyqtgraph.makeARGB + +.. autofunction:: pyqtgraph.makeQImage + + +Mesh Generation Functions +------------------------- + +.. autofunction:: pyqtgraph.isocurve + +.. autofunction:: pyqtgraph.isosurface + + + diff --git a/documentation/source/internals.rst b/documentation/source/internals.rst index 3f25376d..8c1d246e 100644 --- a/documentation/source/internals.rst +++ b/documentation/source/internals.rst @@ -1,5 +1,5 @@ Internals - Extensions to Qt's GraphicsView -================================ +=========================================== * GraphicsView * GraphicsScene (mouse events) diff --git a/functions.py b/functions.py index 8cbea083..345b39d7 100644 --- a/functions.py +++ b/functions.py @@ -740,13 +740,13 @@ def rescaleData(data, scale, offset): def isocurve(data, level): """ - Generate isocurve from 2D data using marching squares algorithm. - - *data* 2D numpy array of scalar values - *level* The level at which to generate an isosurface - - This function is SLOW; plenty of room for optimization here. - """ + Generate isocurve from 2D data using marching squares algorithm. + + *data* 2D numpy array of scalar values + *level* The level at which to generate an isosurface + + This function is SLOW; plenty of room for optimization here. + """ sideTable = [ [], @@ -820,7 +820,7 @@ def isocurve(data, level): def isosurface(data, level): """ - Generate isosurface from volumetric data using marching tetrahedra algorithm. + Generate isosurface from volumetric data using marching cubes algorithm. See Paul Bourke, "Polygonising a Scalar Field" (http://local.wasp.uwa.edu.au/~pbourke/geometry/polygonise/) @@ -1196,60 +1196,3 @@ def isosurface(data, level): return facets -## code has moved to opengl/MeshData.py -#def meshNormals(data): - #""" - #Return list of normal vectors and list of faces which reference the normals - #data must be list of triangles; each triangle is a list of three points - #[ [(x,y,z), (x,y,z), (x,y,z)], ...] - #Return values are - #normals: [(x,y,z), ...] - #faces: [(n1, n2, n3), ...] - #""" - - #normals = [] - #points = {} - #for i, face in enumerate(data): - ### compute face normal - #pts = [QtGui.QVector3D(*x) for x in face] - #norm = QtGui.QVector3D.crossProduct(pts[1]-pts[0], pts[2]-pts[0]) - #normals.append(norm) - - ### remember each point was associated with this normal - #for p in face: - #p = tuple(map(lambda x: np.round(x, 8), p)) - #if p not in points: - #points[p] = [] - #points[p].append(i) - - ### compute averages - #avgLookup = {} - #avgNorms = [] - #for k,v in points.iteritems(): - #norms = [normals[i] for i in v] - #a = norms[0] - #if len(v) > 1: - #for n in norms[1:]: - #a = a + n - #a = a / len(v) - #avgLookup[k] = len(avgNorms) - #avgNorms.append(a) - - ### generate return array - #faces = [] - #for i, face in enumerate(data): - #f = [] - #for p in face: - #p = tuple(map(lambda x: np.round(x, 8), p)) - #f.append(avgLookup[p]) - #faces.append(tuple(f)) - - #return avgNorms, faces - - - - - - - - diff --git a/graphicsItems/GraphicsLayout.py b/graphicsItems/GraphicsLayout.py index 95d98b30..fb6554a7 100644 --- a/graphicsItems/GraphicsLayout.py +++ b/graphicsItems/GraphicsLayout.py @@ -6,6 +6,7 @@ __all__ = ['GraphicsLayout'] class GraphicsLayout(GraphicsWidget): """ Used for laying out GraphicsWidgets in a grid. + This is usually created automatically as part of a :class:`GraphicsWindow ` or :class:`GraphicsLayoutWidget `. """ @@ -33,29 +34,54 @@ class GraphicsLayout(GraphicsWidget): return self.currentCol-colspan def nextCol(self, *args, **kargs): + """Advance to next column for automatic item placement""" return self.nextColumn(*args, **kargs) def addPlot(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create a PlotItem and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`PlotItem.__init__ ` + Returns the created item. + """ plot = PlotItem(**kargs) self.addItem(plot, row, col, rowspan, colspan) return plot def addViewBox(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create a ViewBox and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`ViewBox.__init__ ` + Returns the created item. + """ vb = ViewBox(**kargs) self.addItem(vb, row, col, rowspan, colspan) return vb def addLabel(self, text=' ', row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create a LabelItem with *text* and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`LabelItem.__init__ ` + Returns the created item. + """ text = LabelItem(text, **kargs) self.addItem(text, row, col, rowspan, colspan) return text def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs): + """ + Create an empty GraphicsLayout and place it in the next available cell (or in the cell specified) + All extra keyword arguments are passed to :func:`GraphicsLayout.__init__ ` + Returns the created item. + """ layout = GraphicsLayout(**kargs) self.addItem(layout, row, col, rowspan, colspan) return layout def addItem(self, item, row=None, col=None, rowspan=1, colspan=1): + """ + Add an item to the layout and place it in the next available cell (or in the cell specified). + The item must be an instance of a QGraphicsWidget subclass. + """ if row is None: row = self.currentRow if col is None: @@ -69,6 +95,7 @@ class GraphicsLayout(GraphicsWidget): self.layout.addItem(item, row, col, rowspan, colspan) def getItem(self, row, col): + """Return the item in (*row*, *col*)""" return self.row[row][col] def boundingRect(self): @@ -89,6 +116,7 @@ class GraphicsLayout(GraphicsWidget): raise Exception("Could not determine index of item " + str(item)) def removeItem(self, item): + """Remove *item* from the layout.""" ind = self.itemIndex(item) self.layout.removeAt(ind) self.scene().removeItem(item) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 04fd1afc..7d3a2b2d 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -55,24 +55,35 @@ except: class PlotItem(GraphicsWidget): - sigYRangeChanged = QtCore.Signal(object, object) - sigXRangeChanged = QtCore.Signal(object, object) - sigRangeChanged = QtCore.Signal(object, object) - """ Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox. - Use plot(...) to create a new PlotDataItem and add it to the view. - Use addItem(...) add any QGraphicsItem to the view + Use :func:`plot() ` to create a new PlotDataItem and add it to the view. + Use :func:`addItem() ` to add any QGraphicsItem to the view This class wraps several methods from its internal ViewBox: - setXRange, setYRange, setXLink, setYLink, - setRange, autoRange, viewRect, setMouseEnabled, - enableAutoRange, disableAutoRange, setAspectLocked, - register, unregister. + :func:`setXRange `, + :func:`setYRange `, + :func:`setRange `, + :func:`autoRange `, + :func:`setXLink `, + :func:`setYLink `, + :func:`viewRect `, + :func:`setMouseEnabled `, + :func:`enableAutoRange `, + :func:`disableAutoRange `, + :func:`setAspectLocked `, + :func:`register `, + :func:`unregister ` - The ViewBox itself can be accessed by calling getVewBox() + The ViewBox itself can be accessed by calling :func:`getViewBox() ` """ + + sigRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox range has changed + sigYRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox Y range has changed + sigXRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox X range has changed + + lastFileDir = None managers = {} @@ -81,14 +92,18 @@ class PlotItem(GraphicsWidget): Create a new PlotItem. All arguments are optional. Any extra keyword arguments are passed to PlotItem.plot(). - Arguments: - *title* - Title to display at the top of the item. Html is allowed. - *labels* - A dictionary specifying the axis labels to display. - {'left': (args), 'bottom': (args), ...} - The name of each axis and the corresponding arguments are passed to PlotItem.setLabel() - Optionally, PlotItem my also be initialized with the keyword arguments left, - right, top, or bottom to achieve the same effect. - *name* - Registers a name for this view so that others may link to it + ============= ========================================================================================== + **Arguments** + *title* Title to display at the top of the item. Html is allowed. + *labels* A dictionary specifying the axis labels to display:: + + {'left': (args), 'bottom': (args), ...} + + The name of each axis and the corresponding arguments are passed to PlotItem.setLabel() + Optionally, PlotItem my also be initialized with the keyword arguments left, + right, top, or bottom to achieve the same effect. + *name* Registers a name for this view so that others may link to it + ============= ========================================================================================== """ @@ -165,7 +180,7 @@ class PlotItem(GraphicsWidget): 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setRange', 'autoRange', 'viewRect', 'setMouseEnabled', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', - 'register', 'unregister']: + 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. setattr(self, m, getattr(self.vb, m)) self.items = [] @@ -779,7 +794,7 @@ class PlotItem(GraphicsWidget): def plot(self, *args, **kargs): """ Add and return a new plot. - See PlotDataItem.__init__ for data arguments + See :func:`PlotDataItem.__init__ ` for data arguments Extra allowed arguments are: clear - clear all plots before displaying new data @@ -1262,13 +1277,16 @@ class PlotItem(GraphicsWidget): def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args): """ Set the label for an axis. Basic HTML formatting is allowed. - Arguments: - axis - must be one of 'left', 'bottom', 'right', or 'top' - text - text to display along the axis. HTML allowed. - units - units to display after the title. If units are given, - then an SI prefix will be automatically appended - and the axis values will be scaled accordingly. - (ie, use 'V' instead of 'mV'; 'm' will be added automatically) + + ============= ================================================================= + **Arguments** + axis must be one of 'left', 'bottom', 'right', or 'top' + text text to display along the axis. HTML allowed. + units units to display after the title. If units are given, + then an SI prefix will be automatically appended + and the axis values will be scaled accordingly. + (ie, use 'V' instead of 'mV'; 'm' will be added automatically) + ============= ================================================================= """ self.getScale(axis).setLabel(text=text, units=units, **args) diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index c43b9764..990c4100 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -28,6 +28,15 @@ class ChildGroup(ItemGroup): class ViewBox(GraphicsWidget): """ Box that allows internal scaling/panning of children by mouse drag. + This class is usually created automatically as part of a :class:`PlotItem ` or :class:`Canvas ` or with :func:`GraphicsLayout.addViewBox() `. + + Features: + + - Scaling contents by mouse or auto-scale when contents change + - View linking--multiple views display the same data ranges + - Configurable by context menu + - Item coordinate mapping methods + Not really compatible with GraphicsView having the same functionality. """ @@ -53,6 +62,21 @@ class ViewBox(GraphicsWidget): def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, name=None): + """ + ============= ============================================================= + **Arguments** + *parent* (QGraphicsWidget) Optional parent widget + *border* (QPen) Do draw a border around the view, give any + single argument accepted by :func:`mkPen ` + *lockAspect* (False or float) The aspect ratio to lock the view + coorinates to. (or False to allow the ratio to change) + *enableMouse* (bool) Whether mouse can be used to scale/pan the view + *invertY* (bool) See :func:`invertY ` + ============= ============================================================= + """ + + + GraphicsWidget.__init__(self, parent) self.name = None self.linksBlocked = False @@ -112,7 +136,7 @@ class ViewBox(GraphicsWidget): self.setAspectLocked(lockAspect) - self.border = border + self.border = fn.mkPen(border) self.menu = ViewBoxMenu(self) self.register(name) @@ -134,6 +158,9 @@ class ViewBox(GraphicsWidget): ViewBox.updateAllViewLists() def unregister(self): + """ + Remove this ViewBox forom the list of linkable views. (see :func:`register() `) + """ del ViewBox.AllViews[self] if self.name is not None: del ViewBox.NamedViews[self.name] @@ -165,6 +192,11 @@ class ViewBox(GraphicsWidget): def setMouseMode(self, mode): + """ + Set the mouse interaction mode. *mode* must be either ViewBox.PanMode or ViewBox.RectMode. + In PanMode, the left mouse button pans the view and the right button scales. + In RectMode, the left button draws a rectangle which updates the visible region (this mode is more suitable for single-button mice) + """ if mode not in [ViewBox.PanMode, ViewBox.RectMode]: raise Exception("Mode must be ViewBox.PanMode or ViewBox.RectMode") self.state['mouseMode'] = mode @@ -188,6 +220,10 @@ class ViewBox(GraphicsWidget): return self.childGroup def setMouseEnabled(self, x=None, y=None): + """ + Set whether each axis is enabled for mouse interaction. *x*, *y* arguments must be True or False. + This allows the user to pan/scale one axis of the view while leaving the other axis unchanged. + """ if x is not None: self.state['mouseEnabled'][0] = x if y is not None: @@ -198,6 +234,10 @@ class ViewBox(GraphicsWidget): return self.state['mouseEnabled'][:] def addItem(self, item, ignoreBounds=False): + """ + Add a QGraphicsItem to this view. The view will include this item when determining how to set its range + automatically unless *ignoreBounds* is True. + """ if item.zValue() < self.zValue(): item.setZValue(self.zValue()+1) item.setParentItem(self.childGroup) @@ -207,6 +247,7 @@ class ViewBox(GraphicsWidget): #print "addItem:", item, item.boundingRect() def removeItem(self, item): + """Remove an item from this view.""" try: self.addedItems.remove(item) except: @@ -223,6 +264,7 @@ class ViewBox(GraphicsWidget): #self.linkedYChanged() def viewRange(self): + """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" return [x[:] for x in self.state['viewRange']] ## return copy def viewRect(self): @@ -261,12 +303,14 @@ class ViewBox(GraphicsWidget): Set the visible range of the ViewBox. Must specify at least one of *range*, *xRange*, or *yRange*. - Arguments: - *rect* (QRectF) - The full range that should be visible in the view box. - *xRange* (min,max) - The range that should be visible along the x-axis. - *yRange* (min,max) - The range that should be visible along the y-axis. - *padding* (float) - Expand the view by a fraction of the requested range - By default, this value is 0.02 (2%) + ============= ===================================================================== + **Arguments** + *rect* (QRectF) The full range that should be visible in the view box. + *xRange* (min,max) The range that should be visible along the x-axis. + *yRange* (min,max) The range that should be visible along the y-axis. + *padding* (float) Expand the view by a fraction of the requested range. + By default, this value is 0.02 (2%) + ============= ===================================================================== """ changes = {} @@ -326,14 +370,24 @@ class ViewBox(GraphicsWidget): def setYRange(self, min, max, padding=0.02, update=True): + """ + Set the visible Y range of the view to [*min*, *max*]. + The *padding* argument causes the range to be set larger by the fraction specified. + """ self.setRange(yRange=[min, max], update=update, padding=padding) def setXRange(self, min, max, padding=0.02, update=True): + """ + Set the visible X range of the view to [*min*, *max*]. + The *padding* argument causes the range to be set larger by the fraction specified. + """ self.setRange(xRange=[min, max], update=update, padding=padding) def autoRange(self, padding=0.02): """ Set the range of the view box to make all children visible. + Note that this is not the same as enableAutoRange, which causes the view to + automatically auto-range whenever its contents are changed. """ bounds = self.childrenBoundingRect() if bounds is not None: @@ -404,6 +458,7 @@ class ViewBox(GraphicsWidget): self.sigStateChanged.emit(self) def disableAutoRange(self, axis=None): + """Disables auto-range. (See enableAutoRange)""" self.enableAutoRange(axis, enable=False) def autoRangeEnabled(self): @@ -433,9 +488,11 @@ class ViewBox(GraphicsWidget): self.setRange(tr, padding=0, disableAutoRange=False) def setXLink(self, view): + """Link this view's X axis to another view. (see LinkView)""" self.linkView(self.XAxis, view) def setYLink(self, view): + """Link this view's Y axis to another view. (see LinkView)""" self.linkView(self.YAxis, view) @@ -610,10 +667,12 @@ class ViewBox(GraphicsWidget): return self.mapToScene(self.mapFromView(obj)) def mapFromItemToView(self, item, obj): + """Maps *obj* from the local coordinate system of *item* to the view coordinates""" return self.childGroup.mapFromItem(item, obj) #return self.mapSceneToView(item.mapToScene(obj)) def mapFromViewToItem(self, item, obj): + """Maps *obj* from view coordinates to the local coordinate system of *item*.""" return self.childGroup.mapToItem(item, obj) #return item.mapFromScene(self.mapViewToScene(obj)) @@ -963,19 +1022,19 @@ class ViewBox(GraphicsWidget): #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) p.drawPath(bounds) - def saveSvg(self): - pass + #def saveSvg(self): + #pass - def saveImage(self): - pass + #def saveImage(self): + #pass - def savePrint(self): - printer = QtGui.QPrinter() - if QtGui.QPrintDialog(printer).exec_() == QtGui.QDialog.Accepted: - p = QtGui.QPainter(printer) - p.setRenderHint(p.Antialiasing) - self.scene().render(p) - p.end() + #def savePrint(self): + #printer = QtGui.QPrinter() + #if QtGui.QPrintDialog(printer).exec_() == QtGui.QDialog.Accepted: + #p = QtGui.QPainter(printer) + #p.setRenderHint(p.Antialiasing) + #self.scene().render(p) + #p.end() def updateViewLists(self): def cmpViews(a, b): From c44887e531fc072a790f1e706c18e343624f2b49 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sun, 15 Apr 2012 12:33:56 -0400 Subject: [PATCH 067/238] bugfix --- graphicsItems/ViewBox/ViewBox.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 990c4100..d517f7da 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -103,11 +103,11 @@ class ViewBox(GraphicsWidget): } - self.exportMethods = collections.OrderedDict([ - ('SVG', self.saveSvg), - ('Image', self.saveImage), - ('Print', self.savePrint), - ]) + #self.exportMethods = collections.OrderedDict([ + #('SVG', self.saveSvg), + #('Image', self.saveImage), + #('Print', self.savePrint), + #]) self.setFlag(self.ItemClipsChildrenToShape) self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses From 44f2a0ecc44acc05bad85044211b188b647f4a8f Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 16 Apr 2012 16:45:55 -0400 Subject: [PATCH 068/238] Documentation updates --- documentation/source/graphicsItems/index.rst | 5 +- .../source/graphicsItems/plotitem.rst | 4 +- ...GraphicsItemMethods.py => GraphicsItem.py} | 7 +- graphicsItems/GraphicsObject.py | 18 ++-- graphicsItems/GraphicsWidget.py | 11 ++- graphicsItems/ImageItem.py | 96 +++++++++++++++---- graphicsItems/LinearRegionItem.py | 10 ++ graphicsItems/PlotDataItem.py | 39 ++++++-- graphicsItems/PlotItem/PlotItem.py | 21 +++- 9 files changed, 157 insertions(+), 54 deletions(-) rename graphicsItems/{GraphicsItemMethods.py => GraphicsItem.py} (97%) diff --git a/documentation/source/graphicsItems/index.rst b/documentation/source/graphicsItems/index.rst index 46f5a938..09d92893 100644 --- a/documentation/source/graphicsItems/index.rst +++ b/documentation/source/graphicsItems/index.rst @@ -10,8 +10,6 @@ Contents: :maxdepth: 2 plotdataitem - plotcurveitem - scatterplotitem plotitem imageitem viewbox @@ -19,6 +17,8 @@ Contents: infiniteline roi graphicslayout + plotcurveitem + scatterplotitem axisitem arrowitem curvepoint @@ -33,5 +33,6 @@ Contents: buttonitem graphicsobject graphicswidget + graphicsitem uigraphicsitem diff --git a/documentation/source/graphicsItems/plotitem.rst b/documentation/source/graphicsItems/plotitem.rst index cbf5f9f4..60cedf60 100644 --- a/documentation/source/graphicsItems/plotitem.rst +++ b/documentation/source/graphicsItems/plotitem.rst @@ -1,7 +1,7 @@ PlotItem ======== -.. autoclass:: pyqtgraph.PlotItem +.. autoclass:: pyqtgraph.PlotItem() :members: - + .. automethod:: pyqtgraph.PlotItem.__init__ diff --git a/graphicsItems/GraphicsItemMethods.py b/graphicsItems/GraphicsItem.py similarity index 97% rename from graphicsItems/GraphicsItemMethods.py rename to graphicsItems/GraphicsItem.py index ca9c7a43..f8cb93cf 100644 --- a/graphicsItems/GraphicsItemMethods.py +++ b/graphicsItems/GraphicsItem.py @@ -3,9 +3,12 @@ from pyqtgraph.GraphicsScene import GraphicsScene from pyqtgraph.Point import Point import weakref -class GraphicsItemMethods(object): +class GraphicsItem(object): """ - Class providing useful methods to GraphicsObject and GraphicsWidget. + **Bases:** :class:`object` + + Abstract class providing useful methods to GraphicsObject and GraphicsWidget. + (This is required because we cannot have multiple inheritance with QObject subclasses.) """ def __init__(self): self._viewWidget = None diff --git a/graphicsItems/GraphicsObject.py b/graphicsItems/GraphicsObject.py index af727315..60acc670 100644 --- a/graphicsItems/GraphicsObject.py +++ b/graphicsItems/GraphicsObject.py @@ -1,19 +1,15 @@ from pyqtgraph.Qt import QtGui, QtCore -from GraphicsItemMethods import GraphicsItemMethods +from GraphicsItem import GraphicsItem __all__ = ['GraphicsObject'] -class GraphicsObject(GraphicsItemMethods, QtGui.QGraphicsObject): - """Extends QGraphicsObject with a few important functions. - (Most of these assume that the object is in a scene with a single view) - - This class also generates a cache of the Qt-internal addresses of each item - so that GraphicsScene.items() can return the correct objects (this is a PyQt bug) - - Note: most of the extended functionality is inherited from GraphicsItemMethods, - which is shared between GraphicsObject and GraphicsWidget. +class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): + """ + **Bases:** :class:`GraphicsItem `, :class:`QtGui.QGraphicsObject` + + Extension of QGraphicsObject with some useful methods (provided by :class:`GraphicsItem `) """ def __init__(self, *args): QtGui.QGraphicsObject.__init__(self, *args) - GraphicsItemMethods.__init__(self) + GraphicsItem.__init__(self) diff --git a/graphicsItems/GraphicsWidget.py b/graphicsItems/GraphicsWidget.py index 0181ea17..9f4c5480 100644 --- a/graphicsItems/GraphicsWidget.py +++ b/graphicsItems/GraphicsWidget.py @@ -1,16 +1,19 @@ from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.GraphicsScene import GraphicsScene -from GraphicsItemMethods import GraphicsItemMethods +from GraphicsItem import GraphicsItem __all__ = ['GraphicsWidget'] -class GraphicsWidget(GraphicsItemMethods, QtGui.QGraphicsWidget): + +class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget): def __init__(self, *args, **kargs): """ + **Bases:** :class:`GraphicsItem `, :class:`QtGui.QGraphicsWidget` + Extends QGraphicsWidget with several helpful methods and workarounds for PyQt bugs. - Most of the extra functionality is inherited from GraphicsObjectSuperclass. + Most of the extra functionality is inherited from :class:`GraphicsItem `. """ QtGui.QGraphicsWidget.__init__(self, *args, **kargs) - GraphicsItemMethods.__init__(self) + GraphicsItem.__init__(self) GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() #def getMenu(self): diff --git a/graphicsItems/ImageItem.py b/graphicsItems/ImageItem.py index 423183ab..535d0281 100644 --- a/graphicsItems/ImageItem.py +++ b/graphicsItems/ImageItem.py @@ -12,8 +12,19 @@ from GraphicsObject import GraphicsObject __all__ = ['ImageItem'] class ImageItem(GraphicsObject): """ - GraphicsObject displaying an image. Optimized for rapid update (ie video display) + **Bases:** :class:`GraphicsObject ` + GraphicsObject displaying an image. Optimized for rapid update (ie video display). + This item displays either a 2D numpy array (height, width) or + a 3D array (height, width, RGBa). This array is optionally scaled (see + :func:`setLevels `) and/or colored + with a lookup table (see :func:`setLookupTable `) + before being displayed. + + ImageItem is frequently used in conjunction with + :class:`HistogramLUTItem ` or + :class:`HistogramLUTWidget ` to provide a GUI + for controlling the levels and lookup table used to display the image. """ @@ -24,7 +35,7 @@ class ImageItem(GraphicsObject): def __init__(self, image=None, **kargs): """ - See setImage for all allowed arguments. + See :func:`setImage ` for all allowed initialization arguments. """ GraphicsObject.__init__(self) #self.pixmapItem = QtGui.QGraphicsPixmapItem(self) @@ -51,6 +62,21 @@ class ImageItem(GraphicsObject): self.setOpts(**kargs) def setCompositionMode(self, mode): + """Change the composition mode of the item (see QPainter::CompositionMode + in the Qt documentation). This is useful when overlaying multiple ImageItems. + + ============================================ ============================================================ + **Most common arguments:** + QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it + is opaque. Otherwise, it uses the alpha channel to blend + the image with the background. + QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to + reflect the lightness or darkness of the background. + QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels + are added together. + QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. + ============================================ ============================================================ + """ self.paintMode = mode self.update() @@ -90,10 +116,13 @@ class ImageItem(GraphicsObject): def setLevels(self, levels, update=True): """ - Set image scaling levels. Can be one of: - [blackLevel, whiteLevel] - [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]] - Only the first format is compatible with lookup tables. + Set image scaling levels. Can be one of: + + * [blackLevel, whiteLevel] + * [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]] + + Only the first format is compatible with lookup tables. See :func:`makeARGB ` + for more details on how levels are applied. """ self.levels = levels if update: @@ -105,8 +134,14 @@ class ImageItem(GraphicsObject): def setLookupTable(self, lut, update=True): """ - Set the lookup table to use for this image. (see functions.makeARGB for more information on how this is used) - Optionally, lut can be a callable that accepts the current image as an argument and returns the lookup table to use.""" + Set the lookup table (numpy array) to use for this image. (see + :func:`makeARGB ` for more information on how this is used). + Optionally, lut can be a callable that accepts the current image as an + argument and returns the lookup table to use. + + Ordinarily, this table is supplied by a :class:`HistogramLUTItem ` + or :class:`GradientEditorItem `. + """ self.lut = lut if update: self.updateImage() @@ -126,23 +161,35 @@ class ImageItem(GraphicsObject): self.setBorder(kargs['border']) def setRect(self, rect): - """Scale and translate the image to fit within rect.""" + """Scale and translate the image to fit within rect (must be a QRect or QRectF).""" self.resetTransform() self.translate(rect.left(), rect.top()) self.scale(rect.width() / self.width(), rect.height() / self.height()) def setImage(self, image=None, autoLevels=None, **kargs): """ - Update the image displayed by this item. - Arguments: - image - autoLevels - lut - levels - opacity - compositionMode - border + Update the image displayed by this item. For more information on how the image + is processed before displaying, see :func:`makeARGB ` + ================= ========================================================================= + **Arguments:** + image (numpy array) Specifies the image data. May be 2D (width, height) or + 3D (width, height, RGBa). The array dtype must be integer or floating + point of any bit depth. For 3D arrays, the third dimension must + be of length 3 (RGB) or 4 (RGBA). + autoLevels (bool) If True, this forces the image to automatically select + levels based on the maximum and minimum values in the data. + By default, this argument is true unless the levels argument is + given. + lut (numpy array) The color lookup table to use when displaying the image. + See :func:`setLookupTable `. + levels (min, max) The minimum and maximum values to use when rescaling the image + data. By default, this will be set to the minimum and maximum values + in the image. If the image array has dtype uint8, no rescaling is necessary. + opacity (float 0.0-1.0) + compositionMode see :func:`setCompositionMode ` + border Sets the pen used when drawing the image border. Default is None. + ================= ========================================================================= """ prof = debug.Profiler('ImageItem.setImage', disabled=True) @@ -238,8 +285,10 @@ class ImageItem(GraphicsObject): def getHistogram(self, bins=500, step=3): - """returns x and y arrays containing the histogram values for the current image. - The step argument causes pixels to be skipped when computing the histogram to save time.""" + """Returns x and y arrays containing the histogram values for the current image. + The step argument causes pixels to be skipped when computing the histogram to save time. + This method is also used when automatically computing levels. + """ if self.image is None: return None,None stepData = self.image[::step, ::step] @@ -247,7 +296,12 @@ class ImageItem(GraphicsObject): return hist[1][:-1], hist[0] def setPxMode(self, b): - """Set whether the item ignores transformations and draws directly to screen pixels.""" + """ + Set whether the item ignores transformations and draws directly to screen pixels. + If True, the item will not inherit any scale or rotation transformations from its + parent items, but its position will be transformed as usual. + (see GraphicsItem::ItemIgnoresTransformations in the Qt documentation) + """ self.setFlag(self.ItemIgnoresTransformations, b) def setScaledMode(self): diff --git a/graphicsItems/LinearRegionItem.py b/graphicsItems/LinearRegionItem.py index bdffd075..ef1c1b8e 100644 --- a/graphicsItems/LinearRegionItem.py +++ b/graphicsItems/LinearRegionItem.py @@ -8,8 +8,18 @@ __all__ = ['LinearRegionItem'] class LinearRegionItem(UIGraphicsItem): """ + **Bases:** :class:`UIGraphicsItem ` + Used for marking a horizontal or vertical region in plots. The region can be dragged and is bounded by lines which can be dragged individually. + + =============================== ============================================================================= + **Signals:** + sigRegionChangeFinished(self) Emitted when the user has finished dragging the region (or one of its lines) + and when the region is changed programatically. + sigRegionChanged(self) Emitted while the user is dragging the region (or one of its lines) + and when the region is changed programatically. + =============================== ============================================================================= """ sigRegionChangeFinished = QtCore.Signal(object) diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index 22c62d1a..44493799 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -14,7 +14,22 @@ import pyqtgraph.functions as fn import pyqtgraph.debug as debug class PlotDataItem(GraphicsObject): - """GraphicsItem for displaying plot curves, scatter plots, or both.""" + """ + **Bases:** :class:`GraphicsObject ` + + GraphicsItem for displaying plot curves, scatter plots, or both. + While it is possible to use :class:`PlotCurveItem ` or + :class:`ScatterPlotItem ` individually, this class + provides a unified interface to both. Inspances of :class:`PlotDataItem` are + usually created by plot() methods such as :func:`pyqtgraph.plot` and + :func:`PlotItem.plot() `. + + ===================== ============================================== + **Signals:** + sigPlotChanged(self) Emitted when the data in this item is updated. + sigClicked(self) Emitted when the item is clicked. + ===================== ============================================== + """ sigPlotChanged = QtCore.Signal(object) sigClicked = QtCore.Signal(object) @@ -23,7 +38,7 @@ class PlotDataItem(GraphicsObject): """ There are many different ways to create a PlotDataItem: - Data initialization: (x,y data only) + **Data initialization arguments:** (x,y data only) =================================== ====================================== PlotDataItem(xValues, yValues) x and y values may be any sequence (including ndarray) of real numbers @@ -32,7 +47,7 @@ class PlotDataItem(GraphicsObject): PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where x=data[:,0] and y=data[:,1] =================================== ====================================== - Data initialization: (x,y data AND may include spot style) + **Data initialization arguments:** (x,y data AND may include spot style) =========================== ========================================= PlotDataItem(recarray) numpy array with dtype=[('x', float), ('y', float), ...] @@ -42,34 +57,40 @@ class PlotDataItem(GraphicsObject): OR 2D array with a column 'y' and extra columns as needed. =========================== ========================================= - Line style keyword + **Line style keyword arguments:** ========== ================================================ - pen pen to use for drawing line between points. Default is solid grey, 1px width. Use None to disable line drawing. + pen pen to use for drawing line between points. + Default is solid grey, 1px width. Use None to disable line drawing. + May be any single argument accepted by :func:`mkPen() ` shadowPen pen for secondary line to draw behind the primary line. disabled by default. + May be any single argument accepted by :func:`mkPen() ` fillLevel fill the area between the curve and fillLevel fillBrush fill to use when fillLevel is specified + May be any single argument accepted by :func:`mkBrush() ` ========== ================================================ - Point style keyword arguments: + **Point style keyword arguments:** ============ ================================================ - symbol symbol to use for drawing points OR list of symbols, one per point. Default is no symbol. + symbol (str) symbol to use for drawing points OR list of symbols, one per point. Default is no symbol. options are o, s, t, d, + symbolPen outline pen for drawing points OR list of pens, one per point + May be any single argument accepted by :func:`mkPen() ` symbolBrush brush for filling points OR list of brushes, one per point + May be any single argument accepted by :func:`mkBrush() ` symbolSize diameter of symbols OR list of diameters pxMode (bool) If True, then symbolSize is specified in pixels. If False, then symbolSize is specified in data coordinates. ============ ================================================ - Optimization keyword arguments: + **Optimization keyword arguments:** ========== ================================================ identical spots are all identical. The spot image will be rendered only once and repeated for every point decimate (int) decimate data ========== ================================================ - Meta-info keyword arguments: + **Meta-info keyword arguments:** ========== ================================================ name name of dataset. This would appear in a legend diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index 7d3a2b2d..f1bd6313 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -56,10 +56,12 @@ except: class PlotItem(GraphicsWidget): """ - Plot graphics item that can be added to any graphics scene. Implements axis titles, scales, interactive viewbox. + **Bases:** :class:`GraphicsWidget ` + Plot graphics item that can be added to any graphics scene. Implements axes, titles, and interactive viewbox. + PlotItem also provides some basic analysis functionality that may be accessed from the context menu. Use :func:`plot() ` to create a new PlotDataItem and add it to the view. - Use :func:`addItem() ` to add any QGraphicsItem to the view + Use :func:`addItem() ` to add any QGraphicsItem to the view. This class wraps several methods from its internal ViewBox: :func:`setXRange `, @@ -77,6 +79,13 @@ class PlotItem(GraphicsWidget): :func:`unregister ` The ViewBox itself can be accessed by calling :func:`getViewBox() ` + + ==================== ======================================================================= + **Signals** + sigYRangeChanged wrapped from :class:`ViewBox ` + sigXRangeChanged wrapped from :class:`ViewBox ` + sigRangeChanged wrapped from :class:`ViewBox ` + ==================== ======================================================================= """ sigRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox range has changed @@ -99,7 +108,8 @@ class PlotItem(GraphicsWidget): {'left': (args), 'bottom': (args), ...} - The name of each axis and the corresponding arguments are passed to PlotItem.setLabel() + The name of each axis and the corresponding arguments are passed to + :func:`PlotItem.setLabel() ` Optionally, PlotItem my also be initialized with the keyword arguments left, right, top, or bottom to achieve the same effect. *name* Registers a name for this view so that others may link to it @@ -720,6 +730,11 @@ class PlotItem(GraphicsWidget): ##self.replot() def addItem(self, item, *args, **kargs): + """ + Add a graphics item to the view box. + If the item has plot data (PlotDataItem, PlotCurveItem, ScatterPlotItem), it may + be included in analysis performed by the PlotItem. + """ self.items.append(item) vbargs = {} if 'ignoreBounds' in kargs: From d8624f565b4247924ce7d7f8e50095e364c4edfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingo=20Bre=C3=9Fler?= Date: Mon, 16 Apr 2012 23:15:25 +0200 Subject: [PATCH 069/238] PlotItem.addCurve: fixed typo --- graphicsItems/PlotItem/PlotItem.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index f1bd6313..1513be77 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -776,8 +776,7 @@ class PlotItem(GraphicsWidget): def addCurve(self, c, params=None): print "PlotItem.addCurve is deprecated. Use addItem instead." - self.addItem(item, params) - + self.addItem(c, params) def removeItem(self, item): if not item in self.items: From 4eadccdcc19214d161781c8880077e3fa2901510 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 18 Apr 2012 00:02:15 -0400 Subject: [PATCH 070/238] documentation updates --- GraphicsScene/GraphicsScene.py | 53 +-- GraphicsScene/mouseEvents.py | 121 ++++++- __init__.py | 16 +- documentation/source/apireference.rst | 1 + .../source/graphicsscene/graphicsscene.rst | 8 + .../source/graphicsscene/hoverevent.rst | 5 + documentation/source/graphicsscene/index.rst | 12 + .../source/graphicsscene/mouseclickevent.rst | 5 + .../source/graphicsscene/mousedragevent.rst | 5 + documentation/source/how_to_use.rst | 4 +- documentation/source/introduction.rst | 4 +- graphicsItems/PlotItem/PlotItem.py | 6 + imageview/ImageView.py | 327 ++++++++++-------- widgets/PlotWidget.py | 30 +- 14 files changed, 409 insertions(+), 188 deletions(-) create mode 100644 documentation/source/graphicsscene/graphicsscene.rst create mode 100644 documentation/source/graphicsscene/hoverevent.rst create mode 100644 documentation/source/graphicsscene/index.rst create mode 100644 documentation/source/graphicsscene/mouseclickevent.rst create mode 100644 documentation/source/graphicsscene/mousedragevent.rst diff --git a/GraphicsScene/GraphicsScene.py b/GraphicsScene/GraphicsScene.py index 2bcf504e..cd3c2350 100644 --- a/GraphicsScene/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -23,36 +23,37 @@ class GraphicsScene(QtGui.QGraphicsScene): events, but this turned out to be impossible because the constructor for QGraphicsMouseEvent is private) - - Generates MouseClicked events in addition to the usual press/move/release events. + * Generates MouseClicked events in addition to the usual press/move/release events. (This works around a problem where it is impossible to have one item respond to a drag if another is watching for a click.) - - Adjustable radius around click that will catch objects so you don't have to click *exactly* over small/thin objects - - Global context menu--if an item implements a context menu, then its parent(s) may also add items to the menu. - - Allows items to decide _before_ a mouse click which item will be the recipient of mouse events. + * Adjustable radius around click that will catch objects so you don't have to click *exactly* over small/thin objects + * Global context menu--if an item implements a context menu, then its parent(s) may also add items to the menu. + * Allows items to decide _before_ a mouse click which item will be the recipient of mouse events. This lets us indicate unambiguously to the user which item they are about to click/drag on - - Eats mouseMove events that occur too soon after a mouse press. - - Reimplements items() and itemAt() to circumvent PyQt bug + * Eats mouseMove events that occur too soon after a mouse press. + * Reimplements items() and itemAt() to circumvent PyQt bug Mouse interaction is as follows: + 1) Every time the mouse moves, the scene delivers both the standard hoverEnter/Move/LeaveEvents as well as custom HoverEvents. 2) Items are sent HoverEvents in Z-order and each item may optionally call event.acceptClicks(button), acceptDrags(button) or both. If this method call returns True, this informs the item that _if_ the user clicks/drags the specified mouse button, the item is guaranteed to be the recipient of click/drag events (the item may wish to change its appearance to indicate this). - If the call to acceptClicks/Drags returns False, then the item is guaranteed to NOT receive + If the call to acceptClicks/Drags returns False, then the item is guaranteed to *not* receive the requested event (because another item has already accepted it). 3) If the mouse is clicked, a mousePressEvent is generated as usual. If any items accept this press event, then No click/drag events will be generated and mouse interaction proceeds as defined by Qt. This allows items to function properly if they are expecting the usual press/move/release sequence of events. (It is recommended that items do NOT accept press events, and instead use click/drag events) - Note: The default implementation of QGraphicsItem.mousePressEvent will ACCEPT the event if the + Note: The default implementation of QGraphicsItem.mousePressEvent will *accept* the event if the item is has its Selectable or Movable flags enabled. You may need to override this behavior. - 3) If no item accepts the mousePressEvent, then the scene will begin delivering mouseDrag and/or mouseClick events. + 4) If no item accepts the mousePressEvent, then the scene will begin delivering mouseDrag and/or mouseClick events. If the mouse is moved a sufficient distance (or moved slowly enough) before the button is released, then a mouseDragEvent is generated. If no drag events are generated before the button is released, then a mouseClickEvent is generated. - 4) Click/drag events are delivered to the item that called acceptClicks/acceptDrags on the HoverEvent + 5) Click/drag events are delivered to the item that called acceptClicks/acceptDrags on the HoverEvent in step 1. If no such items exist, then the scene attempts to deliver the events to items near the event. ClickEvents may be delivered in this way even if no item originally claimed it could accept the click. DragEvents may only be delivered this way if it is the initial @@ -470,23 +471,25 @@ class GraphicsScene(QtGui.QGraphicsScene): The final menu will look like: - Original Item 1 - Original Item 2 - ... - Original Item N - ------------------ - Parent Item 1 - Parent Item 2 - ... - Grandparent Item 1 - ... + | Original Item 1 + | Original Item 2 + | ... + | Original Item N + | ------------------ + | Parent Item 1 + | Parent Item 2 + | ... + | Grandparent Item 1 + | ... - Arguments: - item - The item that initially created the context menu - (This is probably the item making the call to this function) - menu - The context menu being shown by the item - event - The original event that triggered the menu to appear. + ============== ================================================== + **Arguments:** + item The item that initially created the context menu + (This is probably the item making the call to this function) + menu The context menu being shown by the item + event The original event that triggered the menu to appear. + ============== ================================================== """ #items = self.itemsNearEvent(ev) diff --git a/GraphicsScene/mouseEvents.py b/GraphicsScene/mouseEvents.py index eb21229a..ce991c84 100644 --- a/GraphicsScene/mouseEvents.py +++ b/GraphicsScene/mouseEvents.py @@ -4,6 +4,13 @@ import weakref import pyqtgraph.ptime as ptime class MouseDragEvent: + """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their mouseDragEvent() method when the item is being mouse-dragged. + + """ + + + def __init__(self, moveEvent, pressEvent, lastEvent, start=False, finish=False): self.start = start self.finish = finish @@ -27,59 +34,99 @@ class MouseDragEvent: self._modifiers = moveEvent.modifiers() def accept(self): + """An item should call this method if it can handle the event. This will prevent the event being delivered to any other items.""" self.accepted = True self.acceptedItem = self.currentItem def ignore(self): + """An item should call this method if it cannot handle the event. This will allow the event to be delivered to other items.""" self.accepted = False def isAccepted(self): return self.accepted def scenePos(self): + """Return the current scene position of the mouse.""" return Point(self._scenePos) def screenPos(self): + """Return the current screen position (pixels relative to widget) of the mouse.""" return Point(self._screenPos) def buttonDownScenePos(self, btn=None): + """ + Return the scene position of the mouse at the time *btn* was pressed. + If *btn* is omitted, then the button that initiated the drag is assumed. + """ if btn is None: btn = self.button() return Point(self._buttonDownScenePos[int(btn)]) def buttonDownScreenPos(self, btn=None): + """ + Return the screen position (pixels relative to widget) of the mouse at the time *btn* was pressed. + If *btn* is omitted, then the button that initiated the drag is assumed. + """ if btn is None: btn = self.button() return Point(self._buttonDownScreenPos[int(btn)]) def lastScenePos(self): + """ + Return the scene position of the mouse immediately prior to this event. + """ return Point(self._lastScenePos) def lastScreenPos(self): + """ + Return the screen position of the mouse immediately prior to this event. + """ return Point(self._lastScreenPos) def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ return self._buttons def button(self): - """Return the button that initiated the drag (may be different from the buttons currently pressed)""" + """Return the button that initiated the drag (may be different from the buttons currently pressed) + (see QGraphicsSceneMouseEvent::button in the Qt documentation) + + """ return self._button def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._scenePos)) def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._lastScenePos)) def buttonDownPos(self, btn=None): + """ + Return the position of the mouse at the time the drag was initiated + in the coordinate system of the item that the event was delivered to. + """ if btn is None: btn = self.button() return Point(self.currentItem.mapFromScene(self._buttonDownScenePos[int(btn)])) def isStart(self): + """Returns True if this event is the first since a drag was initiated.""" return self.start def isFinish(self): + """Returns False if this is the last event in a drag. Note that this + event will have the same position as the previous one.""" return self.finish def __repr__(self): @@ -88,11 +135,21 @@ class MouseDragEvent: return "(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish())) def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + + """ return self._modifiers class MouseClickEvent: + """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their mouseClickEvent() method when the item is clicked. + + + """ + def __init__(self, pressEvent, double=False): self.accepted = False self.currentItem = None @@ -106,37 +163,60 @@ class MouseClickEvent: def accept(self): + """An item should call this method if it can handle the event. This will prevent the event being delivered to any other items.""" self.accepted = True self.acceptedItem = self.currentItem def ignore(self): + """An item should call this method if it cannot handle the event. This will allow the event to be delivered to other items.""" self.accepted = False def isAccepted(self): return self.accepted def scenePos(self): + """Return the current scene position of the mouse.""" return Point(self._scenePos) def screenPos(self): + """Return the current screen position (pixels relative to widget) of the mouse.""" return Point(self._screenPos) def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ return self._buttons def button(self): + """Return the mouse button that generated the click event. + (see QGraphicsSceneMouseEvent::button in the Qt documentation) + """ return self._button def double(self): + """Return True if this is a double-click.""" return self._double def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._scenePos)) def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._lastScenePos)) def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + """ return self._modifiers def __repr__(self): @@ -150,8 +230,9 @@ class MouseClickEvent: class HoverEvent: """ + Instances of this class are delivered to items in a :class:`GraphicsScene ` via their hoverEvent() method when the mouse is hovering over the item. This event class both informs items that the mouse cursor is nearby and allows items to - communicate with one another about whether each item will accept _potential_ mouse events. + communicate with one another about whether each item will accept *potential* mouse events. It is common for multiple overlapping items to receive hover events and respond by changing their appearance. This can be misleading to the user since, in general, only one item will @@ -188,13 +269,21 @@ class HoverEvent: def isEnter(self): + """Returns True if the mouse has just entered the item's shape""" return self.enter def isExit(self): + """Returns True if the mouse has just exited the item's shape""" return self.exit def acceptClicks(self, button): - """""" + """Inform the scene that the item (that the event was delivered to) + would accept a mouse click event if the user were to click before + moving the mouse again. + + Returns True if the request is successful, otherwise returns False (indicating + that some other item would receive an incoming click). + """ if not self.acceptable: return False if button not in self.__clickItems: @@ -203,6 +292,13 @@ class HoverEvent: return False def acceptDrags(self, button): + """Inform the scene that the item (that the event was delivered to) + would accept a mouse drag event if the user were to drag before + the next hover event. + + Returns True if the request is successful, otherwise returns False (indicating + that some other item would receive an incoming drag event). + """ if not self.acceptable: return False if button not in self.__dragItems: @@ -211,24 +307,40 @@ class HoverEvent: return False def scenePos(self): + """Return the current scene position of the mouse.""" return Point(self._scenePos) def screenPos(self): + """Return the current screen position of the mouse.""" return Point(self._screenPos) def lastScenePos(self): + """Return the previous scene position of the mouse.""" return Point(self._lastScenePos) def lastScreenPos(self): + """Return the previous screen position of the mouse.""" return Point(self._lastScreenPos) def buttons(self): + """ + Return the buttons currently pressed on the mouse. + (see QGraphicsSceneMouseEvent::buttons in the Qt documentation) + """ return self._buttons def pos(self): + """ + Return the current position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._scenePos)) def lastPos(self): + """ + Return the previous position of the mouse in the coordinate system of the item + that the event was delivered to. + """ return Point(self.currentItem.mapFromScene(self._lastScenePos)) def __repr__(self): @@ -237,6 +349,9 @@ class HoverEvent: return "(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit())) def modifiers(self): + """Return any keyboard modifiers currently pressed. + (see QGraphicsSceneMouseEvent::modifiers in the Qt documentation) + """ return self._modifiers def clickItems(self): diff --git a/__init__.py b/__init__.py index ddfe7d8e..9e22f53b 100644 --- a/__init__.py +++ b/__init__.py @@ -119,9 +119,10 @@ QAPP = None def plot(*args, **kargs): """ - | Create and return a PlotWindow (this is just a window with PlotWidget inside), plot data in it. - | Accepts a *title* argument to set the title of the window. - | All other arguments are used to plot data. (see :func:`PlotItem.plot() `) + Create and return a :class:`PlotWindow ` + (this is just a window with :class:`PlotWidget ` inside), plot data in it. + Accepts a *title* argument to set the title of the window. + All other arguments are used to plot data. (see :func:`PlotItem.plot() `) """ mkQApp() #if 'title' in kargs: @@ -149,10 +150,11 @@ def plot(*args, **kargs): def image(*args, **kargs): """ - | Create and return an ImageWindow (this is just a window with ImageView widget inside), show image data inside. - | Will show 2D or 3D image data. - | Accepts a *title* argument to set the title of the window. - | All other arguments are used to show data. (see :func:`ImageView.setImage() `) + Create and return an :class:`ImageWindow ` + (this is just a window with :class:`ImageView ` widget inside), show image data inside. + Will show 2D or 3D image data. + Accepts a *title* argument to set the title of the window. + All other arguments are used to show data. (see :func:`ImageView.setImage() `) """ mkQApp() w = ImageWindow(*args, **kargs) diff --git a/documentation/source/apireference.rst b/documentation/source/apireference.rst index ab4ec666..ec303140 100644 --- a/documentation/source/apireference.rst +++ b/documentation/source/apireference.rst @@ -9,3 +9,4 @@ Contents: functions graphicsItems/index widgets/index + graphicsscene/index \ No newline at end of file diff --git a/documentation/source/graphicsscene/graphicsscene.rst b/documentation/source/graphicsscene/graphicsscene.rst new file mode 100644 index 00000000..334a282b --- /dev/null +++ b/documentation/source/graphicsscene/graphicsscene.rst @@ -0,0 +1,8 @@ +GraphicsScene +============= + +.. autoclass:: pyqtgraph.GraphicsScene + :members: + + .. automethod:: pyqtgraph.GraphicsScene.__init__ + diff --git a/documentation/source/graphicsscene/hoverevent.rst b/documentation/source/graphicsscene/hoverevent.rst new file mode 100644 index 00000000..46007f91 --- /dev/null +++ b/documentation/source/graphicsscene/hoverevent.rst @@ -0,0 +1,5 @@ +HoverEvent +========== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.HoverEvent + :members: diff --git a/documentation/source/graphicsscene/index.rst b/documentation/source/graphicsscene/index.rst new file mode 100644 index 00000000..189bde6c --- /dev/null +++ b/documentation/source/graphicsscene/index.rst @@ -0,0 +1,12 @@ +GraphicsScene and Mouse Events +============================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + graphicsscene + hoverevent + mouseclickevent + mousedragevent diff --git a/documentation/source/graphicsscene/mouseclickevent.rst b/documentation/source/graphicsscene/mouseclickevent.rst new file mode 100644 index 00000000..f0c94e16 --- /dev/null +++ b/documentation/source/graphicsscene/mouseclickevent.rst @@ -0,0 +1,5 @@ +MouseClickEvent +=============== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.MouseClickEvent + :members: diff --git a/documentation/source/graphicsscene/mousedragevent.rst b/documentation/source/graphicsscene/mousedragevent.rst new file mode 100644 index 00000000..05c3aa6c --- /dev/null +++ b/documentation/source/graphicsscene/mousedragevent.rst @@ -0,0 +1,5 @@ +MouseDragEvent +============== + +.. autoclass:: pyqtgraph.GraphicsScene.mouseEvents.MouseDragEvent + :members: diff --git a/documentation/source/how_to_use.rst b/documentation/source/how_to_use.rst index 74e901d0..76c2d72b 100644 --- a/documentation/source/how_to_use.rst +++ b/documentation/source/how_to_use.rst @@ -17,7 +17,7 @@ Pyqtgraph makes it very easy to visualize data from the command line. Observe:: import pyqtgraph as pg pg.plot(data) # data can be a list of values or a numpy array -The example above would open a window displaying a line plot of the data given. I don't think it could reasonably be any simpler than that. The call to pg.plot returns a handle to the plot widget that is created, allowing more data to be added to the same window. +The example above would open a window displaying a line plot of the data given. The call to :func:`pg.plot ` returns a handle to the :class:`plot widget ` that is created, allowing more data to be added to the same window. Further examples:: @@ -43,5 +43,5 @@ While I consider this approach somewhat lazy, it is often the case that 'lazy' i Embedding widgets inside PyQt applications ------------------------------------------ -For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: PlotWidget, ImageView, GraphicsView, GraphicsLayoutWidget. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality. +For the serious application developer, all of the functionality in pyqtgraph is available via widgets that can be embedded just like any other Qt widgets. Most importantly, see: :class:`PlotWidget `, :class:`ImageView `, :class:`GraphicsLayoutWidget `, and :class:`GraphicsView `. Pyqtgraph's widgets can be included in Designer's ui files via the "Promote To..." functionality. diff --git a/documentation/source/introduction.rst b/documentation/source/introduction.rst index c5c1dfab..44a498bc 100644 --- a/documentation/source/introduction.rst +++ b/documentation/source/introduction.rst @@ -44,8 +44,8 @@ This will start a launcher with a list of available examples. Select an item fro How does it compare to... ------------------------- -* matplotlib: For plotting and making publication-quality graphics, matplotlib is far more mature than pyqtgraph. However, matplotlib is also much slower and not suitable for applications requiring realtime update of plots/video or rapid interactivity. It also does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. +* matplotlib: For plotting, pyqtgraph is not nearly as complete/mature as matplotlib, but runs much faster. Matplotlib is more aimed toward making publication-quality graphics, whereas pyqtgraph is intended for use in data acquisition and analysis applications. Matplotlib is more intuitive for matlab programmers; pyqtgraph is more intuitive for python/qt programmers. Matplotlib (to my knowledge) does not include many of pyqtgraph's features such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. -* pyqwt5: pyqwt is generally more mature than pyqtgraph for plotting and is about as fast. The major differences are 1) pyqtgraph is written in pure python, so it is somewhat more portable than pyqwt, which often lags behind pyqt in development (and can be a pain to install on some platforms) and 2) like matplotlib, pyqwt does not provide any of the GUI tools and image interaction/slicing functionality in pyqtgraph. +* pyqwt5: About as fast as pyqwt5, but not quite as complete for plotting functionality. Image handling in pyqtgraph is much more complete (again, no ROI widgets in qwt). Also, pyqtgraph is written in pure python, so it is more portable than pyqwt, which often lags behind pyqt in development (I originally used pyqwt, but decided it was too much trouble to rely on it as a dependency in my projects). Like matplotlib, pyqwt (to my knowledge) does not include many of pyqtgraph's features such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. (My experience with these libraries is somewhat outdated; please correct me if I am wrong here) diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index f1bd6313..ebc21c34 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -780,6 +780,9 @@ class PlotItem(GraphicsWidget): def removeItem(self, item): + """ + Remove an item from the internal ViewBox. + """ if not item in self.items: return self.items.remove(item) @@ -796,6 +799,9 @@ class PlotItem(GraphicsWidget): #item.sigPlotChanged.connect(self.plotChanged) def clear(self): + """ + Remove all items from the ViewBox. + """ for i in self.items[:]: self.removeItem(i) self.avgCurves = {} diff --git a/imageview/ImageView.py b/imageview/ImageView.py index 3ff75d9c..26fbfdb8 100644 --- a/imageview/ImageView.py +++ b/imageview/ImageView.py @@ -37,7 +37,26 @@ class PlotROI(ROI): class ImageView(QtGui.QWidget): + """ + Widget used for display and analysis of image data. + Implements many features: + * Displays 2D and 3D image data. For 3D data, a z-axis + slider is displayed allowing the user to select which frame is displayed. + * Displays histogram of image data with movable region defining the dark/light levels + * Editable gradient provides a color lookup table + * Frame slider may also be moved using left/right arrow keys as well as pgup, pgdn, home, and end. + * Basic analysis features including: + + * ROI and embedded plot for measuring image values across frames + * Image normalization / background subtraction + + Basic Usage:: + + imv = pg.ImageView() + imv.show() + imv.setImage(data) + """ sigTimeChanged = QtCore.Signal(object, object) sigProcessingChanged = QtCore.Signal(object) @@ -149,7 +168,168 @@ class ImageView(QtGui.QWidget): self.roiClicked() ## initialize roi plot to correct shape / visibility + def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None): + """ + Set the image to be displayed in the widget. + + ============== ======================================================================= + **Arguments:** + *img* (numpy array) the image to be displayed. + *xvals* (numpy array) 1D array of z-axis values corresponding to the third axis + in a 3D image. For video, this array should contain the time of each frame. + *autoRange* (bool) whether to scale/pan the view to fit the image. + *autoLevels* (bool) whether to update the white/black levels to fit the image. + *levels* (min, max); the white and black level values to use. + *axes* Dictionary indicating the interpretation for each axis. + This is only needed to override the default guess. Format is:: + + {'t':0, 'x':1, 'y':2, 'c':3}; + ============== ======================================================================= + """ + prof = debug.Profiler('ImageView.setImage', disabled=True) + + if not isinstance(img, np.ndarray): + raise Exception("Image must be specified as ndarray.") + self.image = img + + if xvals is not None: + self.tVals = xvals + elif hasattr(img, 'xvals'): + try: + self.tVals = img.xvals(0) + except: + self.tVals = np.arange(img.shape[0]) + else: + self.tVals = np.arange(img.shape[0]) + #self.ui.timeSlider.setValue(0) + #self.ui.normStartSlider.setValue(0) + #self.ui.timeSlider.setMaximum(img.shape[0]-1) + prof.mark('1') + + if axes is None: + if img.ndim == 2: + self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} + elif img.ndim == 3: + if img.shape[2] <= 4: + self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} + else: + self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} + elif img.ndim == 4: + self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} + else: + raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) + elif isinstance(axes, dict): + self.axes = axes.copy() + elif isinstance(axes, list) or isinstance(axes, tuple): + self.axes = {} + for i in range(len(axes)): + self.axes[axes[i]] = i + else: + raise Exception("Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes))) + + for x in ['t', 'x', 'y', 'c']: + self.axes[x] = self.axes.get(x, None) + prof.mark('2') + + self.imageDisp = None + + + prof.mark('3') + + self.currentIndex = 0 + self.updateImage() + if levels is None and autoLevels: + self.autoLevels() + if levels is not None: ## this does nothing since getProcessedImage sets these values again. + self.levelMax = levels[1] + self.levelMin = levels[0] + + if self.ui.roiBtn.isChecked(): + self.roiChanged() + prof.mark('4') + + + if self.axes['t'] is not None: + #self.ui.roiPlot.show() + self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) + self.timeLine.setValue(0) + #self.ui.roiPlot.setMouseEnabled(False, False) + if len(self.tVals) > 1: + start = self.tVals.min() + stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 + elif len(self.tVals) == 1: + start = self.tVals[0] - 0.5 + stop = self.tVals[0] + 0.5 + else: + start = 0 + stop = 1 + for s in [self.timeLine, self.normRgn]: + s.setBounds([start, stop]) + #else: + #self.ui.roiPlot.hide() + prof.mark('5') + + self.imageItem.resetTransform() + if scale is not None: + self.imageItem.scale(*scale) + if pos is not None: + self.imageItem.setPos(*pos) + prof.mark('6') + + if autoRange: + self.autoRange() + self.roiClicked() + prof.mark('7') + prof.finish() + + + def play(self, rate): + """Begin automatically stepping frames forward at the given rate (in fps). + This can also be accessed by pressing the spacebar.""" + #print "play:", rate + self.playRate = rate + if rate == 0: + self.playTimer.stop() + return + + self.lastPlayTime = ptime.time() + if not self.playTimer.isActive(): + self.playTimer.start(16) + + + + def autoLevels(self): + """Set the min/max levels automatically to match the image data.""" + #image = self.getProcessedImage() + self.setLevels(self.levelMin, self.levelMax) + + #self.ui.histogram.imageChanged(autoLevel=True) + + + def setLevels(self, min, max): + """Set the min/max (bright and dark) levels.""" + self.ui.histogram.setLevels(min, max) + + def autoRange(self): + """Auto scale and pan the view around the image.""" + image = self.getProcessedImage() + + #self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) + self.view.setRange(self.imageItem.boundingRect(), padding=0.) + + def getProcessedImage(self): + """Returns the image data after it has been processed by any normalization options in use.""" + if self.imageDisp is None: + image = self.normalize(self.image) + self.imageDisp = image + self.levelMin, self.levelMax = map(float, ImageView.quickMinMax(self.imageDisp)) + self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) + + return self.imageDisp + + def close(self): + """Closes the widget nicely, making sure to clear the graphics scene and release memory.""" self.ui.roiPlot.close() self.ui.graphicsView.close() #self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage) @@ -224,17 +404,6 @@ class ImageView(QtGui.QWidget): else: self.play(0) - def play(self, rate): - #print "play:", rate - self.playRate = rate - if rate == 0: - self.playTimer.stop() - return - - self.lastPlayTime = ptime.time() - if not self.playTimer.isActive(): - self.playTimer.start(16) - def timeout(self): now = ptime.time() @@ -251,6 +420,7 @@ class ImageView(QtGui.QWidget): self.jumpFrames(n) def setCurrentIndex(self, ind): + """Set the currently displayed frame index.""" self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1) self.updateImage() self.ignoreTimeLine = True @@ -258,7 +428,7 @@ class ImageView(QtGui.QWidget): self.ignoreTimeLine = False def jumpFrames(self, n): - """If this is a video, move ahead n frames""" + """Move video frame ahead n frames (may be negative)""" if self.axes['t'] is not None: self.setCurrentIndex(self.currentIndex + n) @@ -360,137 +530,6 @@ class ImageView(QtGui.QWidget): #self.ui.roiPlot.replot() - def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None): - """Set the image to be displayed in the widget. - Options are: - img: ndarray; the image to be displayed. - autoRange: bool; whether to scale/pan the view to fit the image. - autoLevels: bool; whether to update the white/black levels to fit the image. - levels: (min, max); the white and black level values to use. - axes: {'t':0, 'x':1, 'y':2, 'c':3}; Dictionary indicating the interpretation for each axis. - This is only needed to override the default guess. - """ - prof = debug.Profiler('ImageView.setImage', disabled=True) - - if not isinstance(img, np.ndarray): - raise Exception("Image must be specified as ndarray.") - self.image = img - - if xvals is not None: - self.tVals = xvals - elif hasattr(img, 'xvals'): - try: - self.tVals = img.xvals(0) - except: - self.tVals = np.arange(img.shape[0]) - else: - self.tVals = np.arange(img.shape[0]) - #self.ui.timeSlider.setValue(0) - #self.ui.normStartSlider.setValue(0) - #self.ui.timeSlider.setMaximum(img.shape[0]-1) - prof.mark('1') - - if axes is None: - if img.ndim == 2: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} - elif img.ndim == 3: - if img.shape[2] <= 4: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} - else: - self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} - elif img.ndim == 4: - self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} - else: - raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) - elif isinstance(axes, dict): - self.axes = axes.copy() - elif isinstance(axes, list) or isinstance(axes, tuple): - self.axes = {} - for i in range(len(axes)): - self.axes[axes[i]] = i - else: - raise Exception("Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes))) - - for x in ['t', 'x', 'y', 'c']: - self.axes[x] = self.axes.get(x, None) - prof.mark('2') - - self.imageDisp = None - - - prof.mark('3') - - self.currentIndex = 0 - self.updateImage() - if levels is None and autoLevels: - self.autoLevels() - if levels is not None: ## this does nothing since getProcessedImage sets these values again. - self.levelMax = levels[1] - self.levelMin = levels[0] - - if self.ui.roiBtn.isChecked(): - self.roiChanged() - prof.mark('4') - - - if self.axes['t'] is not None: - #self.ui.roiPlot.show() - self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) - self.timeLine.setValue(0) - #self.ui.roiPlot.setMouseEnabled(False, False) - if len(self.tVals) > 1: - start = self.tVals.min() - stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 - elif len(self.tVals) == 1: - start = self.tVals[0] - 0.5 - stop = self.tVals[0] + 0.5 - else: - start = 0 - stop = 1 - for s in [self.timeLine, self.normRgn]: - s.setBounds([start, stop]) - #else: - #self.ui.roiPlot.hide() - prof.mark('5') - - self.imageItem.resetTransform() - if scale is not None: - self.imageItem.scale(*scale) - if pos is not None: - self.imageItem.setPos(*pos) - prof.mark('6') - - if autoRange: - self.autoRange() - self.roiClicked() - prof.mark('7') - prof.finish() - - - def autoLevels(self): - #image = self.getProcessedImage() - self.setLevels(self.levelMin, self.levelMax) - - #self.ui.histogram.imageChanged(autoLevel=True) - - - def setLevels(self, min, max): - self.ui.histogram.setLevels(min, max) - - def autoRange(self): - image = self.getProcessedImage() - - #self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True) - self.view.setRange(self.imageItem.boundingRect(), padding=0.) - - def getProcessedImage(self): - if self.imageDisp is None: - image = self.normalize(self.image) - self.imageDisp = image - self.levelMin, self.levelMax = map(float, ImageView.quickMinMax(self.imageDisp)) - self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) - - return self.imageDisp @staticmethod def quickMinMax(data): @@ -578,7 +617,7 @@ class ImageView(QtGui.QWidget): def timeIndex(self, slider): - """Return the time and frame index indicated by a slider""" + ## Return the time and frame index indicated by a slider if self.image is None: return (0,0) #v = slider.value() diff --git a/widgets/PlotWidget.py b/widgets/PlotWidget.py index fa9fcf1a..3bb1f636 100644 --- a/widgets/PlotWidget.py +++ b/widgets/PlotWidget.py @@ -16,11 +16,30 @@ class PlotWidget(GraphicsView): #sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView """ - Widget implementing a graphicsView with a single PlotItem inside. + :class:`GraphicsView ` widget with a single + :class:`PlotItem ` inside. - The following methods are wrapped directly from PlotItem: addItem, removeItem, - clear, setXRange, setYRange, setRange, setAspectLocked, setMouseEnabled. For all - other methods, use getPlotItem. + The following methods are wrapped directly from PlotItem: + :func:`addItem `, + :func:`removeItem `, + :func:`clear `, + :func:`setXRange `, + :func:`setYRange `, + :func:`setRange `, + :func:`autoRange `, + :func:`setXLink `, + :func:`setYLink `, + :func:`viewRect `, + :func:`setMouseEnabled `, + :func:`enableAutoRange `, + :func:`disableAutoRange `, + :func:`setAspectLocked `, + :func:`register `, + :func:`unregister ` + + + For all + other methods, use :func:`getPlotItem `. """ def __init__(self, parent=None, **kargs): GraphicsView.__init__(self, parent) @@ -29,7 +48,8 @@ class PlotWidget(GraphicsView): self.plotItem = PlotItem(**kargs) self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled']: + ## NOTE: If you change this list, update the documentation above as well. + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'register', 'unregister', 'viewRect']: setattr(self, m, getattr(self.plotItem, m)) #QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged) self.plotItem.sigRangeChanged.connect(self.viewRangeChanged) From 4954b1993188c581806ba16965a87cb1b1643e29 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 18 Apr 2012 00:09:37 -0400 Subject: [PATCH 071/238] example update --- examples/DataSlicing.py | 3 ++- examples/__main__.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/DataSlicing.py b/examples/DataSlicing.py index a9e910be..25b6fbd3 100644 --- a/examples/DataSlicing.py +++ b/examples/DataSlicing.py @@ -46,7 +46,8 @@ roi.sigRegionChanged.connect(update) ## Display the data imv1.setImage(data) -imv1.setHistogramRange(data.min(), data.max()) +imv1.setHistogramRange(-0.01, 0.01) +imv1.setLevels(-0.003, 0.003) update() diff --git a/examples/__main__.py b/examples/__main__.py index 632f516a..49c4ae50 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -15,6 +15,7 @@ examples = OrderedDict([ ('Crosshair / Mouse interaction', 'crosshair.py'), ('Video speed test', 'VideoSpeedTest.py'), ('Plot speed test', 'PlotSpeedTest.py'), + ('Data Slicing', 'DataSlicing.py'), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), From c1963759a1968238bead7a574f06e5b982384915 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Wed, 18 Apr 2012 10:59:30 -0400 Subject: [PATCH 072/238] doc updates --- documentation/source/images.rst | 2 +- documentation/source/index.rst | 1 - documentation/source/parametertree.rst | 42 +++++++++++++++++++-- documentation/source/region_of_interest.rst | 22 ++++++----- graphicsItems/ViewBox/ViewBox.py | 2 + 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/documentation/source/images.rst b/documentation/source/images.rst index 461a9cb7..00d45650 100644 --- a/documentation/source/images.rst +++ b/documentation/source/images.rst @@ -18,7 +18,7 @@ If the data is 3D (time, x, y), then a time axis will be shown with a slider tha There are a few other methods for displaying images as well: * The :class:`~pyqtgraph.ImageView` class can also be instantiated directly and embedded in Qt applications. -* Instances of :class:`~pyqtgraph.ImageItem` can be used inside a GraphicsView. +* Instances of :class:`~pyqtgraph.ImageItem` can be used inside a :class:`ViewBox ` or :class:`GraphicsView `. * For higher performance, use :class:`~pyqtgraph.RawImageWidget`. Any of these classes are acceptable for displaying video by calling setImage() to display a new frame. To increase performance, the image processing system uses scipy.weave to produce compiled libraries. If your computer has a compiler available, weave will automatically attempt to build the libraries it needs on demand. If this fails, then the slower pure-python methods will be used instead. diff --git a/documentation/source/index.rst b/documentation/source/index.rst index 76c60380..32d15524 100644 --- a/documentation/source/index.rst +++ b/documentation/source/index.rst @@ -18,7 +18,6 @@ Contents: images style region_of_interest - graphicswindow parametertree internals apireference diff --git a/documentation/source/parametertree.rst b/documentation/source/parametertree.rst index de699492..59c9db44 100644 --- a/documentation/source/parametertree.rst +++ b/documentation/source/parametertree.rst @@ -1,7 +1,41 @@ Rapid GUI prototyping ===================== - - parametertree - - dockarea - - flowchart - - canvas +[Just an overview; documentation is not complete yet] + +Pyqtgraph offers several powerful features which are commonly used in engineering and scientific applications. + +Parameter Trees +--------------- + +The parameter tree system provides a widget displaying a tree of modifiable values similar to those used in most GUI editor applications. This allows a large number of variables to be controlled by the user with relatively little programming effort. The system also provides separation between the data being controlled and the user interface controlling it (model/view architecture). Parameters may be grouped/nested to any depth and custom parameter types can be built by subclassing from Parameter and ParameterItem. + +See the parametertree example for more information. + + +Visual Programming Flowcharts +----------------------------- + +Pyqtgraph's flowcharts provide a visual programming environment similar in concept to LabView--functional modules are added to a flowchart and connected by wires to define a more complex and arbitrarily configurable algorithm. A small number of predefined modules (called Nodes) are included with pyqtgraph, but most flowchart developers will want to define their own library of Nodes. At their core, the Nodes are little more than 1) a Python function 2) a list of input/output terminals, and 3) an optional widget providing a control panel for the Node. Nodes may transmit/receive any type of Python object via their terminals. + +One major limitation of flowcharts is that there is no mechanism for looping within a flowchart. (however individual Nodes may contain loops (they may contain any Python code at all), and an entire flowchart may be executed from within a loop). + +There are two distinct modes of executing the code in a flowchart: + +1. Provide data to the input terminals of the flowchart. This method is slower and will provide a graphical representation of the data as it passes through the flowchart. This is useful for debugging as it allows the user to inspect the data at each terminal and see where exceptions occurred within the flowchart. +2. Call Flowchart.process. This method does not update the displayed state of the flowchart and only retains the state of each terminal as long as it is needed. Additionally, Nodes which do not contribute to the output values of the flowchart (such as plotting nodes) are ignored. This mode allows for faster processing of large data sets and avoids memory issues which can occur if doo much data is present in the flowchart at once (e.g., when processing image data through several stages). + +See the flowchart example for more information. + +Graphical Canvas +---------------- + +The Canvas is a system designed to allow the user to add/remove items to a 2D canvas similar to most vector graphics applications. Items can be translated/scaled/rotated and each item may define its own custom control interface. + + +Dockable Widgets +---------------- + +The dockarea system allows the design of user interfaces which can be rearranged by the user at runtime. Docks can be moved, resized, stacked, and torn out of the main window. This is similar in principle to the docking system built into Qt, but offers a more deterministic dock placement API (in Qt it is very difficult to programatically generate complex dock arrangements). Additionally, Qt's docks are designed to be used as small panels around the outer edge of a window. Pyqtgraph's docks were created with the notion that the entire window (or any portion of it) would consist of dockable components. + + diff --git a/documentation/source/region_of_interest.rst b/documentation/source/region_of_interest.rst index 24799cb7..eda9cacc 100644 --- a/documentation/source/region_of_interest.rst +++ b/documentation/source/region_of_interest.rst @@ -1,19 +1,23 @@ -Region-of-interest controls -=========================== +Interactive Data Selection Controls +=================================== -Slicing Multidimensional Data ------------------------------ +Pyqtgraph includes graphics items which allow the user to select and mark regions of data. Linear Selection and Marking ---------------------------- +Two classes allow marking and selecting 1-dimensional data: :class:`LinearRegionItem ` and :class:`InfiniteLine `. The first class, :class:`LinearRegionItem `, may be added to any ViewBox or PlotItem to mark either a horizontal or vertical region. The region can be dragged and its bounding edges can be moved independently. The second class, :class:`InfiniteLine `, is usually used to mark a specific position along the x or y axis. These may be dragged by the user. + + 2D Selection and Marking ------------------------ +To select a 2D region from an image, pyqtgraph uses the :class:`ROI ` class or any of its subclasses. By default, :class:`ROI ` simply displays a rectangle which can be moved by the user to mark a specific region (most often this will be a region of an image, but this is not required). To allow the ROI to be resized or rotated, there are several methods for adding handles (:func:`addScaleHandle `, :func:`addRotateHandle `, etc.) which can be dragged by the user. These handles may be placed at any location relative to the ROI and may scale/rotate the ROI around any arbitrary center point. There are several ROI subclasses with a variety of shapes and modes of interaction. + +To automatically extract a region of image data using an ROI and an ImageItem, use :func:`ROI.getArrayRegion `. ROI classes use the :func:`affineSlice ` function to perform this extraction. + +ROI can also be used as a control for moving/rotating/scaling items in a scene similar to most vetctor graphics editing applications. + +See the ROITypes example for more information. - -- translate / rotate / scale -- highly configurable control handles -- automated data slicing -- linearregion, infiniteline diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index d517f7da..cf88508f 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -27,6 +27,8 @@ class ChildGroup(ItemGroup): class ViewBox(GraphicsWidget): """ + **Bases:** :class:`GraphicsWidget ` + Box that allows internal scaling/panning of children by mouse drag. This class is usually created automatically as part of a :class:`PlotItem ` or :class:`Canvas ` or with :func:`GraphicsLayout.addViewBox() `. From 59ad54c55ef95a41a57e18e1a41e6d0cdbe13807 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 21 Apr 2012 15:54:17 -0400 Subject: [PATCH 073/238] minor Canvas updates --- canvas/Canvas.py | 9 ++++ canvas/CanvasItem.py | 15 +++++- canvas/CanvasTemplate.py | 56 ++++++++++---------- canvas/CanvasTemplate.ui | 93 ++++++++++++++++++---------------- canvas/TransformGuiTemplate.py | 15 ++++-- canvas/TransformGuiTemplate.ui | 31 ++++++++---- 6 files changed, 135 insertions(+), 84 deletions(-) diff --git a/canvas/Canvas.py b/canvas/Canvas.py index 5f7c1ac6..5176f41d 100644 --- a/canvas/Canvas.py +++ b/canvas/Canvas.py @@ -43,6 +43,7 @@ class Canvas(QtGui.QWidget): self.multiSelectBox.hide() self.multiSelectBox.setZValue(1e6) self.ui.mirrorSelectionBtn.hide() + self.ui.reflectSelectionBtn.hide() self.ui.resetTransformsBtn.hide() self.redirect = None ## which canvas to redirect items to @@ -75,6 +76,7 @@ class Canvas(QtGui.QWidget): self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged) self.multiSelectBox.sigRegionChangeFinished.connect(self.multiSelectBoxChangeFinished) self.ui.mirrorSelectionBtn.clicked.connect(self.mirrorSelectionClicked) + self.ui.reflectSelectionBtn.clicked.connect(self.reflectSelectionClicked) self.ui.resetTransformsBtn.clicked.connect(self.resetTransformsClicked) self.resizeEvent() @@ -211,6 +213,7 @@ class Canvas(QtGui.QWidget): #item.ctrlWidget().show() self.multiSelectBox.hide() self.ui.mirrorSelectionBtn.hide() + self.ui.reflectSelectionBtn.hide() self.ui.resetTransformsBtn.hide() elif len(sel) > 1: self.showMultiSelectBox() @@ -265,6 +268,7 @@ class Canvas(QtGui.QWidget): self.multiSelectBox.show() self.ui.mirrorSelectionBtn.show() + self.ui.reflectSelectionBtn.show() self.ui.resetTransformsBtn.show() #self.multiSelectBoxBase = self.multiSelectBox.getState().copy() @@ -272,6 +276,11 @@ class Canvas(QtGui.QWidget): for ci in self.selectedItems(): ci.mirrorY() self.showMultiSelectBox() + + def reflectSelectionClicked(self): + for ci in self.selectedItems(): + ci.mirrorXY() + self.showMultiSelectBox() def resetTransformsClicked(self): for i in self.selectedItems(): diff --git a/canvas/CanvasItem.py b/canvas/CanvasItem.py index d9b6f100..67b442af 100644 --- a/canvas/CanvasItem.py +++ b/canvas/CanvasItem.py @@ -73,6 +73,7 @@ class CanvasItem(QtCore.QObject): self.transformGui.setupUi(self.transformWidget) self.layout.addWidget(self.transformWidget, 3, 0, 1, 2) self.transformGui.mirrorImageBtn.clicked.connect(self.mirrorY) + self.transformGui.reflectImageBtn.clicked.connect(self.mirrorXY) self.layout.addWidget(self.resetTransformBtn, 1, 0, 1, 2) self.layout.addWidget(self.copyBtn, 2, 0, 1, 1) @@ -221,6 +222,18 @@ class CanvasItem(QtCore.QObject): #self.selectBoxFromUser() #return + def mirrorXY(self): + if not self.isMovable(): + return + self.rotate(180.) + # inv = pg.Transform() + # inv.scale(-1, -1) + # self.userTransform = self.userTransform * inv #flip lr/ud + # s=self.updateTransform() + # self.setTranslate(-2*s['pos'][0], -2*s['pos'][1]) + # self.selectBoxFromUser() + + def hasUserTransform(self): #print self.userRotate, self.userTranslate return not self.userTransform.isIdentity() @@ -307,7 +320,6 @@ class CanvasItem(QtCore.QObject): def updateTransform(self): """Regenerate the item position from the base, user, and temp transforms""" transform = self.baseTransform * self.userTransform * self.tempTransform ## order is important - s = transform.saveState() self._graphicsItem.setPos(*s['pos']) @@ -316,6 +328,7 @@ class CanvasItem(QtCore.QObject): self.itemScale.setYScale(s['scale'][1]) self.displayTransform(transform) + return(s) # return the transform state def displayTransform(self, transform): """Updates transform numbers in the ctrl widget.""" diff --git a/canvas/CanvasTemplate.py b/canvas/CanvasTemplate.py index c525b705..3fe789c0 100644 --- a/canvas/CanvasTemplate.py +++ b/canvas/CanvasTemplate.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'CanvasTemplate.ui' +# Form implementation generated from reading ui file './lib/util/pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Sun Dec 18 20:04:41 2011 +# Created: Wed Apr 18 13:40:19 2012 # by: PyQt4 UI code generator 4.8.3 # # WARNING! All changes made in this file will be lost! @@ -17,7 +17,7 @@ except AttributeError: class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(466, 422) + Form.resize(490, 414) self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout.setMargin(0) self.gridLayout.setSpacing(0) @@ -32,6 +32,12 @@ class Ui_Form(object): self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) self.gridLayout_2.setMargin(0) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) + self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget) + self.storeSvgBtn.setObjectName(_fromUtf8("storeSvgBtn")) + self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) + self.storePngBtn = QtGui.QPushButton(self.layoutWidget) + self.storePngBtn.setObjectName(_fromUtf8("storePngBtn")) + self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -40,6 +46,16 @@ class Ui_Form(object): self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn")) self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) + self.redirectCheck.setObjectName(_fromUtf8("redirectCheck")) + self.horizontalLayout.addWidget(self.redirectCheck) + self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo.setObjectName(_fromUtf8("redirectCombo")) + self.horizontalLayout.addWidget(self.redirectCombo) + self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) self.itemList = TreeWidget(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -53,29 +69,16 @@ class Ui_Form(object): self.ctrlLayout = QtGui.QGridLayout() self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout")) - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget) - self.storeSvgBtn.setObjectName(_fromUtf8("storeSvgBtn")) - self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) - self.storePngBtn = QtGui.QPushButton(self.layoutWidget) - self.storePngBtn.setObjectName(_fromUtf8("storePngBtn")) - self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) - self.horizontalLayout = QtGui.QHBoxLayout() - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) - self.redirectCheck.setObjectName(_fromUtf8("redirectCheck")) - self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) - self.redirectCombo.setObjectName(_fromUtf8("redirectCombo")) - self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) - self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) - self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 8, 0, 1, 1) + self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) - self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 1, 1, 1) + self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) + self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn")) + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -83,13 +86,14 @@ class Ui_Form(object): def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8)) self.storeSvgBtn.setText(QtGui.QApplication.translate("Form", "Store SVG", None, QtGui.QApplication.UnicodeUTF8)) self.storePngBtn.setText(QtGui.QApplication.translate("Form", "Store PNG", None, QtGui.QApplication.UnicodeUTF8)) + self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8)) self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8)) self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8)) - self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8)) + self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) + self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) from pyqtgraph.widgets.GraphicsView import GraphicsView from CanvasManager import CanvasCombo diff --git a/canvas/CanvasTemplate.ui b/canvas/CanvasTemplate.ui index b104c84c..da032906 100644 --- a/canvas/CanvasTemplate.ui +++ b/canvas/CanvasTemplate.ui @@ -6,8 +6,8 @@ 0 0 - 466 - 422 + 490 + 414
@@ -28,44 +28,6 @@ - - - - - 0 - 1 - - - - Auto Range - - - - - - - - 0 - 100 - - - - true - - - - 1 - - - - - - - - 0 - - - @@ -80,6 +42,19 @@ + + + + + 0 + 1 + + + + Auto Range + + + @@ -100,17 +75,49 @@ + + + + + 0 + 100 + + + + true + + + + 1 + + + + + + + + 0 + + + + + + Reset Transforms + + + + Mirror Selection - - + + - Reset Transforms + MirrorXY diff --git a/canvas/TransformGuiTemplate.py b/canvas/TransformGuiTemplate.py index 5ffc3f08..3c7fcf50 100644 --- a/canvas/TransformGuiTemplate.py +++ b/canvas/TransformGuiTemplate.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'TransformGuiTemplate.ui' +# Form implementation generated from reading ui file './lib/util/pyqtgraph/canvas/TransformGuiTemplate.ui' # -# Created: Sun Dec 18 20:04:40 2011 +# Created: Wed Apr 18 13:40:19 2012 # by: PyQt4 UI code generator 4.8.3 # # WARNING! All changes made in this file will be lost! @@ -17,7 +17,7 @@ except AttributeError: class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(169, 82) + Form.resize(224, 117) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -36,10 +36,16 @@ class Ui_Form(object): self.scaleLabel = QtGui.QLabel(Form) self.scaleLabel.setObjectName(_fromUtf8("scaleLabel")) self.verticalLayout.addWidget(self.scaleLabel) + self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) self.mirrorImageBtn = QtGui.QPushButton(Form) self.mirrorImageBtn.setToolTip(_fromUtf8("")) self.mirrorImageBtn.setObjectName(_fromUtf8("mirrorImageBtn")) - self.verticalLayout.addWidget(self.mirrorImageBtn) + self.horizontalLayout.addWidget(self.mirrorImageBtn) + self.reflectImageBtn = QtGui.QPushButton(Form) + self.reflectImageBtn.setObjectName(_fromUtf8("reflectImageBtn")) + self.horizontalLayout.addWidget(self.reflectImageBtn) + self.verticalLayout.addLayout(self.horizontalLayout) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) @@ -50,4 +56,5 @@ class Ui_Form(object): self.rotateLabel.setText(QtGui.QApplication.translate("Form", "Rotate:", None, QtGui.QApplication.UnicodeUTF8)) self.scaleLabel.setText(QtGui.QApplication.translate("Form", "Scale:", None, QtGui.QApplication.UnicodeUTF8)) self.mirrorImageBtn.setText(QtGui.QApplication.translate("Form", "Mirror", None, QtGui.QApplication.UnicodeUTF8)) + self.reflectImageBtn.setText(QtGui.QApplication.translate("Form", "Reflect", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/canvas/TransformGuiTemplate.ui b/canvas/TransformGuiTemplate.ui index c8c24a95..d8312388 100644 --- a/canvas/TransformGuiTemplate.ui +++ b/canvas/TransformGuiTemplate.ui @@ -6,8 +6,8 @@ 0 0 - 169 - 82 + 224 + 117 @@ -48,14 +48,25 @@ - - - - - - Mirror - - + + + + + + + + Mirror + + + + + + + Reflect + + + +
From 33bc81a121579c33fcca0bd07bb4b89355e5b683 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 21 Apr 2012 15:55:27 -0400 Subject: [PATCH 074/238] Fixed click signal propagation for PlotDataItem --- graphicsItems/PlotCurveItem.py | 55 ++++++++++++++++-- graphicsItems/PlotDataItem.py | 42 +++++++++++--- graphicsItems/ScatterPlotItem.py | 97 ++++++++++++++++++++++---------- 3 files changed, 149 insertions(+), 45 deletions(-) diff --git a/graphicsItems/PlotCurveItem.py b/graphicsItems/PlotCurveItem.py index bc3629d2..4ce9af46 100644 --- a/graphicsItems/PlotCurveItem.py +++ b/graphicsItems/PlotCurveItem.py @@ -12,17 +12,50 @@ __all__ = ['PlotCurveItem'] class PlotCurveItem(GraphicsObject): - """Class representing a single plot curve. Provides: - - Fast data update - - FFT display mode - - shadow pen - - mouse interaction + """ + Class representing a single plot curve. Instances of this class are created + automatically as part of PlotDataItem; these rarely need to be instantiated + directly. + + Features: + + - Fast data update + - FFT display mode (accessed via PlotItem context menu) + - Fill under curve + - Mouse interaction + + ==================== =============================================== + **Signals:** + sigPlotChanged(self) Emitted when the data being plotted has changed + sigClicked(self) Emitted when the curve is clicked + ==================== =============================================== """ sigPlotChanged = QtCore.Signal(object) sigClicked = QtCore.Signal(object) def __init__(self, y=None, x=None, fillLevel=None, copy=False, pen=None, shadowPen=None, brush=None, parent=None, clickable=False): + """ + ============== ======================================================= + **Arguments:** + x, y (numpy arrays) Data to show + pen Pen to use when drawing. Any single argument accepted by + :func:`mkPen ` is allowed. + shadowPen Pen for drawing behind the primary pen. Usually this + is used to emphasize the curve by providing a + high-contrast border. Any single argument accepted by + :func:`mkPen ` is allowed. + fillLevel (float or None) Fill the area 'under' the curve to + *fillLevel* + brush QBrush to use when filling. Any single argument accepted + by :func:`mkBrush ` is allowed. + clickable If True, the item will emit sigClicked when it is + clicked on. + ============== ======================================================= + + + + """ GraphicsObject.__init__(self, parent) self.clear() self.path = None @@ -62,6 +95,7 @@ class PlotCurveItem(GraphicsObject): return interface in ints def setClickable(self, s): + """Sets whether the item responds to mouse clicks.""" self.clickable = s @@ -127,18 +161,25 @@ class PlotCurveItem(GraphicsObject): #return self.metaData def setPen(self, *args, **kargs): + """Set the pen used to draw the curve.""" self.opts['pen'] = fn.mkPen(*args, **kargs) self.update() def setShadowPen(self, *args, **kargs): + """Set the shadow pen used to draw behind tyhe primary pen. + This pen must have a larger width than the primary + pen to be visible. + """ self.opts['shadowPen'] = fn.mkPen(*args, **kargs) self.update() def setBrush(self, *args, **kargs): + """Set the brush used when filling the area under the curve""" self.opts['brush'] = fn.mkBrush(*args, **kargs) self.update() def setFillLevel(self, level): + """Set the level filled to when filling under the curve""" self.opts['fillLevel'] = level self.fillPath = None self.update() @@ -177,7 +218,9 @@ class PlotCurveItem(GraphicsObject): #self.update() def setData(self, *args, **kargs): - """Same as updateData()""" + """ + Accepts most of the same arguments as __init__. + """ self.updateData(*args, **kargs) def updateData(self, *args, **kargs): diff --git a/graphicsItems/PlotDataItem.py b/graphicsItems/PlotDataItem.py index 44493799..bb249bf3 100644 --- a/graphicsItems/PlotDataItem.py +++ b/graphicsItems/PlotDataItem.py @@ -24,15 +24,18 @@ class PlotDataItem(GraphicsObject): usually created by plot() methods such as :func:`pyqtgraph.plot` and :func:`PlotItem.plot() `. - ===================== ============================================== + ============================== ============================================== **Signals:** - sigPlotChanged(self) Emitted when the data in this item is updated. - sigClicked(self) Emitted when the item is clicked. - ===================== ============================================== + sigPlotChanged(self) Emitted when the data in this item is updated. + sigClicked(self) Emitted when the item is clicked. + sigPointsClicked(self, points) Emitted when a plot point is clicked + Sends the list of points under the mouse. + ============================== ============================================== """ sigPlotChanged = QtCore.Signal(object) sigClicked = QtCore.Signal(object) + sigPointsClicked = QtCore.Signal(object, object) def __init__(self, *args, **kargs): """ @@ -109,6 +112,10 @@ class PlotDataItem(GraphicsObject): self.curve.setParentItem(self) self.scatter.setParentItem(self) + self.curve.sigClicked.connect(self.curveClicked) + self.scatter.sigClicked.connect(self.scatterClicked) + + #self.clear() self.opts = { 'fftMode': False, @@ -127,6 +134,8 @@ class PlotDataItem(GraphicsObject): 'symbolPen': (200,200,200), 'symbolBrush': (50, 50, 150), 'identical': False, + + 'data': None, } self.setData(*args, **kargs) @@ -150,8 +159,8 @@ class PlotDataItem(GraphicsObject): self.xDisp = self.yDisp = None self.updateItems() - def setLogMode(self, mode): - self.opts['logMode'] = mode + def setLogMode(self, xMode, yMode): + self.opts['logMode'] = (xMode, yMode) self.xDisp = self.yDisp = None self.updateItems() @@ -244,7 +253,7 @@ class PlotDataItem(GraphicsObject): data = args[0] dt = dataType(data) if dt == 'empty': - return + pass elif dt == 'listOfValues': y = np.array(data) elif dt == 'Nx2array': @@ -260,6 +269,8 @@ class PlotDataItem(GraphicsObject): x = np.array([d.get('x',None) for d in data]) if 'y' in data[0]: y = np.array([d.get('y',None) for d in data]) + for k in ['data', 'symbolSize', 'symbolPen', 'symbolBrush', 'symbolShape']: + kargs[k] = [d.get(k, None) for d in data] elif dt == 'MetaArray': y = data.view(np.ndarray) x = data.xvals(0).view(np.ndarray) @@ -349,8 +360,9 @@ class PlotDataItem(GraphicsObject): curveArgs[v] = self.opts[k] scatterArgs = {} - for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size')]: - scatterArgs[v] = self.opts[k] + for k,v in [('symbolPen','pen'), ('symbolBrush','brush'), ('symbol','symbol'), ('symbolSize', 'size'), ('data', 'data')]: + if k in self.opts: + scatterArgs[v] = self.opts[k] x,y = self.getData() @@ -398,6 +410,11 @@ class PlotDataItem(GraphicsObject): x = np.log10(x) if self.opts['logMode'][1]: y = np.log10(y) + if any(self.opts['logMode']): ## re-check for NANs after log + nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y) + if any(nanMask): + x = x[~nanMask] + y = y[~nanMask] self.xDisp = x self.yDisp = y #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() @@ -438,6 +455,13 @@ class PlotDataItem(GraphicsObject): def appendData(self, *args, **kargs): pass + def curveClicked(self): + self.sigClicked.emit(self) + + def scatterClicked(self, plt, points): + self.sigClicked.emit(self) + self.sigPointsClicked.emit(self, points) + def dataType(obj): if hasattr(obj, '__len__') and len(obj) == 0: diff --git a/graphicsItems/ScatterPlotItem.py b/graphicsItems/ScatterPlotItem.py index f40db2fa..b93c134f 100644 --- a/graphicsItems/ScatterPlotItem.py +++ b/graphicsItems/ScatterPlotItem.py @@ -7,7 +7,23 @@ import scipy.stats __all__ = ['ScatterPlotItem', 'SpotItem'] class ScatterPlotItem(GraphicsObject): + """ + Displays a set of x/y points. Instances of this class are created + automatically as part of PlotDataItem; these rarely need to be instantiated + directly. + The size, shape, pen, and fill brush may be set for each point individually + or for all points. + + + ======================== =============================================== + **Signals:** + sigPlotChanged(self) Emitted when the data being plotted has changed + sigClicked(self, points) Emitted when the curve is clicked. Sends a list + of all the points under the mouse pointer. + ======================== =============================================== + + """ #sigPointClicked = QtCore.Signal(object, object) sigClicked = QtCore.Signal(object, object) ## self, points sigPlotChanged = QtCore.Signal(object) @@ -37,35 +53,37 @@ class ScatterPlotItem(GraphicsObject): def setData(self, *args, **kargs): """ - Ordered Arguments: - If there is only one unnamed argument, it will be interpreted like the 'spots' argument. - - If there are two unnamed arguments, they will be interpreted as sequences of x and y values. + **Ordered Arguments:** - Keyword Arguments: - *spots*: Optional list of dicts. Each dict specifies parameters for a single spot: - {'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method - of passing in data for the corresponding arguments. - *x*,*y*: 1D arrays of x,y values. - *pos*: 2D structure of x,y pairs (such as Nx2 array or list of tuples) - *pxMode*: If True, spots are always the same size regardless of scaling, and size is given in px. - Otherwise, size is in scene coordinates and the spots scale with the view. - Default is True - *identical*: If True, all spots are forced to look identical. - This can result in performance enhancement. - Default is False - *symbol* can be one (or a list) of: - 'o' circle (default) - 's' square - 't' triangle - 'd' diamond - '+' plus - - *pen*: The pen (or list of pens) to use for drawing spot outlines. - *brush*: The brush (or list of brushes) to use for filling spots. - *size*: The size (or list of sizes) of spots. If *pxMode* is True, this value is in pixels. Otherwise, - it is in the item's local coordinate system. - *data*: a list of python objects used to uniquely identify each spot. + * If there is only one unnamed argument, it will be interpreted like the 'spots' argument. + * If there are two unnamed arguments, they will be interpreted as sequences of x and y values. + + ====================== ================================================= + **Keyword Arguments:** + *spots* Optional list of dicts. Each dict specifies parameters for a single spot: + {'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method + of passing in data for the corresponding arguments. + *x*,*y* 1D arrays of x,y values. + *pos* 2D structure of x,y pairs (such as Nx2 array or list of tuples) + *pxMode* If True, spots are always the same size regardless of scaling, and size is given in px. + Otherwise, size is in scene coordinates and the spots scale with the view. + Default is True + *identical* If True, all spots are forced to look identical. + This can result in performance enhancement. + Default is False + *symbol* can be one (or a list) of: + + * 'o' circle (default) + * 's' square + * 't' triangle + * 'd' diamond + * '+' plus + *pen* The pen (or list of pens) to use for drawing spot outlines. + *brush* The brush (or list of brushes) to use for filling spots. + *size* The size (or list of sizes) of spots. If *pxMode* is True, this value is in pixels. Otherwise, + it is in the item's local coordinate system. + *data* a list of python objects used to uniquely identify each spot. + ====================== ================================================= """ self.clear() @@ -148,6 +166,9 @@ class ScatterPlotItem(GraphicsObject): if k in kargs: setMethod = getattr(self, 'set' + k[0].upper() + k[1:]) setMethod(kargs[k]) + + if 'data' in kargs: + self.setPointData(kargs['data']) self.updateSpots() @@ -183,7 +204,7 @@ class ScatterPlotItem(GraphicsObject): #self.data[k].append(v) def setPoints(self, *args, **kargs): - """Deprecated; use setData""" + ##Deprecated; use setData return self.setData(*args, **kargs) #def setPoints(self, spots=None, x=None, y=None, data=None): @@ -259,6 +280,16 @@ class ScatterPlotItem(GraphicsObject): self.opts['size'] = size self.updateSpots() + def setPointData(self, data): + if isinstance(data, np.ndarray) or isinstance(data, list): + if self.data is None: + raise Exception("Must set xy data before setting meta data.") + if len(data) != len(self.data): + raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(self.data))) + self.data['data'] = data + self.updateSpots() + + def setIdentical(self, ident): self.opts['identical'] = ident self.updateSpots() @@ -353,6 +384,12 @@ class ScatterPlotItem(GraphicsObject): symbol = self.data['symbol'].copy() symbol[symbol==''] = self.opts['symbol'] + + data = self.data['data'].copy() + if 'data' in self.opts: + data[data==None] = self.opts['data'] + + for i in xrange(len(self.data)): s = self.data[i] @@ -373,7 +410,7 @@ class ScatterPlotItem(GraphicsObject): #ymn = min(ymn, pos[1]-psize) #ymx = max(ymx, pos[1]+psize) - item = self.mkSpot(pos, size[i], self.opts['pxMode'], brush[i], pen[i], s['data'], symbol=symbol[i], index=len(self.spots)) + item = self.mkSpot(pos, size[i], self.opts['pxMode'], brush[i], pen[i], data[i], symbol=symbol[i], index=len(self.spots)) self.spots.append(item) self.data[i]['spot'] = item #if self.optimize: From 8b721e7d7840fc7de1debf8d178c1804a3f85eeb Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 21 Apr 2012 15:57:13 -0400 Subject: [PATCH 075/238] doc update --- documentation/source/graphicsItems/graphicsitem.rst | 6 ++++++ graphicsItems/ROI.py | 0 2 files changed, 6 insertions(+) create mode 100644 documentation/source/graphicsItems/graphicsitem.rst mode change 100644 => 100755 graphicsItems/ROI.py diff --git a/documentation/source/graphicsItems/graphicsitem.rst b/documentation/source/graphicsItems/graphicsitem.rst new file mode 100644 index 00000000..b9573aea --- /dev/null +++ b/documentation/source/graphicsItems/graphicsitem.rst @@ -0,0 +1,6 @@ +GraphicsItem +============ + +.. autoclass:: pyqtgraph.GraphicsItem + :members: + diff --git a/graphicsItems/ROI.py b/graphicsItems/ROI.py old mode 100644 new mode 100755 From f8758dba39c7a4ccd60c02c1abe12ee78c58ff81 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Sat, 21 Apr 2012 15:57:47 -0400 Subject: [PATCH 076/238] PlotItem (finally) gets log scaling Also cleaned up some context menu items --- examples/Plotting.py | 11 +- examples/logAxis.py | 38 +++++++ graphicsItems/AxisItem.py | 26 +++++ graphicsItems/PlotItem/PlotItem.py | 100 ++++++++++++++++--- graphicsItems/PlotItem/plotConfigTemplate.py | 93 ++++++++++------- graphicsItems/PlotItem/plotConfigTemplate.ui | 82 +++++++++------ 6 files changed, 269 insertions(+), 81 deletions(-) create mode 100644 examples/logAxis.py diff --git a/examples/Plotting.py b/examples/Plotting.py index 79d0d4ac..cb512503 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -35,18 +35,20 @@ p3.plot(np.random.normal(size=100), pen=(200,200,200), symbolBrush=(255,0,0), sy win.nextRow() -p4 = win.addPlot(title="Parametric") +p4 = win.addPlot(title="Parametric, grid enabled") x = np.cos(np.linspace(0, 2*np.pi, 1000)) y = np.sin(np.linspace(0, 4*np.pi, 1000)) p4.plot(x, y) +p4.showGrid(x=True, y=True) -p5 = win.addPlot(title="Scatter plot with labels") +p5 = win.addPlot(title="Scatter plot, axis labels, log scale") x = np.random.normal(size=1000) * 1e-5 y = x*1000 + 0.005 * np.random.normal(size=1000) +y -= y.min()-1.0 p5.plot(x, y, pen=None, symbol='t', symbolPen=None, symbolSize=10, symbolBrush=(100, 100, 255, 50)) p5.setLabel('left', "Y Axis", units='A') p5.setLabel('bottom', "Y Axis", units='s') - +p5.setLogMode(x=True, y=False) p6 = win.addPlot(title="Updating plot") curve = p6.plot(pen='y') @@ -65,9 +67,10 @@ timer.start(50) win.nextRow() -p7 = win.addPlot(title="Filled plot") +p7 = win.addPlot(title="Filled plot, axis disabled") y = np.sin(np.linspace(0, 10, 1000)) + np.random.normal(size=1000, scale=0.1) p7.plot(y, fillLevel=-0.3, brush=(50,50,200,100)) +p7.showAxis('bottom', False) x2 = np.linspace(-100, 100, 1000) diff --git a/examples/logAxis.py b/examples/logAxis.py new file mode 100644 index 00000000..3e291fb2 --- /dev/null +++ b/examples/logAxis.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +import initExample ## Add path to library (just for examples; you do not need this) + +import numpy as np +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg + + +app = QtGui.QApplication([]) + +w = pg.GraphicsWindow() +p1 = w.addPlot(0,0, title="X Semilog") +p2 = w.addPlot(1,0, title="Y Semilog") +p3 = w.addPlot(2,0, title="XY Log") +p1.showGrid(True, True) +p2.showGrid(True, True) +p3.showGrid(True, True) +p1.setLogMode(True, False) +p2.setLogMode(False, True) +p3.setLogMode(True, True) +w.show() + +y = np.random.normal(size=1000) +x = np.linspace(0, 1, 1000) +p1.plot(x, y) +p2.plot(x, y) +p3.plot(x, y) + + + +#p.getAxis('bottom').setLogMode(True) + + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 563f3fb4..bd72561f 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -43,6 +43,7 @@ class AxisItem(GraphicsWidget): self.labelUnits = '' self.labelUnitPrefix='' self.labelStyle = {'color': '#CCC'} + self.logMode = False self.textHeight = 18 self.tickLength = maxTickLength @@ -76,6 +77,10 @@ class AxisItem(GraphicsWidget): self.prepareGeometryChange() self.update() + def setLogMode(self, log): + self.logMode = log + self.picture = None + self.update() def resizeEvent(self, ev=None): #s = self.size() @@ -316,6 +321,9 @@ class AxisItem(GraphicsWidget): By default, this method calls tickSpacing to determine the correct tick locations. This is a good method to override in subclasses. """ + if self.logMode: + return self.logTickValues(minVal, maxVal, size) + ticks = [] tickLevels = self.tickSpacing(minVal, maxVal, size) for i in range(len(tickLevels)): @@ -329,6 +337,16 @@ class AxisItem(GraphicsWidget): ticks.append((spacing, np.arange(num) * spacing + start)) return ticks + def logTickValues(self, minVal, maxVal, size): + v1 = int(np.floor(minVal)) + v2 = int(np.ceil(maxVal)) + major = range(v1+1, v2) + + minor = [] + for v in range(v1, v2): + minor.extend(v + np.log10(np.arange(1, 10))) + minor = filter(lambda x: x>minVal and x