eric6/MicroPython/MicroPythonGraphWidget.py

branch
micropython
changeset 7065
e3d04faced34
child 7066
e3d034e65afc
equal deleted inserted replaced
7062:ac12da95958b 7065:e3d04faced34
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the MicroPython graph widget.
8 """
9
10 from __future__ import unicode_literals
11
12 from collections import deque
13 import bisect
14
15 from PyQt5.QtCore import pyqtSignal, pyqtSlot
16 from PyQt5.QtGui import QPainter
17 from PyQt5.QtWidgets import (
18 QWidget, QHBoxLayout, QVBoxLayout, QToolButton, QSizePolicy, QSpacerItem
19 )
20 from PyQt5.QtChart import QChartView, QChart, QLineSeries, QValueAxis
21
22 import UI.PixmapCache
23
24
25 class MicroPythonGraphWidget(QWidget):
26 """
27 Class implementing the MicroPython graph widget.
28
29 @signal dataFlood emitted to indicate, that too much data is received
30 """
31 dataFlood = pyqtSignal()
32
33 def __init__(self, parent=None):
34 """
35 Constructor
36
37 @param parent reference to the parent widget
38 @type QWidget
39 """
40 super(MicroPythonGraphWidget, self).__init__(parent)
41
42 self.__layout = QHBoxLayout()
43 self.__layout.setContentsMargins(0, 0, 0, 0)
44 self.setLayout(self.__layout)
45
46 self.__chartView = QChartView(self)
47 self.__chartView.setSizePolicy(
48 QSizePolicy.Expanding, QSizePolicy.Expanding)
49 self.__layout.addWidget(self.__chartView)
50
51 self.__buttonsLayout = QVBoxLayout()
52 self.__buttonsLayout.setContentsMargins(0, 0, 0, 0)
53 self.__layout.addLayout(self.__buttonsLayout)
54
55 self.__saveButton = QToolButton(self)
56 self.__saveButton.setIcon(UI.PixmapCache.getIcon("fileSave"))
57 self.__saveButton.setToolTip(self.tr("Press to save the raw data"))
58 self.__saveButton.clicked.connect(self.on_saveButton_clicked)
59 self.__buttonsLayout.addWidget(self.__saveButton)
60
61 spacerItem = QSpacerItem(20, 20, QSizePolicy.Minimum,
62 QSizePolicy.Expanding)
63 self.__buttonsLayout.addItem(spacerItem)
64
65 # holds the data to be checked for plotable data
66 self.__inputBuffer = []
67 # holds the raw data
68 self.__rawData = []
69
70 self.__maxX = 100
71 self.__maxY = 1000
72 self.__flooded = False # flag indicating a data flood
73
74 self.__data = [deque([0] * self.__maxX)]
75 self.__series = [QLineSeries()]
76
77 # Y-axis ranges
78 self.__yRanges = [1, 5, 10, 25, 50, 100, 250, 500, 1000]
79
80 # setup the chart
81 self.__chart = QChart()
82 self.__chart.legend().hide()
83 self.__chart.addSeries(self.__series[0])
84 self.__axisX = QValueAxis()
85 self.__axisX.setRange(0, self.__maxX)
86 self.__axisX.setLabelFormat("time")
87 self.__axisY = QValueAxis()
88 self.__axisY.setRange(-self.__maxY, self.__maxY)
89 self.__axisY.setLabelFormat("%d")
90 self.__chart.setAxisX(self.__axisX, self.__series[0])
91 self.__chart.setAxisY(self.__axisY, self.__series[0])
92 self.__chartView.setChart(self.__chart)
93 self.__chartView.setRenderHint(QPainter.Antialiasing)
94
95 @pyqtSlot(bytes)
96 def processData(self, data):
97 """
98 Public slot to process the raw data.
99
100 It takes raw bytes, checks the data for a valid tuple of ints or
101 floats and adds the data to the graph. If the the length of the bytes
102 data is greater than 1024 then a dataFlood signal is emitted to ensure
103 eric can take action to remain responsive.
104
105 @param data raw data received from the connected device via the main
106 device widget
107 @type bytes
108 """
109 # flooding guard
110 if self.__flooded:
111 return
112
113 if len(data) > 1024:
114 self.__flooded = True
115 self.dataFlood.emit()
116 return
117
118 data = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
119 self.__inputBuffer.append(data)
120
121 # check if the data contains a Python tuple containing numbers (int
122 # or float) on a single line
123 inputBytes = b"".join(self.__inputBuffer)
124 lines = inputBytes.splitlines(True)
125 for line in lines:
126 if not line.endswith(b"\n"):
127 # incomplete line (last line); skip it
128 continue
129
130 line = line.strip()
131 if line.startswith(b"(") and line.endswith(b")"):
132 # it may be a tuple we are interested in
133 rawValues = [val.strip() for val in line[1:-1].split(b",")]
134 values = []
135 for raw in rawValues:
136 try:
137 values.append(int(raw))
138 # ok, it is an integer
139 continue
140 except ValueError:
141 # test for a float
142 pass
143 try:
144 values.append(float(raw))
145 except ValueError:
146 # it is not an int or float, ignore it
147 continue
148 if values:
149 self.__addData(tuple(values))
150
151 self.__inputBuffer = []
152 if lines[-1] and not lines[-1].endswith(b"\n"):
153 # Append any left over bytes for processing next time data is
154 # received.
155 self.__inputBuffer.append(lines[-1])
156
157 def __addData(self, values):
158 """
159 Private method to add a tuple of values to the graph.
160
161 It ensures there are the required number of line series, adds the data
162 to the line series and updates the range of the chart so the chart
163 displays nicely.
164
165 @param values tuple containing the data to be added
166 @type tuple of int or float
167 """
168 # store incoming data to be able to dump it as CSV upon request
169 self.__rawData.append(values)
170
171 # check number of incoming values and adjust line series accordingly
172 if len(values) != len(self.__series):
173 valuesLen = len(values)
174 seriesLen = len(self.__series)
175 if valuesLen > seriesLen:
176 # add a nwe line series
177 for _index in range(valuesLen - seriesLen):
178 newSeries = QLineSeries()
179 self.__chart.addSeries(newSeries)
180 self.__chart.setAxisX(self.__axisX, newSeries)
181 self.__chart.setAxisY(self.__axisY, newSeries)
182 self.__series.append(newSeries)
183 self.__data.append(deque([0] * self.__maxX))
184 else:
185 # remove obsolete line series
186 for oldSeries in self.__series[valuesLen:]:
187 self.__chart.removeSeries(oldSeries)
188 self.__series = self.__series[:valuesLen]
189 self.__data = self.__data[:valuesLen]
190
191 # add the new values to the display and compute the maximum range
192 maxRanges = []
193 for index, value in enumerate(values):
194 self.__data[index].appendleft(value)
195 maxRanges.append(max([max(self.__data[index]),
196 abs(min(self.__data[index]))]))
197 if len(self.__data[index]) > self.__maxX:
198 self.__data[index].pop()
199
200 # re-scale the y-axis
201 maxYRange = max(maxRanges)
202 yRange = bisect.bisect_left(self.__yRanges, maxYRange)
203 if yRange < len(self.__yRanges):
204 self.__maxY = self.__yRanges[yRange]
205 elif maxYRange > self.__maxY:
206 self.__maxY += self.__maxY
207 elif maxYRange < self.__maxY / 2:
208 self.__maxY /= 2
209 self.__axisY.setRange(-self.__maxY, self.__maxY)
210
211 # ensure that floats are used to label the y-axis if the range is small
212 if self.__maxY <= 5:
213 self.__axisY.setLabelFormat("%2.2f")
214 else:
215 self.__axisY.setLabelFormat("%d")
216
217 # update the line series
218 for index, series in enumerate(self.__series):
219 series.clear()
220 xyValues = []
221 for x in range(self.__maxX):
222 value = self.__data[index][self.__maxX - 1 - x]
223 xyValues.append((x, value))
224 for xy in xyValues:
225 series.append(*xy)
226
227 @pyqtSlot()
228 def on_saveButton_clicked(self):
229 """
230 Private slot to save the raw data to a CSV file.
231 """
232 self.saveData()
233
234 def hasData(self):
235 """
236 Public method to check, if the chart contains some valid data.
237
238 @return flag indicating valid data
239 @rtype bool
240 """
241 # TODO: not implemented yet
242
243 def saveData(self):
244 """
245 Public method to save the dialog's raw data.
246
247 @return flag indicating success
248 @rtype bool
249 """
250 # TODO: not implemented yet

eric ide

mercurial