9 |
9 |
10 from __future__ import unicode_literals |
10 from __future__ import unicode_literals |
11 |
11 |
12 from collections import deque |
12 from collections import deque |
13 import bisect |
13 import bisect |
14 |
14 import os |
15 from PyQt5.QtCore import pyqtSignal, pyqtSlot |
15 import time |
|
16 import csv |
|
17 |
|
18 from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt |
16 from PyQt5.QtGui import QPainter |
19 from PyQt5.QtGui import QPainter |
17 from PyQt5.QtWidgets import ( |
20 from PyQt5.QtWidgets import ( |
18 QWidget, QHBoxLayout, QVBoxLayout, QToolButton, QSizePolicy, QSpacerItem |
21 QWidget, QHBoxLayout, QVBoxLayout, QToolButton, QSizePolicy, QSpacerItem, |
|
22 QLabel, QSpinBox |
19 ) |
23 ) |
20 from PyQt5.QtChart import QChartView, QChart, QLineSeries, QValueAxis |
24 from PyQt5.QtChart import QChartView, QChart, QLineSeries, QValueAxis |
21 |
25 |
|
26 from E5Gui import E5MessageBox |
|
27 |
22 import UI.PixmapCache |
28 import UI.PixmapCache |
|
29 import Preferences |
23 |
30 |
24 |
31 |
25 class MicroPythonGraphWidget(QWidget): |
32 class MicroPythonGraphWidget(QWidget): |
26 """ |
33 """ |
27 Class implementing the MicroPython graph widget. |
34 Class implementing the MicroPython graph widget. |
38 @type QWidget |
45 @type QWidget |
39 """ |
46 """ |
40 super(MicroPythonGraphWidget, self).__init__(parent) |
47 super(MicroPythonGraphWidget, self).__init__(parent) |
41 |
48 |
42 self.__layout = QHBoxLayout() |
49 self.__layout = QHBoxLayout() |
43 self.__layout.setContentsMargins(0, 0, 0, 0) |
50 self.__layout.setContentsMargins(2, 2, 2, 2) |
44 self.setLayout(self.__layout) |
51 self.setLayout(self.__layout) |
45 |
52 |
46 self.__chartView = QChartView(self) |
53 self.__chartView = QChartView(self) |
47 self.__chartView.setSizePolicy( |
54 self.__chartView.setSizePolicy( |
48 QSizePolicy.Expanding, QSizePolicy.Expanding) |
55 QSizePolicy.Expanding, QSizePolicy.Expanding) |
49 self.__layout.addWidget(self.__chartView) |
56 self.__layout.addWidget(self.__chartView) |
50 |
57 |
51 self.__buttonsLayout = QVBoxLayout() |
58 self.__verticalLayout = QVBoxLayout() |
52 self.__buttonsLayout.setContentsMargins(0, 0, 0, 0) |
59 self.__verticalLayout.setContentsMargins(0, 0, 0, 0) |
53 self.__layout.addLayout(self.__buttonsLayout) |
60 self.__layout.addLayout(self.__verticalLayout) |
54 |
61 |
55 self.__saveButton = QToolButton(self) |
62 self.__saveButton = QToolButton(self) |
56 self.__saveButton.setIcon(UI.PixmapCache.getIcon("fileSave")) |
63 self.__saveButton.setIcon(UI.PixmapCache.getIcon("fileSave")) |
57 self.__saveButton.setToolTip(self.tr("Press to save the raw data")) |
64 self.__saveButton.setToolTip(self.tr("Press to save the raw data")) |
58 self.__saveButton.clicked.connect(self.on_saveButton_clicked) |
65 self.__saveButton.clicked.connect(self.on_saveButton_clicked) |
59 self.__buttonsLayout.addWidget(self.__saveButton) |
66 self.__verticalLayout.addWidget(self.__saveButton) |
|
67 self.__verticalLayout.setAlignment(self.__saveButton, Qt.AlignHCenter) |
60 |
68 |
61 spacerItem = QSpacerItem(20, 20, QSizePolicy.Minimum, |
69 spacerItem = QSpacerItem(20, 20, QSizePolicy.Minimum, |
62 QSizePolicy.Expanding) |
70 QSizePolicy.Expanding) |
63 self.__buttonsLayout.addItem(spacerItem) |
71 self.__verticalLayout.addItem(spacerItem) |
|
72 |
|
73 label = QLabel(self.tr("max. X:")) |
|
74 self.__verticalLayout.addWidget(label) |
|
75 self.__verticalLayout.setAlignment(label, Qt.AlignHCenter) |
|
76 |
|
77 self.__maxX = 100 |
|
78 self.__maxXSpinBox = QSpinBox() |
|
79 self.__maxXSpinBox.setMinimum(100) |
|
80 self.__maxXSpinBox.setMaximum(1000) |
|
81 self.__maxXSpinBox.setSingleStep(100) |
|
82 self.__maxXSpinBox.setToolTip(self.tr( |
|
83 "Enter the maximum number of data points to be plotted.")) |
|
84 self.__maxXSpinBox.setValue(self.__maxX) |
|
85 self.__maxXSpinBox.setAlignment(Qt.AlignRight) |
|
86 self.__verticalLayout.addWidget(self.__maxXSpinBox) |
64 |
87 |
65 # holds the data to be checked for plotable data |
88 # holds the data to be checked for plotable data |
66 self.__inputBuffer = [] |
89 self.__inputBuffer = [] |
67 # holds the raw data |
90 # holds the raw data |
68 self.__rawData = [] |
91 self.__rawData = [] |
69 |
92 self.__dirty = False |
70 self.__maxX = 100 |
93 |
71 self.__maxY = 1000 |
94 self.__maxY = 1000 |
72 self.__flooded = False # flag indicating a data flood |
95 self.__flooded = False # flag indicating a data flood |
73 |
96 |
74 self.__data = [deque([0] * self.__maxX)] |
97 self.__data = [deque([0] * self.__maxX)] |
75 self.__series = [QLineSeries()] |
98 self.__series = [QLineSeries()] |
89 self.__axisY.setLabelFormat("%d") |
112 self.__axisY.setLabelFormat("%d") |
90 self.__chart.setAxisX(self.__axisX, self.__series[0]) |
113 self.__chart.setAxisX(self.__axisX, self.__series[0]) |
91 self.__chart.setAxisY(self.__axisY, self.__series[0]) |
114 self.__chart.setAxisY(self.__axisY, self.__series[0]) |
92 self.__chartView.setChart(self.__chart) |
115 self.__chartView.setChart(self.__chart) |
93 self.__chartView.setRenderHint(QPainter.Antialiasing) |
116 self.__chartView.setRenderHint(QPainter.Antialiasing) |
|
117 |
|
118 self.__maxXSpinBox.valueChanged.connect(self.__handleMaxXChanged) |
94 |
119 |
95 @pyqtSlot(bytes) |
120 @pyqtSlot(bytes) |
96 def processData(self, data): |
121 def processData(self, data): |
97 """ |
122 """ |
98 Public slot to process the raw data. |
123 Public slot to process the raw data. |
151 self.__inputBuffer = [] |
180 self.__inputBuffer = [] |
152 if lines[-1] and not lines[-1].endswith(b"\n"): |
181 if lines[-1] and not lines[-1].endswith(b"\n"): |
153 # Append any left over bytes for processing next time data is |
182 # Append any left over bytes for processing next time data is |
154 # received. |
183 # received. |
155 self.__inputBuffer.append(lines[-1]) |
184 self.__inputBuffer.append(lines[-1]) |
|
185 |
|
186 # re-enable the inputs |
|
187 self.__saveButton.setEnabled(True) |
|
188 self.__maxXSpinBox.setEnabled(True) |
156 |
189 |
157 def __addData(self, values): |
190 def __addData(self, values): |
158 """ |
191 """ |
159 Private method to add a tuple of values to the graph. |
192 Private method to add a tuple of values to the graph. |
160 |
193 |
165 @param values tuple containing the data to be added |
198 @param values tuple containing the data to be added |
166 @type tuple of int or float |
199 @type tuple of int or float |
167 """ |
200 """ |
168 # store incoming data to be able to dump it as CSV upon request |
201 # store incoming data to be able to dump it as CSV upon request |
169 self.__rawData.append(values) |
202 self.__rawData.append(values) |
|
203 self.__dirty = True |
170 |
204 |
171 # check number of incoming values and adjust line series accordingly |
205 # check number of incoming values and adjust line series accordingly |
172 if len(values) != len(self.__series): |
206 if len(values) != len(self.__series): |
173 valuesLen = len(values) |
207 valuesLen = len(values) |
174 seriesLen = len(self.__series) |
208 seriesLen = len(self.__series) |
236 Public method to check, if the chart contains some valid data. |
270 Public method to check, if the chart contains some valid data. |
237 |
271 |
238 @return flag indicating valid data |
272 @return flag indicating valid data |
239 @rtype bool |
273 @rtype bool |
240 """ |
274 """ |
241 # TODO: not implemented yet |
275 return len(self.__rawData) > 0 |
|
276 |
|
277 def isDirty(self): |
|
278 """ |
|
279 Public method to check, if the chart contains unsaved data. |
|
280 |
|
281 @return flag indicating unsaved data |
|
282 @rtype bool |
|
283 """ |
|
284 return self.hasData() and self.__dirty |
242 |
285 |
243 def saveData(self): |
286 def saveData(self): |
244 """ |
287 """ |
245 Public method to save the dialog's raw data. |
288 Public method to save the dialog's raw data. |
246 |
289 |
247 @return flag indicating success |
290 @return flag indicating success |
248 @rtype bool |
291 @rtype bool |
249 """ |
292 """ |
250 # TODO: not implemented yet |
293 baseDir = (Preferences.getMultiProject("Workspace") or |
|
294 os.path.expanduser("~")) |
|
295 dataDir = os.path.join(baseDir, "data_capture") |
|
296 |
|
297 if not os.path.exists(dataDir): |
|
298 os.makedirs(dataDir) |
|
299 |
|
300 # save the raw data as a CSV file |
|
301 fileName = "{0}.csv".format(time.strftime("%Y%m%d-%H%M%S")) |
|
302 fullPath = os.path.join(dataDir, fileName) |
|
303 try: |
|
304 csvFile = open(fullPath, "w") |
|
305 csvWriter = csv.writer(csvFile) |
|
306 csvWriter.writerows(self.__rawData) |
|
307 csvFile.close() |
|
308 |
|
309 self.__dirty = False |
|
310 return True |
|
311 except (IOError, OSError) as err: |
|
312 E5MessageBox.critical( |
|
313 self, |
|
314 self.tr("Save Chart Data"), |
|
315 self.tr( |
|
316 """<p>The chart data could not be saved into file""" |
|
317 """ <b>{0}</b>.</p><p>Reason: {1}</p>""").format( |
|
318 fullPath, str(err))) |
|
319 return False |
|
320 |
|
321 @pyqtSlot(int) |
|
322 def __handleMaxXChanged(self, value): |
|
323 """ |
|
324 Private slot handling a change of the max. X spin box. |
|
325 |
|
326 @param value value of the spin box |
|
327 @type int |
|
328 """ |
|
329 delta = value - self.__maxX |
|
330 if delta == 0: |
|
331 # nothing to change |
|
332 return |
|
333 elif delta > 0: |
|
334 # range must be increased |
|
335 for deq in self.__data: |
|
336 deq.extend([0] * delta) |
|
337 else: |
|
338 # range must be decreased |
|
339 data = [] |
|
340 for deq in self.__data: |
|
341 data.append(deque(list(deq)[:value])) |
|
342 self.__data = data |
|
343 |
|
344 self.__maxX = value |
|
345 self.__axisX.setRange(0, self.__maxX) |