eric6/MicroPython/MicroPythonGraphWidget.py

branch
micropython
changeset 7065
e3d04faced34
child 7066
e3d034e65afc
diff -r ac12da95958b -r e3d04faced34 eric6/MicroPython/MicroPythonGraphWidget.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/MicroPython/MicroPythonGraphWidget.py	Tue Jul 16 20:12:53 2019 +0200
@@ -0,0 +1,250 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the MicroPython graph widget.
+"""
+
+from __future__ import unicode_literals
+
+from collections import deque
+import bisect
+
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+from PyQt5.QtGui import QPainter
+from PyQt5.QtWidgets import (
+    QWidget, QHBoxLayout, QVBoxLayout, QToolButton, QSizePolicy, QSpacerItem
+)
+from PyQt5.QtChart import QChartView, QChart, QLineSeries, QValueAxis
+
+import UI.PixmapCache
+
+
+class MicroPythonGraphWidget(QWidget):
+    """
+    Class implementing the MicroPython graph widget.
+    
+    @signal dataFlood emitted to indicate, that too much data is received
+    """
+    dataFlood = pyqtSignal()
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super(MicroPythonGraphWidget, self).__init__(parent)
+        
+        self.__layout = QHBoxLayout()
+        self.__layout.setContentsMargins(0, 0, 0, 0)
+        self.setLayout(self.__layout)
+        
+        self.__chartView = QChartView(self)
+        self.__chartView.setSizePolicy(
+            QSizePolicy.Expanding, QSizePolicy.Expanding)
+        self.__layout.addWidget(self.__chartView)
+        
+        self.__buttonsLayout = QVBoxLayout()
+        self.__buttonsLayout.setContentsMargins(0, 0, 0, 0)
+        self.__layout.addLayout(self.__buttonsLayout)
+        
+        self.__saveButton = QToolButton(self)
+        self.__saveButton.setIcon(UI.PixmapCache.getIcon("fileSave"))
+        self.__saveButton.setToolTip(self.tr("Press to save the raw data"))
+        self.__saveButton.clicked.connect(self.on_saveButton_clicked)
+        self.__buttonsLayout.addWidget(self.__saveButton)
+        
+        spacerItem = QSpacerItem(20, 20, QSizePolicy.Minimum,
+                                 QSizePolicy.Expanding)
+        self.__buttonsLayout.addItem(spacerItem)
+        
+        # holds the data to be checked for plotable data
+        self.__inputBuffer = []
+        # holds the raw data
+        self.__rawData = []
+        
+        self.__maxX = 100
+        self.__maxY = 1000
+        self.__flooded = False  # flag indicating a data flood
+        
+        self.__data = [deque([0] * self.__maxX)]
+        self.__series = [QLineSeries()]
+        
+        # Y-axis ranges
+        self.__yRanges = [1, 5, 10, 25, 50, 100, 250, 500, 1000]
+        
+        # setup the chart
+        self.__chart = QChart()
+        self.__chart.legend().hide()
+        self.__chart.addSeries(self.__series[0])
+        self.__axisX = QValueAxis()
+        self.__axisX.setRange(0, self.__maxX)
+        self.__axisX.setLabelFormat("time")
+        self.__axisY = QValueAxis()
+        self.__axisY.setRange(-self.__maxY, self.__maxY)
+        self.__axisY.setLabelFormat("%d")
+        self.__chart.setAxisX(self.__axisX, self.__series[0])
+        self.__chart.setAxisY(self.__axisY, self.__series[0])
+        self.__chartView.setChart(self.__chart)
+        self.__chartView.setRenderHint(QPainter.Antialiasing)
+    
+    @pyqtSlot(bytes)
+    def processData(self, data):
+        """
+        Public slot to process the raw data.
+        
+        It takes raw bytes, checks the data for a valid tuple of ints or
+        floats and adds the data to the graph. If the the length of the bytes
+        data is greater than 1024 then a dataFlood signal is emitted to ensure
+        eric can take action to remain responsive.
+        
+        @param data raw data received from the connected device via the main
+            device widget
+        @type bytes
+        """
+        # flooding guard
+        if self.__flooded:
+            return
+        
+        if len(data) > 1024:
+            self.__flooded = True
+            self.dataFlood.emit()
+            return
+        
+        data = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
+        self.__inputBuffer.append(data)
+        
+        # check if the data contains a Python tuple containing numbers (int
+        # or float) on a single line
+        inputBytes = b"".join(self.__inputBuffer)
+        lines = inputBytes.splitlines(True)
+        for line in lines:
+            if not line.endswith(b"\n"):
+                # incomplete line (last line); skip it
+                continue
+            
+            line = line.strip()
+            if line.startswith(b"(") and line.endswith(b")"):
+                # it may be a tuple we are interested in
+                rawValues = [val.strip() for val in line[1:-1].split(b",")]
+                values = []
+                for raw in rawValues:
+                    try:
+                        values.append(int(raw))
+                        # ok, it is an integer
+                        continue
+                    except ValueError:
+                        # test for a float
+                        pass
+                    try:
+                        values.append(float(raw))
+                    except ValueError:
+                        # it is not an int or float, ignore it
+                        continue
+                if values:
+                    self.__addData(tuple(values))
+        
+        self.__inputBuffer = []
+        if lines[-1] and not lines[-1].endswith(b"\n"):
+            # Append any left over bytes for processing next time data is
+            # received.
+            self.__inputBuffer.append(lines[-1])
+    
+    def __addData(self, values):
+        """
+        Private method to add a tuple of values to the graph.
+        
+        It ensures there are the required number of line series, adds the data
+        to the line series and updates the range of the chart so the chart
+        displays nicely.
+        
+        @param values tuple containing the data to be added
+        @type tuple of int or float
+        """
+        # store incoming data to be able to dump it as CSV upon request
+        self.__rawData.append(values)
+        
+        # check number of incoming values and adjust line series accordingly
+        if len(values) != len(self.__series):
+            valuesLen = len(values)
+            seriesLen = len(self.__series)
+            if valuesLen > seriesLen:
+                # add a nwe line series
+                for _index in range(valuesLen - seriesLen):
+                    newSeries = QLineSeries()
+                    self.__chart.addSeries(newSeries)
+                    self.__chart.setAxisX(self.__axisX, newSeries)
+                    self.__chart.setAxisY(self.__axisY, newSeries)
+                    self.__series.append(newSeries)
+                    self.__data.append(deque([0] * self.__maxX))
+            else:
+                # remove obsolete line series
+                for oldSeries in self.__series[valuesLen:]:
+                    self.__chart.removeSeries(oldSeries)
+                self.__series = self.__series[:valuesLen]
+                self.__data = self.__data[:valuesLen]
+        
+        # add the new values to the display and compute the maximum range
+        maxRanges = []
+        for index, value in enumerate(values):
+            self.__data[index].appendleft(value)
+            maxRanges.append(max([max(self.__data[index]),
+                                  abs(min(self.__data[index]))]))
+            if len(self.__data[index]) > self.__maxX:
+                self.__data[index].pop()
+        
+        # re-scale the y-axis
+        maxYRange = max(maxRanges)
+        yRange = bisect.bisect_left(self.__yRanges, maxYRange)
+        if yRange < len(self.__yRanges):
+            self.__maxY = self.__yRanges[yRange]
+        elif maxYRange > self.__maxY:
+            self.__maxY += self.__maxY
+        elif maxYRange < self.__maxY / 2:
+            self.__maxY /= 2
+        self.__axisY.setRange(-self.__maxY, self.__maxY)
+        
+        # ensure that floats are used to label the y-axis if the range is small
+        if self.__maxY <= 5:
+            self.__axisY.setLabelFormat("%2.2f")
+        else:
+            self.__axisY.setLabelFormat("%d")
+        
+        # update the line series
+        for index, series in enumerate(self.__series):
+            series.clear()
+            xyValues = []
+            for x in range(self.__maxX):
+                value = self.__data[index][self.__maxX - 1 - x]
+                xyValues.append((x, value))
+            for xy in xyValues:
+                series.append(*xy)
+    
+    @pyqtSlot()
+    def on_saveButton_clicked(self):
+        """
+        Private slot to save the raw data to a CSV file.
+        """
+        self.saveData()
+    
+    def hasData(self):
+        """
+        Public method to check, if the chart contains some valid data.
+        
+        @return flag indicating valid data
+        @rtype bool
+        """
+        # TODO: not implemented yet
+    
+    def saveData(self):
+        """
+        Public method to save the dialog's raw data.
+        
+        @return flag indicating success
+        @rtype bool
+        """
+        # TODO: not implemented yet

eric ide

mercurial