├── .gitignore ├── LICENSE.md ├── QtQuick_controls_v1 ├── MainForm.ui.qml ├── QtQuickControls1.PNG ├── SubplotTool.qml ├── main.qml ├── mpl_qtquick1.bat ├── mpl_qtquick1.py └── qml.qrc ├── QtQuick_controls_v2 ├── COPYING-ICONS.txt ├── MainForm.ui.qml ├── QtQuickControls2.PNG ├── SubplotTool.qml ├── document-open.svg ├── help-about.svg ├── main.qml ├── mpl_qtquick2.bat ├── mpl_qtquick2.py ├── qml.qrc └── window-close.svg ├── QtWidgets ├── QtWidgets_UI.PNG ├── mpl_qtwidgets.bat └── mpl_qtwidgets.py ├── README.md ├── backend ├── backend_qtquick5 │ ├── Figure.qml │ ├── FigureToolbar.qml │ ├── SubplotTool.qml │ ├── __init__.py │ └── backend_qquick5agg.py ├── mpl_qquick.bat ├── mpl_qquick.py ├── mpl_qquick_toolbar.bat └── mpl_qquick_toolbar.py ├── environment.yml └── qt_mpl_data.csv /.gitignore: -------------------------------------------------------------------------------- 1 | backend/backend_qtquick5/__pycache__ 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Frédéric Collonval 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QtQuick_controls_v1/MainForm.ui.qml: -------------------------------------------------------------------------------- 1 | import Backend 1.0 2 | import QtQuick 2.6 3 | import QtQuick.Controls 1.5 4 | import QtQuick.Layouts 1.3 5 | import QtQuick.Dialogs 1.2 6 | 7 | Item { 8 | width: 640 9 | height: 320 10 | 11 | 12 | RowLayout { 13 | id: hbox 14 | spacing: 5 15 | anchors.horizontalCenter: parent.horizontalCenter 16 | anchors.fill: parent 17 | 18 | ColumnLayout { 19 | spacing : 0 20 | 21 | FigureToolbar { 22 | id: mplView 23 | objectName : "figure" 24 | width : 480 25 | height: 320 26 | 27 | Layout.fillWidth: true 28 | Layout.fillHeight: true 29 | 30 | Layout.minimumWidth: 10 31 | Layout.minimumHeight: 10 32 | } 33 | 34 | MessageDialog { 35 | id: messageDialog 36 | } 37 | 38 | FileDialog { 39 | id: saveFileDialog 40 | title: "Choose a filename to save to" 41 | folder: mplView.defaultDirectory 42 | nameFilters: mplView.fileFilters 43 | selectedNameFilter: mplView.defaultFileFilter 44 | selectExisting: false 45 | 46 | onAccepted: { 47 | try{ 48 | mplView.print_figure(fileUrl) 49 | } 50 | catch (error){ 51 | messageDialog.title = "Error saving file" 52 | messageDialog.text = error 53 | messageDialog.icon = StandardIcon.Critical 54 | messageDialog.open() 55 | } 56 | } 57 | } 58 | 59 | SubplotTool { 60 | id: setMargin 61 | 62 | left.value: mplView.left 63 | right.value: mplView.right 64 | top.value: mplView.top 65 | bottom.value: mplView.bottom 66 | 67 | hspace.value: mplView.hspace 68 | wspace.value: mplView.wspace 69 | 70 | function initMargin() { 71 | // Init slider value 72 | setMargin.left.value = mplView.left 73 | setMargin.right.value = mplView.right 74 | setMargin.top.value = mplView.top 75 | setMargin.bottom.value = mplView.bottom 76 | 77 | setMargin.hspace.value = mplView.hspace 78 | setMargin.wspace.value = mplView.wspace 79 | 80 | // Invert parameter bindings 81 | mplView.left = Qt.binding(function() { return setMargin.left.value }) 82 | mplView.right = Qt.binding(function() { return setMargin.right.value }) 83 | mplView.top = Qt.binding(function() { return setMargin.top.value }) 84 | mplView.bottom = Qt.binding(function() { return setMargin.bottom.value }) 85 | 86 | mplView.hspace = Qt.binding(function() { return setMargin.hspace.value }) 87 | mplView.wspace = Qt.binding(function() { return setMargin.wspace.value }) 88 | } 89 | 90 | onReset: { 91 | mplView.reset_margin() 92 | setMargin.initMargin() 93 | } 94 | 95 | onTightLayout: { 96 | mplView.tight_layout() 97 | setMargin.initMargin() 98 | } 99 | } 100 | 101 | 102 | ToolBar { 103 | id: toolbar 104 | 105 | Layout.alignment: Qt.AlignLeft | Qt.Bottom 106 | Layout.fillWidth: true 107 | 108 | RowLayout { 109 | Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter 110 | anchors.fill: parent 111 | 112 | ToolButton { 113 | id : home 114 | 115 | iconSource: "image://mplIcons/home" 116 | 117 | onClicked: { 118 | mplView.home() 119 | } 120 | } 121 | 122 | ToolButton { 123 | id : back 124 | iconSource: "image://mplIcons/back" 125 | 126 | onClicked: { 127 | mplView.back() 128 | } 129 | } 130 | 131 | ToolButton { 132 | id : forward 133 | 134 | iconSource: "image://mplIcons/forward" 135 | 136 | onClicked: { 137 | mplView.forward() 138 | } 139 | } 140 | 141 | // Fake separator 142 | Label { 143 | text : "|" 144 | } 145 | 146 | 147 | ExclusiveGroup { 148 | // Gather pan and zoom tools to make them auto-exclusive 149 | id: pan_zoom 150 | } 151 | 152 | ToolButton { 153 | id : pan 154 | 155 | iconSource: "image://mplIcons/move" 156 | 157 | exclusiveGroup: pan_zoom 158 | checkable: true 159 | 160 | onClicked: { 161 | mplView.pan() 162 | } 163 | } 164 | 165 | ToolButton { 166 | id : zoom 167 | 168 | iconSource: "image://mplIcons/zoom_to_rect" 169 | 170 | exclusiveGroup: pan_zoom 171 | checkable: true 172 | 173 | onClicked: { 174 | mplView.zoom() 175 | } 176 | } 177 | 178 | Label { 179 | text : "|" 180 | } 181 | 182 | ToolButton { 183 | id : subplots 184 | iconSource: "image://mplIcons/subplots" 185 | 186 | onClicked: { 187 | setMargin.initMargin() 188 | setMargin.open() 189 | } 190 | } 191 | 192 | ToolButton { 193 | id : save 194 | 195 | iconSource: "image://mplIcons/filesave" 196 | 197 | onClicked: { 198 | saveFileDialog.open() 199 | } 200 | } 201 | /* 202 | ToolButton { 203 | id : figureOptions 204 | 205 | iconSource: "image://mplIcons/qt4_editor_options" 206 | 207 | visible: mplView.figureOptions 208 | 209 | onClicked: { 210 | } 211 | } 212 | */ 213 | Item { 214 | Layout.fillWidth: true 215 | } 216 | 217 | Label{ 218 | id: locLabel 219 | 220 | Layout.alignment: Qt.AlignRight | Qt.AlignVCenter 221 | 222 | text: mplView.message 223 | } 224 | } 225 | } 226 | } 227 | 228 | Connections { 229 | target: dataModel 230 | onDataChanged: { 231 | draw_mpl.update_figure() 232 | } 233 | } 234 | 235 | Rectangle { 236 | id: right 237 | width: 160 238 | Layout.alignment: Qt.AlignLeft | Qt.AlignTop 239 | Layout.fillHeight: true 240 | 241 | ColumnLayout { 242 | id: right_vbox 243 | 244 | spacing: 2 245 | 246 | Label { 247 | id: log_label 248 | text: qsTr("Data series:") 249 | } 250 | 251 | ListView { 252 | id: series_list_view 253 | width: 110 254 | height: 160 255 | Layout.fillWidth: true 256 | model: dataModel 257 | delegate: RowLayout { 258 | CheckBox { 259 | checked : selected 260 | 261 | onClicked: { 262 | selected = checked; 263 | } 264 | } 265 | 266 | Text { 267 | text: name 268 | anchors.verticalCenter: parent.verticalCenter 269 | } 270 | spacing: 10 271 | } 272 | } 273 | 274 | RowLayout { 275 | id: rowLayout1 276 | width: 100 277 | height: 100 278 | 279 | Label { 280 | id: spin_label1 281 | text: qsTr("X from") 282 | } 283 | 284 | SpinBox { 285 | id: from_spin 286 | value: draw_mpl.xFrom 287 | minimumValue: 0 288 | maximumValue: dataModel.lengthData - 1; 289 | enabled: series_list_view.count > 0; 290 | } 291 | 292 | Binding { 293 | target: draw_mpl 294 | property: "xFrom" 295 | value: from_spin.value 296 | } 297 | 298 | Label { 299 | id: spin_label2 300 | text: qsTr("to") 301 | } 302 | 303 | SpinBox { 304 | id: to_spin 305 | value: draw_mpl.xTo 306 | minimumValue: 0 307 | maximumValue: dataModel.lengthData - 1; 308 | enabled: series_list_view.count > 0; 309 | } 310 | 311 | Binding { 312 | target: draw_mpl 313 | property: "xTo" 314 | value: to_spin.value 315 | } 316 | } 317 | 318 | CheckBox { 319 | id: legend_cb 320 | text: qsTr("Show Legend") 321 | checked: draw_mpl.legend 322 | } 323 | 324 | Binding { 325 | target: draw_mpl 326 | property: "legend" 327 | value: legend_cb.checked 328 | } 329 | } 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /QtQuick_controls_v1/QtQuickControls1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcollonval/matplotlib_qtquick_playground/21de5e9734e26ebc1dbd8d7fafc023248d9ce607/QtQuick_controls_v1/QtQuickControls1.PNG -------------------------------------------------------------------------------- /QtQuick_controls_v1/SubplotTool.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Layouts 1.2 3 | import QtQuick.Controls 1.0 4 | import QtQuick.Dialogs 1.2 5 | 6 | Dialog { 7 | id: subplotTool 8 | 9 | title: "Margins & spacing" 10 | 11 | property alias left: left_slider 12 | property alias right: right_slider 13 | property alias top: top_slider 14 | property alias bottom: bottom_slider 15 | property alias hspace: hspace_slider 16 | property alias wspace: wspace_slider 17 | 18 | signal reset 19 | signal tightLayout 20 | 21 | contentItem : ColumnLayout { 22 | anchors.fill: parent 23 | 24 | GroupBox { 25 | id: borders 26 | title: "Borders" 27 | 28 | Layout.alignment: Qt.AlignTop | Qt.AlignHCenter 29 | Layout.fillWidth: true 30 | Layout.fillHeight: true 31 | 32 | GridLayout{ 33 | columns: 3 34 | anchors.fill: parent 35 | 36 | Label { 37 | text: "top" 38 | } 39 | Slider{ 40 | id: top_slider 41 | minimumValue: bottom_slider.value 42 | maximumValue: 1 43 | value: 1 44 | stepSize: 0.01 45 | 46 | Layout.fillWidth: true 47 | } 48 | Label{ 49 | text: top_slider.value.toFixed(2) 50 | } 51 | 52 | Label { 53 | text: "bottom" 54 | } 55 | Slider{ 56 | id: bottom_slider 57 | minimumValue: 0 58 | maximumValue: top_slider.value 59 | value: 0 60 | stepSize: 0.01 61 | 62 | Layout.fillWidth: true 63 | } 64 | Label{ 65 | text: bottom_slider.value.toFixed(2) 66 | } 67 | 68 | Label { 69 | text: "left" 70 | } 71 | Slider{ 72 | id: left_slider 73 | minimumValue: 0 74 | maximumValue: right_slider.value 75 | value: 0 76 | stepSize: 0.01 77 | 78 | Layout.fillWidth: true 79 | } 80 | Label{ 81 | text: left_slider.value.toFixed(2) 82 | } 83 | 84 | Label { 85 | text: "right" 86 | } 87 | Slider{ 88 | id: right_slider 89 | minimumValue: left_slider.value 90 | maximumValue: 1 91 | value: 1 92 | stepSize: 0.01 93 | 94 | Layout.fillWidth: true 95 | } 96 | Label{ 97 | text: right_slider.value.toFixed(2) 98 | } 99 | 100 | } 101 | } 102 | 103 | GroupBox { 104 | id: spacings 105 | title: "Spacings" 106 | 107 | Layout.alignment: Qt.AlignTop | Qt.AlignHCenter 108 | Layout.fillWidth: true 109 | Layout.fillHeight: true 110 | 111 | GridLayout{ 112 | columns: 3 113 | anchors.fill: parent 114 | 115 | Label { 116 | text: "hspace" 117 | } 118 | Slider{ 119 | id: hspace_slider 120 | minimumValue: 0 121 | maximumValue: 1 122 | value: 0 123 | stepSize: 0.01 124 | 125 | Layout.fillWidth: true 126 | } 127 | Label{ 128 | text: hspace_slider.value.toFixed(2) 129 | } 130 | 131 | Label { 132 | text: "wspace" 133 | } 134 | Slider{ 135 | id: wspace_slider 136 | minimumValue: 0 137 | maximumValue: 1 138 | value: 0 139 | stepSize: 0.01 140 | 141 | Layout.fillWidth: true 142 | } 143 | Label{ 144 | text: wspace_slider.value.toFixed(2) 145 | } 146 | } 147 | } 148 | 149 | RowLayout { 150 | id: buttons 151 | 152 | Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter 153 | Layout.fillWidth: true 154 | 155 | Button { 156 | id: tight_layout 157 | text: "Tight Layout" 158 | 159 | Layout.alignment: Qt.AlignLeft | Qt.AlignBottom 160 | 161 | onClicked: { 162 | subplotTool.tightLayout() 163 | } 164 | } 165 | 166 | Item { 167 | Layout.fillWidth: true 168 | } 169 | 170 | Button { 171 | id: reset 172 | text: "Reset" 173 | 174 | Layout.alignment: Qt.AlignRight | Qt.AlignBottom 175 | 176 | onClicked: { 177 | subplotTool.reset() 178 | } 179 | } 180 | 181 | Button { 182 | id: close 183 | text: "Close" 184 | 185 | Layout.alignment: Qt.AlignRight | Qt.AlignBottom 186 | 187 | onClicked: { 188 | subplotTool.close() 189 | } 190 | } 191 | } 192 | 193 | } 194 | } -------------------------------------------------------------------------------- /QtQuick_controls_v1/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 1.5 3 | import QtQuick.Dialogs 1.2 4 | import QtQuick.Layouts 1.0 5 | import QtQuick.Window 2.1 6 | 7 | ApplicationWindow { 8 | visible: true 9 | width: 640 10 | height: 335 11 | title: qsTr("Hello World") 12 | 13 | FileDialog { 14 | id: fileDialog 15 | nameFilters: ["CSV files (*.csv)", "All Files (*.*)"] 16 | onAccepted: { 17 | draw_mpl.filename = fileUrl 18 | } 19 | } 20 | 21 | menuBar: MenuBar { 22 | Menu { 23 | title: qsTr("&File") 24 | MenuItem { 25 | text: qsTr("&Load a file") 26 | onTriggered: { 27 | fileDialog.open() 28 | } 29 | } 30 | MenuItem { 31 | text: qsTr("&Quit") 32 | onTriggered: Qt.quit(); 33 | } 34 | } 35 | Menu { 36 | title: qsTr("&Help") 37 | 38 | MenuItem{ 39 | text: qsTr("&About") 40 | onTriggered: messageDialog.show("About the demo", draw_mpl.about) 41 | } 42 | } 43 | } 44 | 45 | MainForm { 46 | anchors.fill: parent 47 | } 48 | 49 | statusBar: Text { 50 | text: draw_mpl.statusText 51 | } 52 | 53 | MessageDialog { 54 | id: messageDialog 55 | 56 | function show(title, caption) { 57 | messageDialog.title = title; 58 | messageDialog.text = caption; 59 | messageDialog.open(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /QtQuick_controls_v1/mpl_qtquick1.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | CALL C:\Anaconda3\Scripts\activate.bat qtquick 3 | python %~dpn0.py %* -------------------------------------------------------------------------------- /QtQuick_controls_v1/mpl_qtquick1.py: -------------------------------------------------------------------------------- 1 | """ 2 | Series of data are loaded from a .csv file, and their names are 3 | displayed in a checkable list view. The user can select the series 4 | it wants from the list and plot them on a matplotlib canvas. 5 | Use the sample .csv file that comes with the script for an example 6 | of data series. 7 | 8 | [2016-11-05] Convert to QtQuick 2.0 - QtQuick Controls 1.0 9 | [2016-11-01] Update to PyQt5.6 and python 3.5 10 | 11 | Frederic Collonval (fcollonval@gmail.com) 12 | 13 | Inspired from the work of Eli Bendersky (eliben@gmail.com): 14 | https://github.com/eliben/code-for-blog/tree/master/2009/pyqt_dataplot_demo 15 | 16 | License: MIT License 17 | Last modified: 2016-11-05 18 | """ 19 | import sys, os, csv 20 | from PyQt5.QtCore import QAbstractListModel, QModelIndex, QObject, QSize, Qt, QUrl, QVariant, pyqtProperty, pyqtSlot, pyqtSignal 21 | from PyQt5.QtGui import QGuiApplication, QColor, QImage, QPixmap 22 | # from PyQt5.QtWidgets import QApplication 23 | from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType 24 | from PyQt5.QtQuick import QQuickImageProvider 25 | 26 | import matplotlib 27 | matplotlib.use('Agg') 28 | # from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 29 | # from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 30 | # from matplotlib.figure import Figure 31 | import matplotlib.pyplot as plt 32 | 33 | import numpy as np 34 | 35 | sys.path.append('../backend') 36 | from backend_qtquick5 import FigureCanvasQTAggToolbar, MatplotlibIconProvider 37 | 38 | class DataSerie(object): 39 | 40 | def __init__(self, name, data, selected=False): 41 | self._name = name 42 | self._data = data 43 | self._selected = selected 44 | 45 | def name(self): 46 | return self._name 47 | 48 | def selected(self): 49 | return self._selected 50 | 51 | def data(self): 52 | return self._data 53 | 54 | class DataSeriesModel(QAbstractListModel): 55 | 56 | # Define role enum 57 | SelectedRole = Qt.UserRole 58 | NameRole = Qt.UserRole + 1 59 | DataRole = Qt.UserRole + 2 60 | 61 | _roles = { 62 | SelectedRole : b"selected", 63 | NameRole : b"name", 64 | DataRole : b"data" 65 | } 66 | 67 | lengthDataChanged = pyqtSignal() 68 | 69 | def __init__(self, parent=None): 70 | QAbstractListModel.__init__(self, parent) 71 | 72 | self._data_series = list() 73 | self._length_data = 0 74 | 75 | @pyqtProperty(int, notify=lengthDataChanged) 76 | def lengthData(self): 77 | return self._length_data 78 | 79 | @lengthData.setter 80 | def lengthData(self, length): 81 | if self._length_data != length: 82 | self._length_data = length 83 | self.lengthDataChanged.emit() 84 | 85 | def roleNames(self): 86 | return self._roles 87 | 88 | def load_from_file(self, filename=None): 89 | self._data_series.clear() 90 | self._length_data = 0 91 | 92 | if filename: 93 | with open(filename, 'r') as f: 94 | for line in csv.reader(f): 95 | series = DataSerie(line[0], 96 | [i for i in map(int, line[1:])]) 97 | self.add_data(series) 98 | 99 | def add_data(self, data_series): 100 | self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) 101 | self._data_series.append(data_series) 102 | self.lengthData = max(self.lengthData, len(data_series.data())) 103 | self.endInsertRows() 104 | 105 | def rowCount(self, parent=QModelIndex()): 106 | return len(self._data_series) 107 | 108 | def data(self, index, role=Qt.DisplayRole): 109 | if(index.row() < 0 or index.row() >= len(self._data_series)): 110 | return QVariant() 111 | 112 | series = self._data_series[index.row()] 113 | 114 | if role == self.SelectedRole: 115 | return series.selected() 116 | elif role == self.NameRole: 117 | return series.name() 118 | elif role == self.DataRole: 119 | return series.data() 120 | 121 | return QVariant() 122 | 123 | def setData(self, index, value, role=Qt.EditRole): 124 | if(index.row() < 0 or index.row() >= len(self._data_series)): 125 | return False 126 | 127 | series = self._data_series[index.row()] 128 | 129 | if role == self.SelectedRole: 130 | series._selected = value 131 | self.dataChanged.emit(index, index, [role,]) 132 | return True 133 | 134 | return False 135 | 136 | class Form(QObject): 137 | 138 | xFromChanged = pyqtSignal() 139 | xToChanged = pyqtSignal() 140 | legendChanged = pyqtSignal() 141 | statusTextChanged = pyqtSignal() 142 | stateChanged = pyqtSignal() 143 | 144 | def __init__(self, parent=None, data=None): 145 | QObject.__init__(self, parent) 146 | 147 | self._status_text = "Please load a data file" 148 | 149 | self._filename = "" 150 | self._x_from = 0 151 | self._x_to = 1 152 | self._legend = False 153 | 154 | # default dpi=80, so size = (480, 320) 155 | self._figure = None 156 | self.axes = None 157 | 158 | self._data = data 159 | 160 | @property 161 | def figure(self): 162 | return self._figure 163 | 164 | @figure.setter 165 | def figure(self, fig): 166 | self._figure = fig 167 | self._figure.set_facecolor('white') 168 | self.axes = self.figure.add_subplot(111) 169 | 170 | # Signal connection 171 | self.xFromChanged.connect(self._figure.canvas.draw_idle) 172 | self.xToChanged.connect(self._figure.canvas.draw_idle) 173 | self.legendChanged.connect(self._figure.canvas.draw_idle) 174 | self.stateChanged.connect(self._figure.canvas.draw_idle) 175 | 176 | @pyqtProperty('QString', notify=statusTextChanged) 177 | def statusText(self): 178 | return self._status_text 179 | 180 | @statusText.setter 181 | def statusText(self, text): 182 | if self._status_text != text: 183 | self._status_text = text 184 | self.statusTextChanged.emit() 185 | 186 | @pyqtProperty('QString') 187 | def filename(self): 188 | return self._filename 189 | 190 | @filename.setter 191 | def filename(self, filename): 192 | if filename: 193 | filename = QUrl(filename).toLocalFile() 194 | if filename != self._filename: 195 | self._filename = filename 196 | self._data.load_from_file(filename) 197 | self.statusText = "Loaded " + filename 198 | self.xTo = self._data.lengthData 199 | 200 | @pyqtProperty(int, notify=xFromChanged) 201 | def xFrom(self): 202 | return self._x_from 203 | 204 | @xFrom.setter 205 | def xFrom(self, x_from): 206 | if self.figure is None: 207 | return 208 | 209 | x_from = int(x_from) 210 | if self._x_from != x_from: 211 | self._x_from = x_from 212 | self.axes.set_xlim(left=self._x_from) 213 | self.xFromChanged.emit() 214 | 215 | @pyqtProperty(int, notify=xToChanged) 216 | def xTo(self): 217 | return self._x_to 218 | 219 | @xTo.setter 220 | def xTo(self, x_to): 221 | if self.figure is None: 222 | return 223 | 224 | x_to = int(x_to) 225 | if self._x_to != x_to: 226 | self._x_to = x_to 227 | self.axes.set_xlim(right=self._x_to) 228 | self.xToChanged.emit() 229 | 230 | @pyqtProperty(bool, notify=legendChanged) 231 | def legend(self): 232 | return self._legend 233 | 234 | @legend.setter 235 | def legend(self, legend): 236 | if self.figure is None: 237 | return 238 | 239 | if self._legend != legend: 240 | self._legend = legend 241 | if self._legend: 242 | self.axes.legend() 243 | else: 244 | leg = self.axes.get_legend() 245 | if leg is not None: 246 | leg.remove() 247 | self.legendChanged.emit() 248 | 249 | @pyqtProperty('QString', constant=True) 250 | def about(self): 251 | msg = __doc__ 252 | return msg.strip() 253 | 254 | 255 | @pyqtSlot() 256 | def update_figure(self): 257 | if self.figure is None: 258 | return 259 | 260 | self.axes.clear() 261 | self.axes.grid(True) 262 | 263 | has_series = False 264 | 265 | for row in range(self._data.rowCount()): 266 | model_index = self._data.index(row, 0) 267 | checked = self._data.data(model_index, DataSeriesModel.SelectedRole) 268 | 269 | if checked: 270 | has_series = True 271 | name = self._data.data(model_index, DataSeriesModel.NameRole) 272 | values = self._data.data(model_index, DataSeriesModel.DataRole) 273 | 274 | self.axes.plot(range(len(values)), values, 'o-', label=name) 275 | 276 | self.axes.set_xlim((self.xFrom, self.xTo)) 277 | if has_series and self.legend: 278 | self.axes.legend() 279 | 280 | self.stateChanged.emit() 281 | 282 | 283 | def main(): 284 | app = QGuiApplication(sys.argv) 285 | 286 | qmlRegisterType(FigureCanvasQTAggToolbar, "Backend", 1, 0, "FigureToolbar") 287 | 288 | imgProvider = MatplotlibIconProvider() 289 | 290 | engine = QQmlApplicationEngine(parent=app) 291 | engine.addImageProvider("mplIcons", imgProvider) 292 | 293 | context = engine.rootContext() 294 | data_model = DataSeriesModel() 295 | context.setContextProperty("dataModel", data_model) 296 | mainApp = Form(data=data_model) 297 | context.setContextProperty("draw_mpl", mainApp) 298 | 299 | engine.load(QUrl('main.qml')) 300 | 301 | win = engine.rootObjects()[0] 302 | mainApp.figure = win.findChild(QObject, "figure").getFigure() 303 | 304 | rc = app.exec_() 305 | sys.exit(rc) 306 | 307 | 308 | if __name__ == "__main__": 309 | main() -------------------------------------------------------------------------------- /QtQuick_controls_v1/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | MainForm.ui.qml 5 | 6 | 7 | -------------------------------------------------------------------------------- /QtQuick_controls_v2/COPYING-ICONS.txt: -------------------------------------------------------------------------------- 1 | The Breeze Icon Theme in icons/ 2 | 3 | Copyright (C) 2014 Uri Herrera and others 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 3 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library. If not, see . -------------------------------------------------------------------------------- /QtQuick_controls_v2/MainForm.ui.qml: -------------------------------------------------------------------------------- 1 | import Backend 1.0 2 | import QtQuick 2.6 3 | import Qt.labs.controls 1.0 4 | import QtQuick.Layouts 1.3 5 | import QtQuick.Dialogs 1.2 6 | 7 | Item { 8 | anchors.fill: parent 9 | 10 | RowLayout { 11 | id: hbox 12 | spacing: 5 13 | anchors.horizontalCenter: parent.horizontalCenter 14 | anchors.fill: parent 15 | 16 | ColumnLayout { 17 | spacing : 0 18 | width: 640 19 | height: 480 20 | 21 | Layout.fillWidth: true 22 | 23 | FigureToolbar { 24 | id: mplView 25 | objectName : "figure" 26 | 27 | Layout.fillWidth: true 28 | Layout.fillHeight: true 29 | 30 | Layout.minimumWidth: 10 31 | Layout.minimumHeight: 10 32 | } 33 | 34 | MessageDialog { 35 | id: messageDialog 36 | } 37 | 38 | FileDialog { 39 | id: saveFileDialog 40 | title: "Choose a filename to save to" 41 | folder: mplView.defaultDirectory 42 | nameFilters: mplView.fileFilters 43 | selectedNameFilter: mplView.defaultFileFilter 44 | selectExisting: false 45 | 46 | onAccepted: { 47 | try{ 48 | mplView.print_figure(fileUrl) 49 | } 50 | catch (error){ 51 | messageDialog.title = "Error saving file" 52 | messageDialog.text = error 53 | messageDialog.icon = StandardIcon.Critical 54 | messageDialog.open() 55 | } 56 | } 57 | } 58 | 59 | SubplotTool { 60 | id: setMargin 61 | 62 | left.value: mplView.left 63 | right.value: mplView.right 64 | top.value: mplView.top 65 | bottom.value: mplView.bottom 66 | 67 | hspace.value: mplView.hspace 68 | wspace.value: mplView.wspace 69 | 70 | function initMargin() { 71 | // Init slider value 72 | setMargin.left.value = mplView.left 73 | setMargin.right.value = mplView.right 74 | setMargin.top.value = mplView.top 75 | setMargin.bottom.value = mplView.bottom 76 | 77 | setMargin.hspace.value = mplView.hspace 78 | setMargin.wspace.value = mplView.wspace 79 | 80 | // Invert parameter bindings 81 | mplView.left = Qt.binding(function() { return setMargin.left.value }) 82 | mplView.right = Qt.binding(function() { return setMargin.right.value }) 83 | mplView.top = Qt.binding(function() { return setMargin.top.value }) 84 | mplView.bottom = Qt.binding(function() { return setMargin.bottom.value }) 85 | 86 | mplView.hspace = Qt.binding(function() { return setMargin.hspace.value }) 87 | mplView.wspace = Qt.binding(function() { return setMargin.wspace.value }) 88 | } 89 | 90 | onReset: { 91 | mplView.reset_margin() 92 | setMargin.initMargin() 93 | } 94 | 95 | onTightLayout: { 96 | mplView.tight_layout() 97 | setMargin.initMargin() 98 | } 99 | } 100 | 101 | 102 | ToolBar { 103 | id: toolbar 104 | height: 48 105 | 106 | Layout.maximumHeight: height 107 | Layout.minimumHeight: height 108 | Layout.alignment: Qt.AlignLeft | Qt.Bottom 109 | Layout.fillWidth: true 110 | 111 | RowLayout { 112 | Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter 113 | anchors.fill: parent 114 | spacing: 0 115 | 116 | ToolButton { 117 | id : home 118 | 119 | contentItem: Image{ 120 | fillMode: Image.PreserveAspectFit 121 | source: "image://mplIcons/home" 122 | } 123 | onClicked: { 124 | mplView.home() 125 | } 126 | } 127 | 128 | ToolButton { 129 | id : back 130 | contentItem: Image{ 131 | fillMode: Image.PreserveAspectFit 132 | source: "image://mplIcons/back" 133 | } 134 | onClicked: { 135 | mplView.back() 136 | } 137 | } 138 | 139 | ToolButton { 140 | id : forward 141 | 142 | contentItem: Image{ 143 | fillMode: Image.PreserveAspectFit 144 | source: "image://mplIcons/forward" 145 | } 146 | onClicked: { 147 | mplView.forward() 148 | } 149 | } 150 | 151 | // Fake separator 152 | Label { 153 | text : "|" 154 | } 155 | 156 | ButtonGroup { 157 | // Gather pan and zoom tools to make them auto-exclusive 158 | id: pan_zoom 159 | } 160 | 161 | ToolButton { 162 | id : pan 163 | 164 | contentItem: Image{ 165 | fillMode: Image.PreserveAspectFit 166 | source: "image://mplIcons/move" 167 | } 168 | 169 | ButtonGroup.group: pan_zoom 170 | checkable: true 171 | 172 | onClicked: { 173 | mplView.pan() 174 | } 175 | } 176 | 177 | ToolButton { 178 | id : zoom 179 | 180 | contentItem: Image{ 181 | fillMode: Image.PreserveAspectFit 182 | source: "image://mplIcons/zoom_to_rect" 183 | } 184 | 185 | ButtonGroup.group: pan_zoom 186 | checkable: true 187 | 188 | onClicked: { 189 | mplView.zoom() 190 | } 191 | } 192 | 193 | Label { 194 | text : "|" 195 | } 196 | 197 | ToolButton { 198 | id : subplots 199 | contentItem: Image{ 200 | fillMode: Image.PreserveAspectFit 201 | source: "image://mplIcons/subplots" 202 | } 203 | onClicked: { 204 | setMargin.initMargin() 205 | setMargin.open() 206 | } 207 | } 208 | 209 | ToolButton { 210 | id : save 211 | contentItem: Image{ 212 | fillMode: Image.PreserveAspectFit 213 | source: "image://mplIcons/filesave" 214 | } 215 | onClicked: { 216 | saveFileDialog.open() 217 | } 218 | } 219 | /* 220 | ToolButton { 221 | id : figureOptions 222 | 223 | contentItem: Image{ 224 | fillMode: Image.PreserveAspectFit 225 | source: "image://mplIcons/qt4_editor_options" 226 | } 227 | 228 | visible: mplView.figureOptions 229 | 230 | onClicked: { 231 | } 232 | } 233 | */ 234 | Item { 235 | Layout.fillWidth: true 236 | } 237 | 238 | Label{ 239 | id: locLabel 240 | 241 | Layout.alignment: Qt.AlignRight | Qt.AlignVCenter 242 | 243 | text: mplView.message 244 | } 245 | } 246 | } 247 | } 248 | 249 | Connections { 250 | target: dataModel 251 | onDataChanged: { 252 | draw_mpl.update_figure() 253 | } 254 | } 255 | 256 | Pane { 257 | id: right 258 | Layout.alignment: Qt.AlignLeft | Qt.AlignTop 259 | Layout.fillHeight: true 260 | 261 | ColumnLayout { 262 | id: right_vbox 263 | 264 | spacing: 2 265 | 266 | Label { 267 | id: log_label 268 | text: qsTr("Data series:") 269 | } 270 | 271 | ListView { 272 | id: series_list_view 273 | height: 180 274 | Layout.fillWidth: true 275 | 276 | clip: true 277 | 278 | model: dataModel 279 | delegate: CheckBox { 280 | checked : false; 281 | text: name 282 | onClicked: { 283 | selected = checked; 284 | } 285 | } 286 | } 287 | 288 | RowLayout { 289 | id: rowLayout1 290 | Layout.fillWidth: true 291 | 292 | Label { 293 | id: spin_label1 294 | text: qsTr("X") 295 | } 296 | 297 | RangeSlider { 298 | id: xSlider 299 | first.value: draw_mpl.xFrom 300 | second.value: draw_mpl.xTo 301 | from: 0 302 | to: dataModel.lengthData - 1; 303 | enabled: series_list_view.count > 0; 304 | 305 | } 306 | 307 | Binding { 308 | target: draw_mpl 309 | property: "xFrom" 310 | value: xSlider.first.value 311 | } 312 | 313 | Binding { 314 | target: draw_mpl 315 | property: "xTo" 316 | value: xSlider.second.value 317 | } 318 | 319 | } 320 | 321 | Switch { 322 | id: legend_cb 323 | text: qsTr("Show Legend") 324 | checked: draw_mpl.legend 325 | } 326 | 327 | Binding { 328 | target: draw_mpl 329 | property: "legend" 330 | value: legend_cb.checked 331 | } 332 | 333 | } 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /QtQuick_controls_v2/QtQuickControls2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcollonval/matplotlib_qtquick_playground/21de5e9734e26ebc1dbd8d7fafc023248d9ce607/QtQuick_controls_v2/QtQuickControls2.PNG -------------------------------------------------------------------------------- /QtQuick_controls_v2/SubplotTool.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Layouts 1.2 3 | import Qt.labs.controls 1.0 4 | import QtQuick.Dialogs 1.2 5 | 6 | Dialog { 7 | id: subplotTool 8 | 9 | title: "Margins & spacing" 10 | 11 | property alias left: left_slider 12 | property alias right: right_slider 13 | property alias top: top_slider 14 | property alias bottom: bottom_slider 15 | property alias hspace: hspace_slider 16 | property alias wspace: wspace_slider 17 | 18 | signal reset 19 | signal tightLayout 20 | 21 | contentItem : ColumnLayout { 22 | anchors.fill: parent 23 | 24 | GroupBox { 25 | id: borders 26 | title: "Borders" 27 | 28 | Layout.alignment: Qt.AlignTop | Qt.AlignHCenter 29 | Layout.fillWidth: true 30 | Layout.fillHeight: true 31 | 32 | GridLayout{ 33 | columns: 3 34 | anchors.fill: parent 35 | 36 | Label { 37 | text: "top" 38 | } 39 | Slider{ 40 | id: top_slider 41 | from: bottom_slider.value 42 | to: 1 43 | value: 1 44 | stepSize: 0.01 45 | snapMode: Slider.SnapOnRelease 46 | 47 | Layout.fillWidth: true 48 | } 49 | Label{ 50 | text: top_slider.value.toFixed(2) 51 | } 52 | 53 | Label { 54 | text: "bottom" 55 | } 56 | Slider{ 57 | id: bottom_slider 58 | from: 0 59 | to: top_slider.value 60 | value: 0 61 | stepSize: 0.01 62 | snapMode: Slider.SnapOnRelease 63 | 64 | Layout.fillWidth: true 65 | } 66 | Label{ 67 | text: bottom_slider.value.toFixed(2) 68 | } 69 | 70 | Label { 71 | text: "left" 72 | } 73 | Slider{ 74 | id: left_slider 75 | from: 0 76 | to: right_slider.value 77 | value: 0 78 | stepSize: 0.01 79 | snapMode: Slider.SnapOnRelease 80 | 81 | Layout.fillWidth: true 82 | } 83 | Label{ 84 | text: left_slider.value.toFixed(2) 85 | } 86 | 87 | Label { 88 | text: "right" 89 | } 90 | Slider{ 91 | id: right_slider 92 | from: left_slider.value 93 | to: 1 94 | value: 1 95 | stepSize: 0.01 96 | snapMode: Slider.SnapOnRelease 97 | 98 | Layout.fillWidth: true 99 | } 100 | Label{ 101 | text: right_slider.value.toFixed(2) 102 | } 103 | 104 | } 105 | } 106 | 107 | GroupBox { 108 | id: spacings 109 | title: "Spacings" 110 | 111 | Layout.alignment: Qt.AlignTop | Qt.AlignHCenter 112 | Layout.fillWidth: true 113 | Layout.fillHeight: true 114 | 115 | GridLayout{ 116 | columns: 3 117 | anchors.fill: parent 118 | 119 | Label { 120 | text: "hspace" 121 | } 122 | Slider{ 123 | id: hspace_slider 124 | from: 0 125 | to: 1 126 | value: 0 127 | stepSize: 0.01 128 | snapMode: Slider.SnapOnRelease 129 | 130 | Layout.fillWidth: true 131 | } 132 | Label{ 133 | text: hspace_slider.value.toFixed(2) 134 | } 135 | 136 | Label { 137 | text: "wspace" 138 | } 139 | Slider{ 140 | id: wspace_slider 141 | from: 0 142 | to: 1 143 | value: 0 144 | stepSize: 0.01 145 | snapMode: Slider.SnapOnRelease 146 | 147 | Layout.fillWidth: true 148 | } 149 | Label{ 150 | text: wspace_slider.value.toFixed(2) 151 | } 152 | } 153 | } 154 | 155 | RowLayout { 156 | id: buttons 157 | 158 | anchors.bottom: parent.bottom 159 | Layout.fillWidth: true 160 | 161 | Button { 162 | id: tight_layout 163 | text: "Tight Layout" 164 | 165 | Layout.alignment: Qt.AlignLeft | Qt.AlignBottom 166 | 167 | onClicked: { 168 | subplotTool.tightLayout() 169 | } 170 | } 171 | 172 | Item { 173 | Layout.fillWidth: true 174 | } 175 | 176 | Button { 177 | id: reset 178 | text: "Reset" 179 | 180 | Layout.alignment: Qt.AlignRight | Qt.AlignBottom 181 | 182 | onClicked: { 183 | subplotTool.reset() 184 | } 185 | } 186 | 187 | Button { 188 | id: close 189 | text: "Close" 190 | 191 | Layout.alignment: Qt.AlignRight | Qt.AlignBottom 192 | 193 | onClicked: { 194 | subplotTool.close() 195 | } 196 | } 197 | } 198 | 199 | } 200 | } -------------------------------------------------------------------------------- /QtQuick_controls_v2/document-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /QtQuick_controls_v2/help-about.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /QtQuick_controls_v2/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import Qt.labs.controls 1.0 3 | import QtQuick.Dialogs 1.2 4 | import QtQuick.Layouts 1.0 5 | import QtQuick.Window 2.1 6 | import Qt.labs.controls.material 1.0 7 | import Qt.labs.controls.universal 1.0 8 | 9 | ApplicationWindow { 10 | id: root 11 | visible: true 12 | width: 940 13 | height: 500 14 | title: qsTr("Hello World") 15 | 16 | Material.theme : Material.Light 17 | Material.accent : Material.LightGreen 18 | Universal.theme : Universal.Light 19 | Universal.accent : Universal.Amber 20 | 21 | FileDialog { 22 | id: fileDialog 23 | nameFilters: ["CSV files (*.csv)", "All Files (*.*)"] 24 | onAccepted: { 25 | draw_mpl.filename = fileUrl 26 | } 27 | } 28 | 29 | header: ToolBar { 30 | id: head 31 | 32 | RowLayout { 33 | Layout.alignment: Qt.AlignLeft | Qt.AlignTop 34 | anchors.fill: parent 35 | ToolButton { 36 | width: 32 37 | // text: qsTr("Load a file") 38 | contentItem: Image{ 39 | fillMode: Image.PreserveAspectFit 40 | source: "document-open.svg" 41 | } 42 | onClicked: { 43 | fileDialog.open() 44 | } 45 | } 46 | ToolButton{ 47 | width: 32 48 | // text: qsTr("About") 49 | contentItem: Image{ 50 | fillMode: Image.PreserveAspectFit 51 | source: "help-about.svg" 52 | } 53 | onClicked: messageDialog.show("About the demo", draw_mpl.about) 54 | } 55 | ToolButton { 56 | width: 32 57 | // text: qsTr("Quit") 58 | contentItem: Image{ 59 | fillMode: Image.PreserveAspectFit 60 | source: "window-close.svg" 61 | } 62 | onClicked: Qt.quit(); 63 | } 64 | Item { 65 | Layout.fillWidth: true 66 | } 67 | } 68 | } 69 | 70 | MainForm { 71 | id: mainView 72 | width: parent.width 73 | height: 320 74 | 75 | transform: [ 76 | Scale { 77 | id: scale; 78 | xScale: yScale; 79 | yScale: Math.min(root.width/mainView.width, 80 | (root.height-head.height-foot.height)/mainView.height); 81 | }, 82 | Translate { 83 | x: (root.width-mainView.width*scale.xScale)/2; 84 | y: (root.height-head.height-foot.height-mainView.height*scale.yScale)/2;} 85 | ] 86 | } 87 | 88 | footer: Label { 89 | id: foot 90 | text: draw_mpl.statusText 91 | } 92 | 93 | MessageDialog { 94 | id: messageDialog 95 | 96 | function show(title, caption) { 97 | messageDialog.title = title; 98 | messageDialog.text = caption; 99 | messageDialog.open(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /QtQuick_controls_v2/mpl_qtquick2.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | CALL C:\Anaconda3\Scripts\activate.bat qtquick 3 | python %~dpn0.py %* -------------------------------------------------------------------------------- /QtQuick_controls_v2/mpl_qtquick2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Series of data are loaded from a .csv file, and their names are 3 | displayed in a checkable list view. The user can select the series 4 | it wants from the list and plot them on a matplotlib canvas. 5 | Use the sample .csv file that comes with the script for an example 6 | of data series. 7 | 8 | [2016-11-06] Convert to QtQuick 2.0 - Qt.labs.controls 1.0 9 | [2016-11-05] Convert to QtQuick 2.0 - QtQuick Controls 1.0 10 | [2016-11-01] Update to PyQt5.6 and python 3.5 11 | 12 | Frederic Collonval (fcollonval@gmail.com) 13 | 14 | Inspired from the work of Eli Bendersky (eliben@gmail.com): 15 | https://github.com/eliben/code-for-blog/tree/master/2009/pyqt_dataplot_demo 16 | 17 | License: MIT License 18 | Last modified: 2016-11-06 19 | """ 20 | import sys, os, csv 21 | from PyQt5.QtCore import QAbstractListModel, QModelIndex, QObject, QSize, Qt, QUrl, QVariant, pyqtProperty, pyqtSlot, pyqtSignal 22 | from PyQt5.QtGui import QGuiApplication, QColor, QImage, QPixmap 23 | # from PyQt5.QtWidgets import QApplication 24 | from PyQt5.QtQml import QQmlApplicationEngine, qmlRegisterType 25 | from PyQt5.QtQuick import QQuickImageProvider 26 | 27 | import matplotlib 28 | matplotlib.use('Agg') 29 | # from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 30 | # from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 31 | # from matplotlib.figure import Figure 32 | import matplotlib.pyplot as plt 33 | 34 | import numpy as np 35 | 36 | sys.path.append('../backend') 37 | from backend_qtquick5 import FigureCanvasQTAggToolbar, MatplotlibIconProvider 38 | 39 | class DataSerie(object): 40 | 41 | def __init__(self, name, data, selected=False): 42 | self._name = name 43 | self._data = data 44 | self._selected = selected 45 | 46 | def name(self): 47 | return self._name 48 | 49 | def selected(self): 50 | return self._selected 51 | 52 | def data(self): 53 | return self._data 54 | 55 | class DataSeriesModel(QAbstractListModel): 56 | 57 | # Define role enum 58 | SelectedRole = Qt.UserRole 59 | NameRole = Qt.UserRole + 1 60 | DataRole = Qt.UserRole + 2 61 | 62 | _roles = { 63 | SelectedRole : b"selected", 64 | NameRole : b"name", 65 | DataRole : b"data" 66 | } 67 | 68 | lengthDataChanged = pyqtSignal() 69 | 70 | def __init__(self, parent=None): 71 | QAbstractListModel.__init__(self, parent) 72 | 73 | self._data_series = list() 74 | self._length_data = 0 75 | 76 | @pyqtProperty(int, notify=lengthDataChanged) 77 | def lengthData(self): 78 | return self._length_data 79 | 80 | @lengthData.setter 81 | def lengthData(self, length): 82 | if self._length_data != length: 83 | self._length_data = length 84 | self.lengthDataChanged.emit() 85 | 86 | def roleNames(self): 87 | return self._roles 88 | 89 | def load_from_file(self, filename=None): 90 | self._data_series.clear() 91 | self._length_data = 0 92 | 93 | if filename: 94 | with open(filename, 'r') as f: 95 | for line in csv.reader(f): 96 | series = DataSerie(line[0], 97 | [i for i in map(int, line[1:])]) 98 | self.add_data(series) 99 | 100 | def add_data(self, data_series): 101 | self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) 102 | self._data_series.append(data_series) 103 | self.lengthData = max(self.lengthData, len(data_series.data())) 104 | self.endInsertRows() 105 | 106 | def rowCount(self, parent=QModelIndex()): 107 | return len(self._data_series) 108 | 109 | def data(self, index, role=Qt.DisplayRole): 110 | if(index.row() < 0 or index.row() >= len(self._data_series)): 111 | return QVariant() 112 | 113 | series = self._data_series[index.row()] 114 | 115 | if role == self.SelectedRole: 116 | return series.selected() 117 | elif role == self.NameRole: 118 | return series.name() 119 | elif role == self.DataRole: 120 | return series.data() 121 | 122 | return QVariant() 123 | 124 | def setData(self, index, value, role=Qt.EditRole): 125 | if(index.row() < 0 or index.row() >= len(self._data_series)): 126 | return False 127 | 128 | series = self._data_series[index.row()] 129 | 130 | if role == self.SelectedRole: 131 | series._selected = not value 132 | self.dataChanged.emit(index, index, [role,]) 133 | return True 134 | 135 | return False 136 | 137 | class Form(QObject): 138 | 139 | xFromChanged = pyqtSignal() 140 | xToChanged = pyqtSignal() 141 | legendChanged = pyqtSignal() 142 | statusTextChanged = pyqtSignal() 143 | stateChanged = pyqtSignal() 144 | 145 | def __init__(self, parent=None, data=None): 146 | QObject.__init__(self, parent) 147 | 148 | self._status_text = "Please load a data file" 149 | 150 | self._filename = "" 151 | self._x_from = 0 152 | self._x_to = 1 153 | self._legend = False 154 | 155 | # default dpi=80, so size = (480, 320) 156 | self._figure = None 157 | self.axes = None 158 | 159 | self._data = data 160 | 161 | @property 162 | def figure(self): 163 | return self._figure 164 | 165 | @figure.setter 166 | def figure(self, fig): 167 | self._figure = fig 168 | self._figure.set_facecolor('white') 169 | self.axes = self.figure.add_subplot(111) 170 | 171 | # Signal connection 172 | self.xFromChanged.connect(self._figure.canvas.draw_idle) 173 | self.xToChanged.connect(self._figure.canvas.draw_idle) 174 | self.legendChanged.connect(self._figure.canvas.draw_idle) 175 | self.stateChanged.connect(self._figure.canvas.draw_idle) 176 | 177 | @pyqtProperty('QString', notify=statusTextChanged) 178 | def statusText(self): 179 | return self._status_text 180 | 181 | @statusText.setter 182 | def statusText(self, text): 183 | if self._status_text != text: 184 | self._status_text = text 185 | self.statusTextChanged.emit() 186 | 187 | @pyqtProperty('QString') 188 | def filename(self): 189 | return self._filename 190 | 191 | @filename.setter 192 | def filename(self, filename): 193 | if filename: 194 | filename = QUrl(filename).toLocalFile() 195 | if filename != self._filename: 196 | self._filename = filename 197 | self._data.load_from_file(filename) 198 | self.statusText = "Loaded " + filename 199 | self.xTo = self._data.lengthData 200 | self.update_figure() 201 | 202 | @pyqtProperty(int, notify=xFromChanged) 203 | def xFrom(self): 204 | return self._x_from 205 | 206 | @xFrom.setter 207 | def xFrom(self, x_from): 208 | if self.figure is None: 209 | return 210 | 211 | x_from = int(x_from) 212 | if self._x_from != x_from: 213 | self._x_from = x_from 214 | self.axes.set_xlim(left=self._x_from) 215 | self.xFromChanged.emit() 216 | 217 | @pyqtProperty(int, notify=xToChanged) 218 | def xTo(self): 219 | return self._x_to 220 | 221 | @xTo.setter 222 | def xTo(self, x_to): 223 | if self.figure is None: 224 | return 225 | 226 | x_to = int(x_to) 227 | if self._x_to != x_to: 228 | self._x_to = x_to 229 | self.axes.set_xlim(right=self._x_to) 230 | self.xToChanged.emit() 231 | 232 | @pyqtProperty(bool, notify=legendChanged) 233 | def legend(self): 234 | return self._legend 235 | 236 | @legend.setter 237 | def legend(self, legend): 238 | if self.figure is None: 239 | return 240 | 241 | if self._legend != legend: 242 | self._legend = legend 243 | if self._legend: 244 | self.axes.legend() 245 | else: 246 | leg = self.axes.get_legend() 247 | if leg is not None: 248 | leg.remove() 249 | self.legendChanged.emit() 250 | 251 | @pyqtProperty('QString', constant=True) 252 | def about(self): 253 | msg = __doc__ 254 | return msg.strip() 255 | 256 | 257 | @pyqtSlot() 258 | def update_figure(self): 259 | if self.figure is None: 260 | return 261 | 262 | self.axes.clear() 263 | self.axes.grid(True) 264 | 265 | has_series = False 266 | 267 | for row in range(self._data.rowCount()): 268 | model_index = self._data.index(row, 0) 269 | checked = self._data.data(model_index, DataSeriesModel.SelectedRole) 270 | 271 | if checked: 272 | has_series = True 273 | name = self._data.data(model_index, DataSeriesModel.NameRole) 274 | values = self._data.data(model_index, DataSeriesModel.DataRole) 275 | 276 | self.axes.plot(range(len(values)), values, 'o-', label=name) 277 | 278 | self.axes.set_xlim((self.xFrom, self.xTo)) 279 | if has_series and self.legend: 280 | self.axes.legend() 281 | 282 | self.stateChanged.emit() 283 | 284 | 285 | def main(): 286 | argv = sys.argv 287 | 288 | # Trick to set the style / not found how to do it in pythonic way 289 | argv.extend(["-style", "universal"]) 290 | app = QGuiApplication(argv) 291 | 292 | qmlRegisterType(FigureCanvasQTAggToolbar, "Backend", 1, 0, "FigureToolbar") 293 | imgProvider = MatplotlibIconProvider() 294 | 295 | # !! You must specified the QApplication as parent of QQmlApplicationEngine 296 | # otherwise a segmentation fault is raised when exiting the app 297 | engine = QQmlApplicationEngine(parent=app) 298 | engine.addImageProvider("mplIcons", imgProvider) 299 | 300 | context = engine.rootContext() 301 | data_model = DataSeriesModel() 302 | context.setContextProperty("dataModel", data_model) 303 | mainApp = Form(data=data_model) 304 | context.setContextProperty("draw_mpl", mainApp) 305 | 306 | engine.load(QUrl('main.qml')) 307 | 308 | win = engine.rootObjects()[0] 309 | mainApp.figure = win.findChild(QObject, "figure").getFigure() 310 | 311 | rc = app.exec_() 312 | # There is some trouble arising when deleting all the objects here 313 | # but I have not figure out how to solve the error message. 314 | # It looks like 'app' is destroyed before some QObject 315 | sys.exit(rc) 316 | 317 | 318 | if __name__ == "__main__": 319 | main() -------------------------------------------------------------------------------- /QtQuick_controls_v2/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | MainForm.ui.qml 5 | 6 | 7 | -------------------------------------------------------------------------------- /QtQuick_controls_v2/window-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /QtWidgets/QtWidgets_UI.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcollonval/matplotlib_qtquick_playground/21de5e9734e26ebc1dbd8d7fafc023248d9ce607/QtWidgets/QtWidgets_UI.PNG -------------------------------------------------------------------------------- /QtWidgets/mpl_qtwidgets.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | CALL C:\Anaconda3\Scripts\activate.bat qtquick 3 | python %~dpn0.py %* -------------------------------------------------------------------------------- /QtWidgets/mpl_qtwidgets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Series of data are loaded from a .csv file, and their names are 3 | displayed in a checkable list view. The user can select the series 4 | it wants from the list and plot them on a matplotlib canvas. 5 | Use the sample .csv file that comes with the script for an example 6 | of data series. 7 | 8 | [2016-11-01] Update to PyQt5.6 and python 3.5 9 | 10 | Frederic Collonval (fcollonval@gmail.com) 11 | 12 | Inspired from the work of Eli Bendersky (eliben@gmail.com): 13 | https://github.com/eliben/code-for-blog/tree/master/2009/pyqt_dataplot_demo 14 | 15 | License: MIT License 16 | License: this code is in the public domain 17 | Last modified: 2016-11-01 18 | """ 19 | import sys, os, csv 20 | from PyQt5.QtCore import * 21 | from PyQt5.QtGui import * 22 | from PyQt5.QtWidgets import * 23 | 24 | import matplotlib 25 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 26 | from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 27 | from matplotlib.figure import Figure 28 | 29 | 30 | class Form(QMainWindow): 31 | def __init__(self, parent=None): 32 | super(Form, self).__init__(parent) 33 | self.setWindowTitle('PyQt & matplotlib demo: Data plotting') 34 | 35 | self.data = DataHolder() 36 | self.series_list_model = QStandardItemModel() 37 | 38 | self.create_menu() 39 | self.create_main_frame() 40 | self.create_status_bar() 41 | 42 | self.update_ui() 43 | self.on_show() 44 | 45 | def load_file(self, filename=None): 46 | filename, filters = QFileDialog.getOpenFileName(self, 47 | 'Open a data file', '.', 'CSV files (*.csv);;All Files (*.*)') 48 | 49 | if filename: 50 | self.data.load_from_file(filename) 51 | self.fill_series_list(self.data.series_names()) 52 | self.status_text.setText("Loaded " + filename) 53 | self.update_ui() 54 | 55 | def update_ui(self): 56 | if self.data.series_count() > 0 and self.data.series_len() > 0: 57 | self.from_spin.setValue(0) 58 | self.to_spin.setValue(self.data.series_len() - 1) 59 | 60 | for w in [self.from_spin, self.to_spin]: 61 | w.setRange(0, self.data.series_len() - 1) 62 | w.setEnabled(True) 63 | else: 64 | for w in [self.from_spin, self.to_spin]: 65 | w.setEnabled(False) 66 | 67 | def on_show(self): 68 | self.axes.clear() 69 | self.axes.grid(True) 70 | 71 | has_series = False 72 | 73 | for row in range(self.series_list_model.rowCount()): 74 | model_index = self.series_list_model.index(row, 0) 75 | checked = self.series_list_model.data(model_index, 76 | Qt.CheckStateRole) == QVariant(Qt.Checked) 77 | name = str(self.series_list_model.data(model_index)) 78 | 79 | if checked: 80 | has_series = True 81 | 82 | x_from = self.from_spin.value() 83 | x_to = self.to_spin.value() 84 | series = self.data.get_series_data(name)[x_from:x_to + 1] 85 | self.axes.plot(range(len(series)), series, 'o-', label=name) 86 | 87 | if has_series and self.legend_cb.isChecked(): 88 | self.axes.legend() 89 | self.canvas.draw() 90 | 91 | def on_about(self): 92 | msg = __doc__ 93 | QMessageBox.about(self, "About the demo", msg.strip()) 94 | 95 | def fill_series_list(self, names): 96 | self.series_list_model.clear() 97 | 98 | for name in names: 99 | item = QStandardItem(name) 100 | item.setCheckState(Qt.Unchecked) 101 | item.setCheckable(True) 102 | self.series_list_model.appendRow(item) 103 | 104 | def create_main_frame(self): 105 | self.main_frame = QWidget() 106 | 107 | plot_frame = QWidget() 108 | 109 | self.dpi = 100 110 | self.fig = Figure((6.0, 4.0), dpi=self.dpi) 111 | self.canvas = FigureCanvas(self.fig) 112 | self.canvas.setParent(self.main_frame) 113 | 114 | self.axes = self.fig.add_subplot(111) 115 | self.mpl_toolbar = NavigationToolbar(self.canvas, self.main_frame) 116 | 117 | log_label = QLabel("Data series:") 118 | self.series_list_view = QListView() 119 | self.series_list_view.setModel(self.series_list_model) 120 | 121 | spin_label1 = QLabel('X from') 122 | self.from_spin = QSpinBox() 123 | spin_label2 = QLabel('to') 124 | self.to_spin = QSpinBox() 125 | 126 | spins_hbox = QHBoxLayout() 127 | spins_hbox.addWidget(spin_label1) 128 | spins_hbox.addWidget(self.from_spin) 129 | spins_hbox.addWidget(spin_label2) 130 | spins_hbox.addWidget(self.to_spin) 131 | spins_hbox.addStretch(1) 132 | 133 | self.legend_cb = QCheckBox("Show L&egend") 134 | self.legend_cb.setChecked(False) 135 | 136 | self.show_button = QPushButton("&Show") 137 | # self.connect(self.show_button, SIGNAL('clicked()'), self.on_show) 138 | self.show_button.clicked.connect(self.on_show) 139 | 140 | left_vbox = QVBoxLayout() 141 | left_vbox.addWidget(self.canvas) 142 | left_vbox.addWidget(self.mpl_toolbar) 143 | 144 | right_vbox = QVBoxLayout() 145 | right_vbox.addWidget(log_label) 146 | right_vbox.addWidget(self.series_list_view) 147 | right_vbox.addLayout(spins_hbox) 148 | right_vbox.addWidget(self.legend_cb) 149 | right_vbox.addWidget(self.show_button) 150 | right_vbox.addStretch(1) 151 | 152 | hbox = QHBoxLayout() 153 | hbox.addLayout(left_vbox) 154 | hbox.addLayout(right_vbox) 155 | self.main_frame.setLayout(hbox) 156 | 157 | self.setCentralWidget(self.main_frame) 158 | 159 | def create_status_bar(self): 160 | self.status_text = QLabel("Please load a data file") 161 | self.statusBar().addWidget(self.status_text, 1) 162 | 163 | def create_menu(self): 164 | self.file_menu = self.menuBar().addMenu("&File") 165 | 166 | load_action = self.create_action("&Load file", 167 | shortcut="Ctrl+L", slot=self.load_file, tip="Load a file") 168 | quit_action = self.create_action("&Quit", slot=self.close, 169 | shortcut="Ctrl+Q", tip="Close the application") 170 | 171 | self.add_actions(self.file_menu, 172 | (load_action, None, quit_action)) 173 | 174 | self.help_menu = self.menuBar().addMenu("&Help") 175 | about_action = self.create_action("&About", 176 | shortcut='F1', slot=self.on_about, 177 | tip='About the demo') 178 | 179 | self.add_actions(self.help_menu, (about_action,)) 180 | 181 | def add_actions(self, target, actions): 182 | for action in actions: 183 | if action is None: 184 | target.addSeparator() 185 | else: 186 | target.addAction(action) 187 | 188 | def create_action( self, text, slot=None, shortcut=None, 189 | icon=None, tip=None, checkable=False, 190 | signal="triggered"): 191 | action = QAction(text, self) 192 | if icon is not None: 193 | action.setIcon(QIcon(":/%s.png" % icon)) 194 | if shortcut is not None: 195 | action.setShortcut(shortcut) 196 | if tip is not None: 197 | action.setToolTip(tip) 198 | action.setStatusTip(tip) 199 | if slot is not None: 200 | # self.connect(action, SIGNAL(signal), slot) 201 | getattr(action, signal).connect(slot) 202 | if checkable: 203 | action.setCheckable(True) 204 | return action 205 | 206 | 207 | class DataHolder(object): 208 | """ Just a thin wrapper over a dictionary that holds integer 209 | data series. Each series has a name and a list of numbers 210 | as its data. The length of all series is assumed to be 211 | the same. 212 | The series can be read from a CSV file, where each line 213 | is a separate series. In each series, the first item in 214 | the line is the name, and the rest are data numbers. 215 | """ 216 | def __init__(self, filename=None): 217 | self.load_from_file(filename) 218 | 219 | def load_from_file(self, filename=None): 220 | self.data = {} 221 | self.names = [] 222 | 223 | if filename: 224 | with open(filename, 'r') as f: 225 | for line in csv.reader(f): 226 | self.names.append(line[0]) 227 | self.data[line[0]] = [i for i in map(int, line[1:])] 228 | self.datalen = len(line[1:]) 229 | 230 | def series_names(self): 231 | """ Names of the data series 232 | """ 233 | return self.names 234 | 235 | def series_len(self): 236 | """ Length of a data series 237 | """ 238 | return self.datalen 239 | 240 | def series_count(self): 241 | return len(self.data) 242 | 243 | def get_series_data(self, name): 244 | return self.data[name] 245 | 246 | 247 | def main(): 248 | app = QApplication(sys.argv) 249 | form = Form() 250 | form.show() 251 | app.exec_() 252 | 253 | 254 | if __name__ == "__main__": 255 | main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: **This project is archived as no longer maintained** 2 | 3 | # matplotlib_qtquick_playground 4 | Port of the example kindly provided by Eli Bendersky to PyQt5: 5 | https://github.com/eliben/code-for-blog/tree/master/2009/pyqt_dataplot_demo 6 | 7 | Derivation of the example have been made based on the three following Qt technologies: 8 | - QtWidgets 9 | - QtQuick Controls 1.0 10 | - QtQuick Controls 2.0 (actually Qt.labs.controls 1.0 as I used PyQt 5.6) 11 | 12 | The goal of this work was to play around with QtQuick and PyQt5. The integration of matplotlib with QtWidgets is the best 13 | as a backend support full interactivity and navigation toolbar. A new matplotlib backend based on a QQuickItem has been 14 | created to restore maximal interactivity. 15 | 16 | The logic behind QtWidgets GUI and QtQuick is quite different. For example, in the former, the Python script takes care of 17 | reading all widgets before updating the figure. But in the latter, QtQuick controls are binded to Python properties that 18 | emit signal forcing the figure to update. 19 | 20 | ## QtWidgets version 21 | 22 | ![QtWidgets version](./QtWidgets/QtWidgets_UI.PNG) 23 | 24 | ## QtQuick Controls 1.0 version 25 | 26 | ![QtQuick Controls 1.0 version](./QtQuick_controls_v1/QtQuickControls1.PNG) 27 | 28 | ## QtQuick Controls 2.0 version 29 | 30 | ![QtQuick Controls 2.0 version](./QtQuick_controls_v2/QtQuickControls2.PNG) 31 | 32 | Code functions 33 | ============== 34 | 35 | Series of data are loaded from a .csv file, and their names are 36 | displayed in a checkable list view. The user can select the series 37 | it wants from the list and plot them on a matplotlib canvas. 38 | Use the sample .csv file that comes with the scripts for an example 39 | of data series. 40 | 41 | Requirements 42 | ============ 43 | 44 | * Python >= 3.5 45 | * PyQt = 5.6 (if you plan to use PyQt 5.7, references have changed as QtQuick.Controls 2.0 have integrated the official library) 46 | * matplolib >= 1.4 47 | 48 | License 49 | ======= 50 | 51 | MIT License 52 | 53 | Copyright (C) 2016 Frederic Collonval 54 | 55 | The code for QtQuick Controls 2.0 makes used of the KDE Breeze Icons Theme (https://github.com/KDE/breeze-icons) distributed under LGPLv3 56 | 57 | > The Breeze Icon Theme in icons/ 58 | 59 | > Copyright (C) 2014 Uri Herrera and others 60 | -------------------------------------------------------------------------------- /backend/backend_qtquick5/Figure.qml: -------------------------------------------------------------------------------- 1 | import Backend 1.0 2 | import QtQuick 2.6 3 | 4 | Item { 5 | width: 480 6 | height: 320 7 | 8 | FigureCanvas { 9 | id: mplView 10 | objectName : "figure" 11 | anchors.fill: parent 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/backend_qtquick5/FigureToolbar.qml: -------------------------------------------------------------------------------- 1 | import Backend 1.0 2 | import QtQuick 2.6 3 | import QtQuick.Layouts 1.2 4 | import Qt.labs.controls 1.0 5 | import QtQuick.Dialogs 1.2 6 | 7 | Item{ 8 | width: 640 9 | height: 480 10 | 11 | ColumnLayout { 12 | spacing : 0 13 | anchors.fill: parent 14 | 15 | FigureToolbar { 16 | id: mplView 17 | objectName : "figure" 18 | 19 | Layout.fillWidth: true 20 | Layout.fillHeight: true 21 | 22 | Layout.minimumWidth: 10 23 | Layout.minimumHeight: 10 24 | } 25 | 26 | MessageDialog { 27 | id: messageDialog 28 | } 29 | 30 | FileDialog { 31 | id: saveFileDialog 32 | title: "Choose a filename to save to" 33 | folder: mplView.defaultDirectory 34 | nameFilters: mplView.fileFilters 35 | selectedNameFilter: mplView.defaultFileFilter 36 | selectExisting: false 37 | 38 | onAccepted: { 39 | try{ 40 | mplView.print_figure(fileUrl) 41 | } 42 | catch (error){ 43 | messageDialog.title = "Error saving file" 44 | messageDialog.text = error 45 | messageDialog.icon = StandardIcon.Critical 46 | messageDialog.open() 47 | } 48 | } 49 | } 50 | 51 | SubplotTool { 52 | id: setMargin 53 | 54 | left.value: mplView.left 55 | right.value: mplView.right 56 | top.value: mplView.top 57 | bottom.value: mplView.bottom 58 | 59 | hspace.value: mplView.hspace 60 | wspace.value: mplView.wspace 61 | 62 | function initMargin() { 63 | // Init slider value 64 | setMargin.left.value = mplView.left 65 | setMargin.right.value = mplView.right 66 | setMargin.top.value = mplView.top 67 | setMargin.bottom.value = mplView.bottom 68 | 69 | setMargin.hspace.value = mplView.hspace 70 | setMargin.wspace.value = mplView.wspace 71 | 72 | // Invert parameter bindings 73 | mplView.left = Qt.binding(function() { return setMargin.left.value }) 74 | mplView.right = Qt.binding(function() { return setMargin.right.value }) 75 | mplView.top = Qt.binding(function() { return setMargin.top.value }) 76 | mplView.bottom = Qt.binding(function() { return setMargin.bottom.value }) 77 | 78 | mplView.hspace = Qt.binding(function() { return setMargin.hspace.value }) 79 | mplView.wspace = Qt.binding(function() { return setMargin.wspace.value }) 80 | } 81 | 82 | onReset: { 83 | mplView.reset_margin() 84 | setMargin.initMargin() 85 | } 86 | 87 | onTightLayout: { 88 | mplView.tight_layout() 89 | setMargin.initMargin() 90 | } 91 | } 92 | 93 | 94 | ToolBar { 95 | id: toolbar 96 | 97 | Layout.alignment: Qt.AlignLeft | Qt.Bottom 98 | Layout.fillWidth: true 99 | 100 | RowLayout { 101 | Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter 102 | anchors.fill: parent 103 | spacing: 0 104 | 105 | ToolButton { 106 | id : home 107 | contentItem: Image{ 108 | fillMode: Image.PreserveAspectFit 109 | source: "image://mplIcons/home" 110 | } 111 | onClicked: { 112 | mplView.home() 113 | } 114 | } 115 | 116 | ToolButton { 117 | id : back 118 | contentItem: Image{ 119 | fillMode: Image.PreserveAspectFit 120 | source: "image://mplIcons/back" 121 | } 122 | onClicked: { 123 | mplView.back() 124 | } 125 | } 126 | 127 | ToolButton { 128 | id : forward 129 | 130 | contentItem: Image{ 131 | fillMode: Image.PreserveAspectFit 132 | source: "image://mplIcons/forward" 133 | } 134 | onClicked: { 135 | mplView.forward() 136 | } 137 | } 138 | 139 | // Fake separator 140 | Label { 141 | text : "|" 142 | } 143 | 144 | ButtonGroup { 145 | // Gather pan and zoom tools to make them auto-exclusive 146 | id: pan_zoom 147 | } 148 | 149 | ToolButton { 150 | id : pan 151 | 152 | contentItem: Image{ 153 | fillMode: Image.PreserveAspectFit 154 | source: "image://mplIcons/move" 155 | } 156 | 157 | ButtonGroup.group: pan_zoom 158 | checkable: true 159 | 160 | onClicked: { 161 | mplView.pan() 162 | } 163 | } 164 | 165 | ToolButton { 166 | id : zoom 167 | 168 | contentItem: Image{ 169 | fillMode: Image.PreserveAspectFit 170 | source: "image://mplIcons/zoom_to_rect" 171 | } 172 | 173 | ButtonGroup.group: pan_zoom 174 | checkable: true 175 | 176 | onClicked: { 177 | mplView.zoom() 178 | } 179 | } 180 | 181 | Label { 182 | text : "|" 183 | } 184 | 185 | ToolButton { 186 | id : subplots 187 | contentItem: Image{ 188 | fillMode: Image.PreserveAspectFit 189 | source: "image://mplIcons/subplots" 190 | } 191 | onClicked: { 192 | setMargin.initMargin() 193 | setMargin.open() 194 | } 195 | } 196 | 197 | ToolButton { 198 | id : save 199 | contentItem: Image{ 200 | fillMode: Image.PreserveAspectFit 201 | source: "image://mplIcons/filesave" 202 | } 203 | onClicked: { 204 | saveFileDialog.open() 205 | } 206 | } 207 | 208 | Item { 209 | Layout.fillWidth: true 210 | } 211 | 212 | Label{ 213 | id: locLabel 214 | 215 | Layout.alignment: Qt.AlignRight | Qt.AlignVCenter 216 | 217 | text: mplView.message 218 | } 219 | } 220 | } 221 | } 222 | } -------------------------------------------------------------------------------- /backend/backend_qtquick5/SubplotTool.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Layouts 1.2 3 | import Qt.labs.controls 1.0 4 | import QtQuick.Dialogs 1.2 5 | 6 | Dialog { 7 | id: subplotTool 8 | 9 | title: "Margins & spacing" 10 | 11 | property alias left: left_slider 12 | property alias right: right_slider 13 | property alias top: top_slider 14 | property alias bottom: bottom_slider 15 | property alias hspace: hspace_slider 16 | property alias wspace: wspace_slider 17 | 18 | signal reset 19 | signal tightLayout 20 | 21 | contentItem : ColumnLayout { 22 | anchors.fill: parent 23 | 24 | GroupBox { 25 | id: borders 26 | title: "Borders" 27 | 28 | Layout.alignment: Qt.AlignTop | Qt.AlignHCenter 29 | Layout.fillWidth: true 30 | Layout.fillHeight: true 31 | 32 | GridLayout{ 33 | columns: 3 34 | anchors.fill: parent 35 | 36 | Label { 37 | text: "top" 38 | } 39 | Slider{ 40 | id: top_slider 41 | from: bottom_slider.value 42 | to: 1 43 | value: 1 44 | stepSize: 0.01 45 | snapMode: Slider.SnapOnRelease 46 | 47 | Layout.fillWidth: true 48 | } 49 | Label{ 50 | text: top_slider.value.toFixed(2) 51 | } 52 | 53 | Label { 54 | text: "bottom" 55 | } 56 | Slider{ 57 | id: bottom_slider 58 | from: 0 59 | to: top_slider.value 60 | value: 0 61 | stepSize: 0.01 62 | snapMode: Slider.SnapOnRelease 63 | 64 | Layout.fillWidth: true 65 | } 66 | Label{ 67 | text: bottom_slider.value.toFixed(2) 68 | } 69 | 70 | Label { 71 | text: "left" 72 | } 73 | Slider{ 74 | id: left_slider 75 | from: 0 76 | to: right_slider.value 77 | value: 0 78 | stepSize: 0.01 79 | snapMode: Slider.SnapOnRelease 80 | 81 | Layout.fillWidth: true 82 | } 83 | Label{ 84 | text: left_slider.value.toFixed(2) 85 | } 86 | 87 | Label { 88 | text: "right" 89 | } 90 | Slider{ 91 | id: right_slider 92 | from: left_slider.value 93 | to: 1 94 | value: 1 95 | stepSize: 0.01 96 | snapMode: Slider.SnapOnRelease 97 | 98 | Layout.fillWidth: true 99 | } 100 | Label{ 101 | text: right_slider.value.toFixed(2) 102 | } 103 | 104 | } 105 | } 106 | 107 | GroupBox { 108 | id: spacings 109 | title: "Spacings" 110 | 111 | Layout.alignment: Qt.AlignTop | Qt.AlignHCenter 112 | Layout.fillWidth: true 113 | Layout.fillHeight: true 114 | 115 | GridLayout{ 116 | columns: 3 117 | anchors.fill: parent 118 | 119 | Label { 120 | text: "hspace" 121 | } 122 | Slider{ 123 | id: hspace_slider 124 | from: 0 125 | to: 1 126 | value: 0 127 | stepSize: 0.01 128 | snapMode: Slider.SnapOnRelease 129 | 130 | Layout.fillWidth: true 131 | } 132 | Label{ 133 | text: hspace_slider.value.toFixed(2) 134 | } 135 | 136 | Label { 137 | text: "wspace" 138 | } 139 | Slider{ 140 | id: wspace_slider 141 | from: 0 142 | to: 1 143 | value: 0 144 | stepSize: 0.01 145 | snapMode: Slider.SnapOnRelease 146 | 147 | Layout.fillWidth: true 148 | } 149 | Label{ 150 | text: wspace_slider.value.toFixed(2) 151 | } 152 | } 153 | } 154 | 155 | RowLayout { 156 | id: buttons 157 | 158 | anchors.bottom: parent.bottom 159 | Layout.fillWidth: true 160 | 161 | Button { 162 | id: tight_layout 163 | text: "Tight Layout" 164 | 165 | Layout.alignment: Qt.AlignLeft | Qt.AlignBottom 166 | 167 | onClicked: { 168 | subplotTool.tightLayout() 169 | } 170 | } 171 | 172 | Item { 173 | Layout.fillWidth: true 174 | } 175 | 176 | Button { 177 | id: reset 178 | text: "Reset" 179 | 180 | Layout.alignment: Qt.AlignRight | Qt.AlignBottom 181 | 182 | onClicked: { 183 | subplotTool.reset() 184 | } 185 | } 186 | 187 | Button { 188 | id: close 189 | text: "Close" 190 | 191 | Layout.alignment: Qt.AlignRight | Qt.AlignBottom 192 | 193 | onClicked: { 194 | subplotTool.close() 195 | } 196 | } 197 | } 198 | 199 | } 200 | } -------------------------------------------------------------------------------- /backend/backend_qtquick5/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .backend_qquick5agg import FigureCanvasQTAgg, FigureCanvasQTAggToolbar, MatplotlibIconProvider 3 | -------------------------------------------------------------------------------- /backend/backend_qtquick5/backend_qquick5agg.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import os 3 | import sys 4 | import traceback 5 | 6 | import matplotlib 7 | from matplotlib.backends.backend_agg import FigureCanvasAgg 8 | from matplotlib.backend_bases import cursors 9 | from matplotlib.figure import Figure 10 | from matplotlib.backends.backend_qt5 import TimerQT 11 | 12 | from matplotlib.externals import six 13 | 14 | from PyQt5 import QtCore, QtGui, QtQuick, QtWidgets 15 | 16 | DEBUG = False 17 | 18 | class MatplotlibIconProvider(QtQuick.QQuickImageProvider): 19 | """ This class provide the matplotlib icons for the navigation toolbar. 20 | """ 21 | 22 | def __init__(self, img_type = QtQuick.QQuickImageProvider.Pixmap): 23 | self.basedir = os.path.join(matplotlib.rcParams['datapath'], 'images') 24 | QtQuick.QQuickImageProvider.__init__(self, img_type) 25 | 26 | def requestImage(self, id, size): 27 | img = QtGui.QImage(os.path.join(self.basedir, id + '.png')) 28 | size = img.size() 29 | return img, size 30 | 31 | def requestPixmap(self, id, size): 32 | img, size = self.requestImage(id, size) 33 | pixmap = QtGui.QPixmap.fromImage(img) 34 | 35 | return pixmap, size 36 | 37 | class FigureCanvasQtQuickAgg(QtQuick.QQuickPaintedItem, FigureCanvasAgg): 38 | """ This class creates a QtQuick Item encapsulating a Matplotlib 39 | Figure and all the functions to interact with the 'standard' 40 | Matplotlib navigation toolbar. 41 | """ 42 | 43 | # map Qt button codes to MouseEvent's ones: 44 | buttond = { 45 | QtCore.Qt.LeftButton: 1, 46 | QtCore.Qt.MidButton: 2, 47 | QtCore.Qt.RightButton: 3, 48 | # QtCore.Qt.XButton1: None, 49 | # QtCore.Qt.XButton2: None, 50 | } 51 | 52 | cursord = { 53 | cursors.MOVE: QtCore.Qt.SizeAllCursor, 54 | cursors.HAND: QtCore.Qt.PointingHandCursor, 55 | cursors.POINTER: QtCore.Qt.ArrowCursor, 56 | cursors.SELECT_REGION: QtCore.Qt.CrossCursor, 57 | } 58 | 59 | messageChanged = QtCore.pyqtSignal(str) 60 | 61 | leftChanged = QtCore.pyqtSignal() 62 | rightChanged = QtCore.pyqtSignal() 63 | topChanged = QtCore.pyqtSignal() 64 | bottomChanged = QtCore.pyqtSignal() 65 | wspaceChanged = QtCore.pyqtSignal() 66 | hspaceChanged = QtCore.pyqtSignal() 67 | 68 | def __init__(self, figure, parent=None, coordinates=True): 69 | if DEBUG: 70 | print('FigureCanvasQtQuickAgg qtquick5: ', figure) 71 | # _create_qApp() 72 | if figure is None: 73 | figure = Figure((6.0, 4.0)) 74 | 75 | QtQuick.QQuickPaintedItem.__init__(self, parent=parent) 76 | FigureCanvasAgg.__init__(self, figure=figure) 77 | 78 | self._drawRect = None 79 | self.blitbox = None 80 | 81 | # Activate hover events and mouse press events 82 | self.setAcceptHoverEvents(True) 83 | self.setAcceptedMouseButtons(QtCore.Qt.AllButtons) 84 | 85 | self._agg_draw_pending = False 86 | 87 | def getFigure(self): 88 | return self.figure 89 | 90 | def drawRectangle(self, rect): 91 | self._drawRect = rect 92 | self.update() 93 | 94 | def paint(self, p): 95 | """ 96 | Copy the image from the Agg canvas to the qt.drawable. 97 | In Qt, all drawing should be done inside of here when a widget is 98 | shown onscreen. 99 | """ 100 | # if the canvas does not have a renderer, then give up and wait for 101 | # FigureCanvasAgg.draw(self) to be called 102 | if not hasattr(self, 'renderer'): 103 | return 104 | 105 | if DEBUG: 106 | print('FigureCanvasQtQuickAgg.paint: ', self, 107 | self.get_width_height()) 108 | 109 | if self.blitbox is None: 110 | # matplotlib is in rgba byte order. QImage wants to put the bytes 111 | # into argb format and is in a 4 byte unsigned int. Little endian 112 | # system is LSB first and expects the bytes in reverse order 113 | # (bgra). 114 | if QtCore.QSysInfo.ByteOrder == QtCore.QSysInfo.LittleEndian: 115 | stringBuffer = self.renderer._renderer.tostring_bgra() 116 | else: 117 | stringBuffer = self.renderer._renderer.tostring_argb() 118 | 119 | refcnt = sys.getrefcount(stringBuffer) 120 | 121 | # convert the Agg rendered image -> qImage 122 | qImage = QtGui.QImage(stringBuffer, self.renderer.width, 123 | self.renderer.height, 124 | QtGui.QImage.Format_ARGB32) 125 | # get the rectangle for the image 126 | rect = qImage.rect() 127 | # p = QtGui.QPainter(self) 128 | # reset the image area of the canvas to be the back-ground color 129 | p.eraseRect(rect) 130 | # draw the rendered image on to the canvas 131 | p.drawPixmap(QtCore.QPoint(0, 0), QtGui.QPixmap.fromImage(qImage)) 132 | 133 | # draw the zoom rectangle to the QPainter 134 | if self._drawRect is not None: 135 | p.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.DotLine)) 136 | x, y, w, h = self._drawRect 137 | p.drawRect(x, y, w, h) 138 | 139 | else: 140 | bbox = self.blitbox 141 | l, b, r, t = bbox.extents 142 | w = int(r) - int(l) 143 | h = int(t) - int(b) 144 | t = int(b) + h 145 | reg = self.copy_from_bbox(bbox) 146 | stringBuffer = reg.to_string_argb() 147 | qImage = QtGui.QImage(stringBuffer, w, h, 148 | QtGui.QImage.Format_ARGB32) 149 | 150 | pixmap = QtGui.QPixmap.fromImage(qImage) 151 | p.drawPixmap(QtCore.QPoint(l, self.renderer.height-t), pixmap) 152 | 153 | # draw the zoom rectangle to the QPainter 154 | if self._drawRect is not None: 155 | p.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.DotLine)) 156 | x, y, w, h = self._drawRect 157 | p.drawRect(x, y, w, h) 158 | 159 | self.blitbox = None 160 | 161 | def draw(self): 162 | """ 163 | Draw the figure with Agg, and queue a request for a Qt draw. 164 | """ 165 | # The Agg draw is done here; delaying causes problems with code that 166 | # uses the result of the draw() to update plot elements. 167 | FigureCanvasAgg.draw(self) 168 | self.update() 169 | 170 | def draw_idle(self): 171 | """ 172 | Queue redraw of the Agg buffer and request Qt paintEvent. 173 | """ 174 | # The Agg draw needs to be handled by the same thread matplotlib 175 | # modifies the scene graph from. Post Agg draw request to the 176 | # current event loop in order to ensure thread affinity and to 177 | # accumulate multiple draw requests from event handling. 178 | # TODO: queued signal connection might be safer than singleShot 179 | if not self._agg_draw_pending: 180 | self._agg_draw_pending = True 181 | QtCore.QTimer.singleShot(0, self.__draw_idle_agg) 182 | 183 | def __draw_idle_agg(self, *args): 184 | if self.height() < 0 or self.width() < 0: 185 | self._agg_draw_pending = False 186 | return 187 | try: 188 | FigureCanvasAgg.draw(self) 189 | self.update() 190 | except Exception: 191 | # Uncaught exceptions are fatal for PyQt5, so catch them instead. 192 | traceback.print_exc() 193 | finally: 194 | self._agg_draw_pending = False 195 | 196 | def blit(self, bbox=None): 197 | """ 198 | Blit the region in bbox 199 | """ 200 | # If bbox is None, blit the entire canvas. Otherwise 201 | # blit only the area defined by the bbox. 202 | if bbox is None and self.figure: 203 | bbox = self.figure.bbox 204 | 205 | self.blitbox = bbox 206 | l, b, w, h = bbox.bounds 207 | t = b + h 208 | self.repaint(l, self.renderer.height-t, w, h) 209 | 210 | def geometryChanged(self, new_geometry, old_geometry): 211 | w = new_geometry.width() 212 | h = new_geometry.height() 213 | 214 | if (w <= 0.0) and (h <= 0.0): 215 | return 216 | 217 | if DEBUG: 218 | print('resize (%d x %d)' % (w, h)) 219 | print("FigureCanvasQtQuickAgg.geometryChanged(%d, %d)" % (w, h)) 220 | dpival = self.figure.dpi 221 | winch = w / dpival 222 | hinch = h / dpival 223 | self.figure.set_size_inches(winch, hinch) 224 | FigureCanvasAgg.resize_event(self) 225 | self.draw_idle() 226 | QtQuick.QQuickPaintedItem.geometryChanged(self, new_geometry, old_geometry) 227 | 228 | def hoverEnterEvent(self, event): 229 | FigureCanvasAgg.enter_notify_event(self, guiEvent=event) 230 | 231 | def hoverLeaveEvent(self, event): 232 | QtWidgets.QApplication.restoreOverrideCursor() 233 | FigureCanvasAgg.leave_notify_event(self, guiEvent=event) 234 | 235 | def hoverMoveEvent(self, event): 236 | x = event.pos().x() 237 | # flipy so y=0 is bottom of canvas 238 | y = self.figure.bbox.height - event.pos().y() 239 | FigureCanvasAgg.motion_notify_event(self, x, y, guiEvent=event) 240 | 241 | # if DEBUG: 242 | # print('hover move') 243 | 244 | # hoverMoveEvent kicks in when no mouse buttons are pressed 245 | # otherwise mouseMoveEvent are emitted 246 | def mouseMoveEvent(self, event): 247 | x = event.x() 248 | # flipy so y=0 is bottom of canvas 249 | y = self.figure.bbox.height - event.y() 250 | FigureCanvasAgg.motion_notify_event(self, x, y, guiEvent=event) 251 | # if DEBUG: 252 | # print('mouse move') 253 | 254 | def mousePressEvent(self, event): 255 | x = event.pos().x() 256 | # flipy so y=0 is bottom of canvas 257 | y = self.figure.bbox.height - event.pos().y() 258 | button = self.buttond.get(event.button()) 259 | if button is not None: 260 | FigureCanvasAgg.button_press_event(self, x, y, button, 261 | guiEvent=event) 262 | if DEBUG: 263 | print('button pressed:', event.button()) 264 | 265 | def mouseReleaseEvent(self, event): 266 | x = event.x() 267 | # flipy so y=0 is bottom of canvas 268 | y = self.figure.bbox.height - event.y() 269 | button = self.buttond.get(event.button()) 270 | if button is not None: 271 | FigureCanvasAgg.button_release_event(self, x, y, button, 272 | guiEvent=event) 273 | if DEBUG: 274 | print('button released') 275 | 276 | def mouseDoubleClickEvent(self, event): 277 | x = event.pos().x() 278 | # flipy so y=0 is bottom of canvas 279 | y = self.figure.bbox.height - event.pos().y() 280 | button = self.buttond.get(event.button()) 281 | if button is not None: 282 | FigureCanvasAgg.button_press_event(self, x, y, 283 | button, dblclick=True, 284 | guiEvent=event) 285 | if DEBUG: 286 | print('button doubleclicked:', event.button()) 287 | 288 | def wheelEvent(self, event): 289 | x = event.x() 290 | # flipy so y=0 is bottom of canvas 291 | y = self.figure.bbox.height - event.y() 292 | # from QWheelEvent::delta doc 293 | if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0: 294 | steps = event.angleDelta().y() / 120 295 | else: 296 | steps = event.pixelDelta().y() 297 | 298 | if steps != 0: 299 | FigureCanvasAgg.scroll_event(self, x, y, steps, guiEvent=event) 300 | if DEBUG: 301 | print('scroll event: ' 302 | 'steps = %i ' % (steps)) 303 | 304 | def keyPressEvent(self, event): 305 | key = self._get_key(event) 306 | if key is None: 307 | return 308 | FigureCanvasAgg.key_press_event(self, key, guiEvent=event) 309 | if DEBUG: 310 | print('key press', key) 311 | 312 | def keyReleaseEvent(self, event): 313 | key = self._get_key(event) 314 | if key is None: 315 | return 316 | FigureCanvasAgg.key_release_event(self, key, guiEvent=event) 317 | if DEBUG: 318 | print('key release', key) 319 | 320 | def _get_key(self, event): 321 | if event.isAutoRepeat(): 322 | return None 323 | 324 | event_key = event.key() 325 | event_mods = int(event.modifiers()) # actually a bitmask 326 | 327 | # get names of the pressed modifier keys 328 | # bit twiddling to pick out modifier keys from event_mods bitmask, 329 | # if event_key is a MODIFIER, it should not be duplicated in mods 330 | mods = [name for name, mod_key, qt_key in MODIFIER_KEYS 331 | if event_key != qt_key and (event_mods & mod_key) == mod_key] 332 | try: 333 | # for certain keys (enter, left, backspace, etc) use a word for the 334 | # key, rather than unicode 335 | key = SPECIAL_KEYS[event_key] 336 | except KeyError: 337 | # unicode defines code points up to 0x0010ffff 338 | # QT will use Key_Codes larger than that for keyboard keys that are 339 | # are not unicode characters (like multimedia keys) 340 | # skip these 341 | # if you really want them, you should add them to SPECIAL_KEYS 342 | MAX_UNICODE = 0x10ffff 343 | if event_key > MAX_UNICODE: 344 | return None 345 | 346 | key = six.unichr(event_key) 347 | # qt delivers capitalized letters. fix capitalization 348 | # note that capslock is ignored 349 | if 'shift' in mods: 350 | mods.remove('shift') 351 | else: 352 | key = key.lower() 353 | 354 | mods.reverse() 355 | return '+'.join(mods + [key]) 356 | 357 | def new_timer(self, *args, **kwargs): 358 | """ 359 | Creates a new backend-specific subclass of 360 | :class:`backend_bases.Timer`. This is useful for getting 361 | periodic events through the backend's native event 362 | loop. Implemented only for backends with GUIs. 363 | 364 | optional arguments: 365 | 366 | *interval* 367 | Timer interval in milliseconds 368 | 369 | *callbacks* 370 | Sequence of (func, args, kwargs) where func(*args, **kwargs) 371 | will be executed by the timer every *interval*. 372 | """ 373 | return TimerQT(*args, **kwargs) 374 | 375 | def flush_events(self): 376 | global qApp 377 | qApp.processEvents() 378 | 379 | def start_event_loop(self, timeout): 380 | FigureCanvasAgg.start_event_loop_default(self, timeout) 381 | 382 | start_event_loop.__doc__ = \ 383 | FigureCanvasAgg.start_event_loop_default.__doc__ 384 | 385 | def stop_event_loop(self): 386 | FigureCanvasAgg.stop_event_loop_default(self) 387 | 388 | stop_event_loop.__doc__ = FigureCanvasAgg.stop_event_loop_default.__doc__ 389 | 390 | 391 | class FigureQtQuickAggToolbar(FigureCanvasQtQuickAgg): 392 | """ This class creates a QtQuick Item encapsulating a Matplotlib 393 | Figure and all the functions to interact with the 'standard' 394 | Matplotlib navigation toolbar. 395 | """ 396 | 397 | cursord = { 398 | cursors.MOVE: QtCore.Qt.SizeAllCursor, 399 | cursors.HAND: QtCore.Qt.PointingHandCursor, 400 | cursors.POINTER: QtCore.Qt.ArrowCursor, 401 | cursors.SELECT_REGION: QtCore.Qt.CrossCursor, 402 | } 403 | 404 | messageChanged = QtCore.pyqtSignal(str) 405 | 406 | leftChanged = QtCore.pyqtSignal() 407 | rightChanged = QtCore.pyqtSignal() 408 | topChanged = QtCore.pyqtSignal() 409 | bottomChanged = QtCore.pyqtSignal() 410 | wspaceChanged = QtCore.pyqtSignal() 411 | hspaceChanged = QtCore.pyqtSignal() 412 | 413 | def __init__(self, figure, parent=None, coordinates=True): 414 | if DEBUG: 415 | print('FigureQtQuickAggToolbar qtquick5: ', figure) 416 | 417 | FigureCanvasQtQuickAgg.__init__(self, figure=figure, parent=parent) 418 | 419 | self._message = "" 420 | # 421 | # Attributes from NavigationToolbar2QT 422 | # 423 | self.coordinates = coordinates 424 | self._actions = {} 425 | 426 | # reference holder for subplots_adjust window 427 | self.adj_window = None 428 | # 429 | # Attributes from NavigationToolbar2 430 | # 431 | self.canvas = self.figure.canvas 432 | self.toolbar = self 433 | # a dict from axes index to a list of view limits 434 | self._views = matplotlib.cbook.Stack() 435 | self._positions = matplotlib.cbook.Stack() # stack of subplot positions 436 | self._xypress = None # the location and axis info at the time 437 | # of the press 438 | self._idPress = None 439 | self._idRelease = None 440 | self._active = None 441 | self._lastCursor = None 442 | 443 | self._idDrag = self.canvas.mpl_connect( 444 | 'motion_notify_event', self.mouse_move) 445 | 446 | self._ids_zoom = [] 447 | self._zoom_mode = None 448 | 449 | self._button_pressed = None # determined by the button pressed 450 | # at start 451 | 452 | self.mode = '' # a mode string for the status bar 453 | self.set_history_buttons() 454 | 455 | # 456 | # Store margin 457 | # 458 | self._defaults = {} 459 | for attr in ('left', 'bottom', 'right', 'top', 'wspace', 'hspace', ): 460 | val = getattr(self.figure.subplotpars, attr) 461 | self._defaults[attr] = val 462 | setattr(self, attr, val) 463 | 464 | @QtCore.pyqtProperty('QString', notify=messageChanged) 465 | def message(self): 466 | return self._message 467 | 468 | @message.setter 469 | def message(self, msg): 470 | if msg != self._message: 471 | self._message = msg 472 | self.messageChanged.emit(msg) 473 | 474 | @QtCore.pyqtProperty('QString', constant=True) 475 | def defaultDirectory(self): 476 | startpath = matplotlib.rcParams.get('savefig.directory', '') 477 | return os.path.expanduser(startpath) 478 | 479 | @QtCore.pyqtProperty('QStringList', constant=True) 480 | def fileFilters(self): 481 | filetypes = self.canvas.get_supported_filetypes_grouped() 482 | sorted_filetypes = list(six.iteritems(filetypes)) 483 | sorted_filetypes.sort() 484 | 485 | filters = [] 486 | for name, exts in sorted_filetypes: 487 | exts_list = " ".join(['*.%s' % ext for ext in exts]) 488 | filter = '%s (%s)' % (name, exts_list) 489 | filters.append(filter) 490 | 491 | return filters 492 | 493 | @QtCore.pyqtProperty('QString', constant=True) 494 | def defaultFileFilter(self): 495 | default_filetype = self.canvas.get_default_filetype() 496 | 497 | selectedFilter = None 498 | for filter in self.fileFilters: 499 | exts = filter.split('(', maxsplit=1)[1] 500 | exts = exts[:-1].split() 501 | if default_filetype in exts: 502 | selectedFilter = filter 503 | break 504 | 505 | if selectedFilter is None: 506 | selectedFilter = self.fileFilters[0] 507 | 508 | return selectedFilter 509 | 510 | @QtCore.pyqtProperty(float, notify=leftChanged) 511 | def left(self): 512 | return self.figure.subplotpars.left 513 | 514 | @left.setter 515 | def left(self, value): 516 | if value != self.figure.subplotpars.left: 517 | self.figure.subplots_adjust(left=value) 518 | self.leftChanged.emit() 519 | 520 | self.figure.canvas.draw_idle() 521 | 522 | @QtCore.pyqtProperty(float, notify=rightChanged) 523 | def right(self): 524 | return self.figure.subplotpars.right 525 | 526 | @right.setter 527 | def right(self, value): 528 | if value != self.figure.subplotpars.right: 529 | self.figure.subplots_adjust(right=value) 530 | self.rightChanged.emit() 531 | 532 | self.figure.canvas.draw_idle() 533 | 534 | @QtCore.pyqtProperty(float, notify=topChanged) 535 | def top(self): 536 | return self.figure.subplotpars.top 537 | 538 | @top.setter 539 | def top(self, value): 540 | if value != self.figure.subplotpars.top: 541 | self.figure.subplots_adjust(top=value) 542 | self.topChanged.emit() 543 | 544 | self.figure.canvas.draw_idle() 545 | 546 | @QtCore.pyqtProperty(float, notify=bottomChanged) 547 | def bottom(self): 548 | return self.figure.subplotpars.bottom 549 | 550 | @bottom.setter 551 | def bottom(self, value): 552 | if value != self.figure.subplotpars.bottom: 553 | self.figure.subplots_adjust(bottom=value) 554 | self.bottomChanged.emit() 555 | 556 | self.figure.canvas.draw_idle() 557 | 558 | @QtCore.pyqtProperty(float, notify=hspaceChanged) 559 | def hspace(self): 560 | return self.figure.subplotpars.hspace 561 | 562 | @hspace.setter 563 | def hspace(self, value): 564 | if value != self.figure.subplotpars.hspace: 565 | self.figure.subplots_adjust(hspace=value) 566 | self.hspaceChanged.emit() 567 | 568 | self.figure.canvas.draw_idle() 569 | 570 | @QtCore.pyqtProperty(float, notify=wspaceChanged) 571 | def wspace(self): 572 | return self.figure.subplotpars.wspace 573 | 574 | @wspace.setter 575 | def wspace(self, value): 576 | if value != self.figure.subplotpars.wspace: 577 | self.figure.subplots_adjust(wspace=value) 578 | self.wspaceChanged.emit() 579 | 580 | self.figure.canvas.draw_idle() 581 | 582 | def mouse_move(self, event): 583 | self._set_cursor(event) 584 | 585 | if event.inaxes and event.inaxes.get_navigate(): 586 | 587 | try: 588 | s = event.inaxes.format_coord(event.xdata, event.ydata) 589 | except (ValueError, OverflowError): 590 | pass 591 | else: 592 | artists = [a for a in event.inaxes.mouseover_set 593 | if a.contains(event)] 594 | 595 | if artists: 596 | 597 | a = max(enumerate(artists), key=lambda x: x[1].zorder)[1] 598 | if a is not event.inaxes.patch: 599 | data = a.get_cursor_data(event) 600 | if data is not None: 601 | s += ' [{:s}]'.format(a.format_cursor_data(data)) 602 | 603 | if len(self.mode): 604 | self.message = '{:s}, {:s}'.format(self.mode, s) 605 | else: 606 | self.message = s 607 | else: 608 | self.message = self.mode 609 | 610 | def dynamic_update(self): 611 | self.canvas.draw_idle() 612 | 613 | def push_current(self): 614 | """push the current view limits and position onto the stack""" 615 | views = [] 616 | pos = [] 617 | for a in self.canvas.figure.get_axes(): 618 | views.append(a._get_view()) 619 | # Store both the original and modified positions 620 | pos.append(( 621 | a.get_position(True).frozen(), 622 | a.get_position().frozen())) 623 | self._views.push(views) 624 | self._positions.push(pos) 625 | self.set_history_buttons() 626 | 627 | def set_history_buttons(self): 628 | """Enable or disable back/forward button""" 629 | pass 630 | 631 | def _update_view(self): 632 | """Update the viewlim and position from the view and 633 | position stack for each axes 634 | """ 635 | 636 | views = self._views() 637 | if views is None: 638 | return 639 | pos = self._positions() 640 | if pos is None: 641 | return 642 | for i, a in enumerate(self.canvas.figure.get_axes()): 643 | a._set_view(views[i]) 644 | # Restore both the original and modified positions 645 | a.set_position(pos[i][0], 'original') 646 | a.set_position(pos[i][1], 'active') 647 | 648 | self.canvas.draw_idle() 649 | 650 | @QtCore.pyqtSlot() 651 | def home(self, *args): 652 | """Restore the original view""" 653 | self._views.home() 654 | self._positions.home() 655 | self.set_history_buttons() 656 | self._update_view() 657 | 658 | @QtCore.pyqtSlot() 659 | def forward(self, *args): 660 | """Move forward in the view lim stack""" 661 | self._views.forward() 662 | self._positions.forward() 663 | self.set_history_buttons() 664 | self._update_view() 665 | 666 | @QtCore.pyqtSlot() 667 | def back(self, *args): 668 | """move back up the view lim stack""" 669 | self._views.back() 670 | self._positions.back() 671 | self.set_history_buttons() 672 | self._update_view() 673 | 674 | def _set_cursor(self, event): 675 | if not event.inaxes or not self._active: 676 | if self._lastCursor != cursors.POINTER: 677 | self.set_cursor(cursors.POINTER) 678 | self._lastCursor = cursors.POINTER 679 | else: 680 | if self._active == 'ZOOM': 681 | if self._lastCursor != cursors.SELECT_REGION: 682 | self.set_cursor(cursors.SELECT_REGION) 683 | self._lastCursor = cursors.SELECT_REGION 684 | elif (self._active == 'PAN' and 685 | self._lastCursor != cursors.MOVE): 686 | self.set_cursor(cursors.MOVE) 687 | 688 | self._lastCursor = cursors.MOVE 689 | 690 | def set_cursor(self, cursor): 691 | """ 692 | Set the current cursor to one of the :class:`Cursors` 693 | enums values 694 | """ 695 | if DEBUG: 696 | print('Set cursor', cursor) 697 | self.canvas.setCursor(self.cursord[cursor]) 698 | 699 | def draw_with_locators_update(self): 700 | """Redraw the canvases, update the locators""" 701 | for a in self.canvas.figure.get_axes(): 702 | xaxis = getattr(a, 'xaxis', None) 703 | yaxis = getattr(a, 'yaxis', None) 704 | locators = [] 705 | if xaxis is not None: 706 | locators.append(xaxis.get_major_locator()) 707 | locators.append(xaxis.get_minor_locator()) 708 | if yaxis is not None: 709 | locators.append(yaxis.get_major_locator()) 710 | locators.append(yaxis.get_minor_locator()) 711 | 712 | for loc in locators: 713 | loc.refresh() 714 | self.canvas.draw_idle() 715 | 716 | def press(self, event): 717 | """Called whenever a mouse button is pressed.""" 718 | pass 719 | 720 | def press_pan(self, event): 721 | """the press mouse button in pan/zoom mode callback""" 722 | 723 | if event.button == 1: 724 | self._button_pressed = 1 725 | elif event.button == 3: 726 | self._button_pressed = 3 727 | else: 728 | self._button_pressed = None 729 | return 730 | 731 | x, y = event.x, event.y 732 | 733 | # push the current view to define home if stack is empty 734 | if self._views.empty(): 735 | self.push_current() 736 | 737 | self._xypress = [] 738 | for i, a in enumerate(self.canvas.figure.get_axes()): 739 | if (x is not None and y is not None and a.in_axes(event) and 740 | a.get_navigate() and a.can_pan()): 741 | a.start_pan(x, y, event.button) 742 | self._xypress.append((a, i)) 743 | self.canvas.mpl_disconnect(self._idDrag) 744 | self._idDrag = self.canvas.mpl_connect('motion_notify_event', 745 | self.drag_pan) 746 | 747 | self.press(event) 748 | 749 | def release(self, event): 750 | """this will be called whenever mouse button is released""" 751 | pass 752 | 753 | def release_pan(self, event): 754 | """the release mouse button callback in pan/zoom mode""" 755 | 756 | if self._button_pressed is None: 757 | return 758 | self.canvas.mpl_disconnect(self._idDrag) 759 | self._idDrag = self.canvas.mpl_connect( 760 | 'motion_notify_event', self.mouse_move) 761 | for a, ind in self._xypress: 762 | a.end_pan() 763 | if not self._xypress: 764 | return 765 | self._xypress = [] 766 | self._button_pressed = None 767 | self.push_current() 768 | self.release(event) 769 | self.draw_with_locators_update() 770 | 771 | def drag_pan(self, event): 772 | """the drag callback in pan/zoom mode""" 773 | 774 | for a, ind in self._xypress: 775 | #safer to use the recorded button at the press than current button: 776 | #multiple button can get pressed during motion... 777 | a.drag_pan(self._button_pressed, event.key, event.x, event.y) 778 | self.dynamic_update() 779 | 780 | @QtCore.pyqtSlot() 781 | def pan(self, *args): 782 | """Activate the pan/zoom tool. pan with left button, zoom with right""" 783 | # set the pointer icon and button press funcs to the 784 | # appropriate callbacks 785 | 786 | if self._active == 'PAN': 787 | self._active = None 788 | else: 789 | self._active = 'PAN' 790 | if self._idPress is not None: 791 | self._idPress = self.canvas.mpl_disconnect(self._idPress) 792 | self.mode = '' 793 | 794 | if self._idRelease is not None: 795 | self._idRelease = self.canvas.mpl_disconnect(self._idRelease) 796 | self.mode = '' 797 | 798 | if self._active: 799 | self._idPress = self.canvas.mpl_connect( 800 | 'button_press_event', self.press_pan) 801 | self._idRelease = self.canvas.mpl_connect( 802 | 'button_release_event', self.release_pan) 803 | self.mode = 'pan/zoom' 804 | self.canvas.widgetlock(self) 805 | else: 806 | self.canvas.widgetlock.release(self) 807 | 808 | for a in self.canvas.figure.get_axes(): 809 | a.set_navigate_mode(self._active) 810 | 811 | self.message = self.mode 812 | 813 | def draw_rubberband(self, event, x0, y0, x1, y1): 814 | """Draw a rectangle rubberband to indicate zoom limits""" 815 | height = self.canvas.figure.bbox.height 816 | y1 = height - y1 817 | y0 = height - y0 818 | 819 | w = abs(x1 - x0) 820 | h = abs(y1 - y0) 821 | 822 | rect = [int(val)for val in (min(x0, x1), min(y0, y1), w, h)] 823 | self.canvas.drawRectangle(rect) 824 | 825 | def remove_rubberband(self): 826 | """Remove the rubberband""" 827 | self.canvas.drawRectangle(None) 828 | 829 | def _switch_on_zoom_mode(self, event): 830 | self._zoom_mode = event.key 831 | self.mouse_move(event) 832 | 833 | def _switch_off_zoom_mode(self, event): 834 | self._zoom_mode = None 835 | self.mouse_move(event) 836 | 837 | def drag_zoom(self, event): 838 | """the drag callback in zoom mode""" 839 | 840 | if self._xypress: 841 | x, y = event.x, event.y 842 | lastx, lasty, a, ind, view = self._xypress[0] 843 | 844 | # adjust x, last, y, last 845 | x1, y1, x2, y2 = a.bbox.extents 846 | x, lastx = max(min(x, lastx), x1), min(max(x, lastx), x2) 847 | y, lasty = max(min(y, lasty), y1), min(max(y, lasty), y2) 848 | 849 | if self._zoom_mode == "x": 850 | x1, y1, x2, y2 = a.bbox.extents 851 | y, lasty = y1, y2 852 | elif self._zoom_mode == "y": 853 | x1, y1, x2, y2 = a.bbox.extents 854 | x, lastx = x1, x2 855 | 856 | self.draw_rubberband(event, x, y, lastx, lasty) 857 | 858 | def press_zoom(self, event): 859 | """the press mouse button in zoom to rect mode callback""" 860 | # If we're already in the middle of a zoom, pressing another 861 | # button works to "cancel" 862 | if self._ids_zoom != []: 863 | for zoom_id in self._ids_zoom: 864 | self.canvas.mpl_disconnect(zoom_id) 865 | self.release(event) 866 | self.draw_with_locators_update() 867 | self._xypress = None 868 | self._button_pressed = None 869 | self._ids_zoom = [] 870 | return 871 | 872 | if event.button == 1: 873 | self._button_pressed = 1 874 | elif event.button == 3: 875 | self._button_pressed = 3 876 | else: 877 | self._button_pressed = None 878 | return 879 | 880 | x, y = event.x, event.y 881 | 882 | # push the current view to define home if stack is empty 883 | if self._views.empty(): 884 | self.push_current() 885 | 886 | self._xypress = [] 887 | for i, a in enumerate(self.canvas.figure.get_axes()): 888 | if (x is not None and y is not None and a.in_axes(event) and 889 | a.get_navigate() and a.can_zoom()): 890 | self._xypress.append((x, y, a, i, a._get_view())) 891 | 892 | id1 = self.canvas.mpl_connect('motion_notify_event', self.drag_zoom) 893 | id2 = self.canvas.mpl_connect('key_press_event', 894 | self._switch_on_zoom_mode) 895 | id3 = self.canvas.mpl_connect('key_release_event', 896 | self._switch_off_zoom_mode) 897 | 898 | self._ids_zoom = id1, id2, id3 899 | self._zoom_mode = event.key 900 | 901 | self.press(event) 902 | 903 | def release_zoom(self, event): 904 | """the release mouse button callback in zoom to rect mode""" 905 | for zoom_id in self._ids_zoom: 906 | self.canvas.mpl_disconnect(zoom_id) 907 | self._ids_zoom = [] 908 | 909 | self.remove_rubberband() 910 | 911 | if not self._xypress: 912 | return 913 | 914 | last_a = [] 915 | 916 | for cur_xypress in self._xypress: 917 | x, y = event.x, event.y 918 | lastx, lasty, a, ind, view = cur_xypress 919 | # ignore singular clicks - 5 pixels is a threshold 920 | # allows the user to "cancel" a zoom action 921 | # by zooming by less than 5 pixels 922 | if ((abs(x - lastx) < 5 and self._zoom_mode!="y") or 923 | (abs(y - lasty) < 5 and self._zoom_mode!="x")): 924 | self._xypress = None 925 | self.release(event) 926 | self.draw_with_locators_update() 927 | return 928 | 929 | # detect twinx,y axes and avoid double zooming 930 | twinx, twiny = False, False 931 | if last_a: 932 | for la in last_a: 933 | if a.get_shared_x_axes().joined(a, la): 934 | twinx = True 935 | if a.get_shared_y_axes().joined(a, la): 936 | twiny = True 937 | last_a.append(a) 938 | 939 | if self._button_pressed == 1: 940 | direction = 'in' 941 | elif self._button_pressed == 3: 942 | direction = 'out' 943 | else: 944 | continue 945 | 946 | a._set_view_from_bbox((lastx, lasty, x, y), direction, 947 | self._zoom_mode, twinx, twiny) 948 | 949 | self.draw_with_locators_update() 950 | self._xypress = None 951 | self._button_pressed = None 952 | 953 | self._zoom_mode = None 954 | 955 | self.push_current() 956 | self.release(event) 957 | 958 | @QtCore.pyqtSlot() 959 | def zoom(self, *args): 960 | """Activate zoom to rect mode""" 961 | if self._active == 'ZOOM': 962 | self._active = None 963 | else: 964 | self._active = 'ZOOM' 965 | 966 | if self._idPress is not None: 967 | self._idPress = self.canvas.mpl_disconnect(self._idPress) 968 | self.mode = '' 969 | 970 | if self._idRelease is not None: 971 | self._idRelease = self.canvas.mpl_disconnect(self._idRelease) 972 | self.mode = '' 973 | 974 | if self._active: 975 | self._idPress = self.canvas.mpl_connect('button_press_event', 976 | self.press_zoom) 977 | self._idRelease = self.canvas.mpl_connect('button_release_event', 978 | self.release_zoom) 979 | self.mode = 'zoom rect' 980 | self.canvas.widgetlock(self) 981 | else: 982 | self.canvas.widgetlock.release(self) 983 | 984 | for a in self.canvas.figure.get_axes(): 985 | a.set_navigate_mode(self._active) 986 | 987 | self.message = self.mode 988 | 989 | @QtCore.pyqtSlot() 990 | def tight_layout(self): 991 | self.figure.tight_layout() 992 | # self._setSliderPositions() 993 | self.draw_idle() 994 | 995 | @QtCore.pyqtSlot() 996 | def reset_margin(self): 997 | self.figure.subplots_adjust(**self._defaults) 998 | # self._setSliderPositions() 999 | self.draw_idle() 1000 | 1001 | @QtCore.pyqtSlot(str) 1002 | def print_figure(self, fname, *args, **kwargs): 1003 | if fname: 1004 | fname = QtCore.QUrl(fname).toLocalFile() 1005 | # save dir for next time 1006 | savefig_dir = os.path.dirname(six.text_type(fname)) 1007 | matplotlib.rcParams['savefig.directory'] = savefig_dir 1008 | fname = six.text_type(fname) 1009 | FigureCanvasAgg.print_figure(self, fname, *args, **kwargs) 1010 | self.draw() 1011 | 1012 | FigureCanvasQTAgg = FigureCanvasQtQuickAgg 1013 | FigureCanvasQTAggToolbar = FigureQtQuickAggToolbar -------------------------------------------------------------------------------- /backend/mpl_qquick.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | CALL C:\Anaconda3\Scripts\activate.bat qtquick 3 | python %~dpn0.py %* -------------------------------------------------------------------------------- /backend/mpl_qquick.py: -------------------------------------------------------------------------------- 1 | """ 2 | Series of data are loaded from a .csv file, and their names are 3 | displayed in a checkable list view. The user can select the series 4 | it wants from the list and plot them on a matplotlib canvas. 5 | Use the sample .csv file that comes with the script for an example 6 | of data series. 7 | 8 | [2016-11-06] Convert to QtQuick 2.0 - Qt.labs.controls 1.0 9 | [2016-11-05] Convert to QtQuick 2.0 - QtQuick Controls 1.0 10 | [2016-11-01] Update to PyQt5.6 and python 3.5 11 | 12 | Frederic Collonval (fcollonval@gmail.com) 13 | 14 | Inspired from the work of Eli Bendersky (eliben@gmail.com): 15 | https://github.com/eliben/code-for-blog/tree/master/2009/pyqt_dataplot_demo 16 | 17 | License: MIT License 18 | Last modified: 2016-11-06 19 | """ 20 | import sys, os, csv 21 | from PyQt5.QtCore import Qt, QObject, QUrl 22 | from PyQt5.QtGui import QGuiApplication 23 | from PyQt5.QtQml import qmlRegisterType 24 | from PyQt5.QtQuick import QQuickView 25 | 26 | from backend_qtquick5 import FigureCanvasQTAgg 27 | 28 | import matplotlib 29 | # matplotlib.use('module://backend_qtquick5') 30 | # from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 31 | # from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 32 | # from matplotlib.figure import Figure 33 | import matplotlib.pyplot as plt 34 | 35 | import numpy as np 36 | 37 | def main(): 38 | argv = sys.argv 39 | 40 | # Trick to set the style / not found how to do it in pythonic way 41 | argv.extend(["-style", "universal"]) 42 | app = QGuiApplication(argv) 43 | 44 | qmlRegisterType(FigureCanvasQTAgg, "Backend", 1, 0, "FigureCanvas") 45 | 46 | view = QQuickView() 47 | view.setResizeMode(QQuickView.SizeRootObjectToView) 48 | view.setSource(QUrl('backend_qtquick5/Figure.qml')) 49 | view.show() 50 | 51 | win = view.rootObject() 52 | fig = win.findChild(QObject, "figure").getFigure() 53 | print(fig) 54 | ax = fig.add_subplot(111) 55 | x = np.linspace(-5, 5) 56 | ax.plot(x, np.sin(x)) 57 | 58 | rc = app.exec_() 59 | # There is some trouble arising when deleting all the objects here 60 | # but I have not figure out how to solve the error message. 61 | # It looks like 'app' is destroyed before some QObject 62 | sys.exit(rc) 63 | 64 | 65 | if __name__ == "__main__": 66 | main() -------------------------------------------------------------------------------- /backend/mpl_qquick_toolbar.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | CALL C:\Anaconda3\Scripts\activate.bat qtquick 3 | python %~dpn0.py %* -------------------------------------------------------------------------------- /backend/mpl_qquick_toolbar.py: -------------------------------------------------------------------------------- 1 | """ 2 | Series of data are loaded from a .csv file, and their names are 3 | displayed in a checkable list view. The user can select the series 4 | it wants from the list and plot them on a matplotlib canvas. 5 | Use the sample .csv file that comes with the script for an example 6 | of data series. 7 | 8 | [2016-11-06] Convert to QtQuick 2.0 - Qt.labs.controls 1.0 9 | [2016-11-05] Convert to QtQuick 2.0 - QtQuick Controls 1.0 10 | [2016-11-01] Update to PyQt5.6 and python 3.5 11 | 12 | Frederic Collonval (fcollonval@gmail.com) 13 | 14 | Inspired from the work of Eli Bendersky (eliben@gmail.com): 15 | https://github.com/eliben/code-for-blog/tree/master/2009/pyqt_dataplot_demo 16 | 17 | License: MIT License 18 | Last modified: 2016-11-06 19 | """ 20 | import sys, os, csv 21 | from PyQt5.QtCore import Qt, QObject, QUrl 22 | from PyQt5.QtGui import QGuiApplication 23 | from PyQt5.QtQml import qmlRegisterType 24 | from PyQt5.QtQuick import QQuickView 25 | 26 | from backend_qtquick5 import FigureCanvasQTAggToolbar, MatplotlibIconProvider 27 | 28 | import matplotlib 29 | # matplotlib.use('module://backend_qtquick5') 30 | # from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 31 | # from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 32 | # from matplotlib.figure import Figure 33 | import matplotlib.pyplot as plt 34 | 35 | import numpy as np 36 | 37 | def main(): 38 | argv = sys.argv 39 | 40 | app = QGuiApplication(argv) 41 | 42 | qmlRegisterType(FigureCanvasQTAggToolbar, "Backend", 1, 0, "FigureToolbar") 43 | 44 | imgProvider = MatplotlibIconProvider() 45 | view = QQuickView() 46 | view.engine().addImageProvider("mplIcons", imgProvider) 47 | view.setResizeMode(QQuickView.SizeRootObjectToView) 48 | view.setSource(QUrl('backend_qtquick5/FigureToolbar.qml')) 49 | 50 | win = view.rootObject() 51 | fig = win.findChild(QObject, "figure").getFigure() 52 | ax = fig.add_subplot(111) 53 | x = np.linspace(-5, 5) 54 | ax.plot(x, np.sin(x)) 55 | 56 | view.show() 57 | 58 | rc = app.exec_() 59 | # There is some trouble arising when deleting all the objects here 60 | # but I have not figure out how to solve the error message. 61 | # It looks like 'app' is destroyed before some QObject 62 | sys.exit(rc) 63 | 64 | 65 | if __name__ == "__main__": 66 | main() -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: qtquick 2 | channels: !!python/tuple 3 | - defaults 4 | dependencies: 5 | - cycler=0.10.0=py35_0 6 | - icu=57.1=vc14_0 7 | - jpeg=8d=vc14_2 8 | - libpng=1.6.22=vc14_0 9 | - matplotlib=1.5.3=np111py35_1 10 | - mkl=11.3.3=1 11 | - numpy=1.11.2=py35_0 12 | - openssl=1.0.2j=vc14_0 13 | - pip=9.0.0=py35_0 14 | - pyparsing=2.1.4=py35_0 15 | - pyqt=5.6.0=py35_0 16 | - python=3.5.2=0 17 | - python-dateutil=2.5.3=py35_0 18 | - pytz=2016.7=py35_0 19 | - qt=5.6.0=vc14_0 20 | - setuptools=27.2.0=py35_1 21 | - sip=4.18=py35_0 22 | - six=1.10.0=py35_0 23 | - tk=8.5.18=vc14_0 24 | - vs2015_runtime=14.0.25123=0 25 | - wheel=0.29.0=py35_0 26 | - zlib=1.2.8=vc14_3 27 | prefix: C:\Anaconda3\envs\qtquick 28 | 29 | -------------------------------------------------------------------------------- /qt_mpl_data.csv: -------------------------------------------------------------------------------- 1 | 1990 Sales,15, 82, 95, 83, 123, 9, 59, 30, 19, 40, 99, 10 2 | 1991 Sales,12, 16, 15, 17, 24, 38, 71, 88, 62, 40, 43, 12 3 | 1992 Sales,6, 26, 11, 67, 24, 38, 1, 8, 62, 40, 43, 12 4 | 1993 Sales,19, 19, 19, 19, 26, 39, 79, 89, 69, 49, 49, 19 5 | --------------------------------------------------------------------------------