mu-editor / mu

A small, simple editor for beginner Python programmers. Written in Python and Qt5.
http://codewith.mu
GNU General Public License v3.0
1.41k stars 435 forks source link

Plotter pane #311

Closed ladyada closed 6 years ago

ladyada commented 6 years ago

ok im going to start hacking on adding a plotter current plan is to use QtCharts which will handle the drawing. ideally it will look for CSV-like data coming in off the repl. ideal would be if we could have REPL and plotter going at the same time. will see how it goes :)

ladyada commented 6 years ago

some noodling

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import random
from collections import deque
from itertools import islice
from PyQt5.QtCore import QPointF, QTimer
from PyQt5.QtChart import QChart, QLineSeries, QChartView, QValueAxis
from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QPushButton
from PyQt5.QtGui import QPainter

class PressureGraph(QWidget):
    def __init__(self):
        super().__init__()
        self.t = range(0,150)
        self.q = deque([0]*len(self.t))
        self.initUI()

    def on_resize(self, event):
        #print(event.size())
        x = event.size().width() // 2
        y = event.size().height()
        self.chart.axisY().setMax(y)
        self.chart.axisX().setMax(x)
        self.t = range(0, x)
        q_len = len(self.q)
        if x > q_len: # extend it!
            self.q.extendleft([0] * (x - q_len))
        if x < q_len: # contract it
            self.q = deque(islice(self.q, q_len-x, q_len))
        #print(self.q)
        self.chartView.update()

    def initUI(self):
        self.series = QLineSeries()

        self.chart = QChart()
        self.chart.legend().hide()
        self.chart.addSeries(self.series)

        self.axis_x = QValueAxis()
        self.axis_y = QValueAxis()
        self.axis_x.setRange(0, 150)
        self.axis_y.setRange(0, 200)
        self.axis_x.setLabelFormat("")
        self.axis_y.setLabelFormat("%d")
        self.chart.setAxisX(self.axis_x, self.series)
        self.chart.setAxisY(self.axis_y, self.series)

        self.chartView = QChartView(self.chart)
        self.chartView.setRenderHint(QPainter.Antialiasing)

        self.grid = QGridLayout()
        self.grid.addWidget(self.chartView)

        self.setLayout(self.grid)

        self.resize(1000,500)
        self.resizeEvent = lambda e: self.on_resize(e)

    def update(self, newpoint):
        self.q.append(newpoint)
        if len(self.q) > len(self.t):
            self.q.popleft()

        p_list = []
        for i in range(0, len(self.t)):

            if i > (len(self.q) - 1):
                temp = 0
            else:
                temp = self.q[len(self.t) - 1 - i]
            p_list.append((self.t[i], temp))

        self.series.clear()         
        for i in p_list:
            self.series.append(*i)

        self.chartView.update()

if __name__ == "__main__":
    import sys

    global graph

    app = QApplication([sys.argv])
    graph = PressureGraph()
    graph.show()

    timer = QTimer()
    timer.timeout.connect(lambda: graph.update(random.randrange(0,180)))
    timer.start(500) # in millis

    sys.exit(app.exec_())
ntoll commented 6 years ago

Oooh.... very nice (and simple). I wasn't aware of QtCharts. I can see how we could create a new control in the mu.interface.panes module. Such things can be docked together in Mu, so you'll be able to have the REPL and "charts" widget on the UI at the same time.

Here's a potential bump in the road: QtCharts only arrived in Qt5.9 (https://doc.qt.io/qt-5/whatsnew59.html#qt-charts-module) . On 17.04 Ubuntu I'm on Qt5.7 (so got around the problem of my system missing QtCharts by pip installing a binary wheel of 5.9 into my Python virtualenv). Raspbian (one of the target platforms) is only on Qt5.5. :-/

When we natively package Mu for Linux (as a .bin), OSX and Windows we include Qt as part of the package, although, looking at the build scripts, we're using a relatively old version of Qt depending on the platform. @carlosperate do you have any thoughts on us updating to PyQt 5.9 across all platforms? In any case, the problem of packaging is looming large in the front of my mind at the moment and I'll reference any work we do to this ticket so you (@ladyada) can see our progress.

I'm going to email @bennuttall at Raspberry Pi and Phil at Riverbank (who maintain PyQt) and ask them when/what/if/how I can help to update Raspbian to 5.9 so we remove this potential dependency pitfall.

ladyada commented 6 years ago

kk! the current design question is whether the repl/plotter should replace each other (on arduino ide, you can only have one or the other) or should you have two panes. either/or is much easier to implement. 2 panes is nicer, but then you have to figure out how to share the incoming serial port data.

right now the REPL pane 'owns' the serial connection https://github.com/mu-editor/mu/blob/master/mu/interface/panes.py#L123 so i'm not sure exactly how or which ill approach. i think two separate objects with a message/callback from serialport pane -> plotter pane wouldn't be too bad - there's very little data being managed.

ladyada commented 6 years ago

image

¯\_(ツ)_/¯

ntoll commented 6 years ago

Ooooh. That looks sweet.

The message passing approach sounds good especially as Qt has a notion of signals that emit messages to connected handlers. The object connected to the serial port only needs to emit things and anything else wanting such data as its emitted need only connect to the signal with a function to handle the emitted data.

Is your code pushed anywhere? I could probably bodge something together pretty fast to prove such message passing works well in this case (or not).

ntoll commented 6 years ago

Actually, check out https://github.com/mu-editor/mu/blob/master/mu/interface/panes.py#L137 where the self.serial.readyRead signal is connected to the on_serial_read method. This method could emit a signal of the read bytes for anything else connected to it to process further. Sound like a plan?

ladyada commented 6 years ago

i made a hook because i dont know about 'signals' and we can replace it later

https://github.com/adafruit/mu/blob/qtplotter/mu/interface/panes.py#L193

current status: needs to buffer the data for proper parsing as we dont get a full line sometimes

image

ntoll commented 6 years ago

@ladyada I'm spending some (much needed) time on Mu this week.

A quick question: was there any reason for using the QtCharts API (other than because it was there)?

I wonder, for the sake of those not on the latest Qt, if making use of Matplotlib would be a better/wider supported alternative? For example, something like https://matplotlib.org/examples/user_interfaces/embedding_in_qt5.html (note: the Python3/Jupyter REPL already means we need Matplotlib so it's an already met requirement).

Thoughts?

ladyada commented 6 years ago

hiya yah that's awesome, i also hope to go back and finish the charts

I used qcharts because we're already using Qt so it came up in my search, and it works really well. but, the chart can be reworked with matplotlib - is Qt not releasing Qt 5.9 for some platforms? 5.10 just came out ;)

ntoll commented 6 years ago

It's more a case of backwards compatibility for OS's that don't have Qt 5.9 packaged (especially Raspbian). I'll try to hack something together once I've finished the current tidy-ups.

ladyada commented 6 years ago

ok ill not write anymore code w.r.t. the plotter!

ntoll commented 6 years ago

I just tried a quick experiment based upon the code found at the link pasted above (the plot was updating at 20FPS)... it takes but a couple of seconds to get the fan started and for my 4-core CPU to hit 300% :-/ I think QCharts is definitely back on the table as a result and I'll try to manage the upstream dependencies with Raspbian folks.

ladyada commented 6 years ago

w/qchart i also had some bad CPU usage if i had more than 500 points. you could try reducing plotpoints? funny how this kinda thing is so hard for computers :D

yonghuming commented 6 years ago

wanderful work

ladyada commented 6 years ago

@ntoll fyi - ill keep hacking on the plotter but i dont want to hold up a release, since a different version of qt may be required! so please continue with the current release schedule, ill get the plotter into the next one! :)

ntoll commented 6 years ago

Ack @ladyada happy holidays to you and yours.

ntoll commented 6 years ago

FYI beta.13 just landed in PyPI.

ladyada commented 6 years ago

awesome, thanks for this great editor, ill try beta13 :)

ntoll commented 6 years ago

@ladyada happy new year.

I'm spending some time on the plotter this week. I'm working on my own plotter branch (https://github.com/mu-editor/mu/tree/plotter) and I intend to do the following:

plotter

Thoughts and comments most welcome. I wonder about what Mu should be listening for as a signal to plot something..? In your code you mention CSV like output. How about Mu watch for output that looks like a Python tuple of arbitrary (but consistent) length..? For example: (234, 567, 987) to represent three values to be plotted at the current timestamp. I also wonder about some way to let users export the current "session" as CSV so they can play with the data in other apps (such as a Jupyter notebook or a spreadsheet). Thoughts..?

I'll mention this work at this evening's London MicroPython meetup and I want to get it working in Adafruit and micro:bit modes in the first instance.

bennuttall commented 6 years ago

Looks awesome! Would other libraries need to do something to utilise the plotting functionality? It would be cool to plot GPIO Zero device values, amongst other things.

ntoll commented 6 years ago

Oi oi @bennuttall -- you're thinking exactly what I'm thinking. :smiley:

I intend to refactor things so the plotter can take any arbitrary numeric values as a tuple and plot them through time as they happen. The serial data coming from Adafruit/micro:bit based boards would be a good start, but I can't see it being a problem to connect it to stdout from a running process (for example, a GPIOZero based application that emits str(my_tuple) values to stdout).

Does this make sense?

carlosperate commented 6 years ago

Absolutely awesome work @ladyada and @ntoll, this feature will be great to have!

This is the PyQt plotting project that I briefly mentioned in a previous conversation: https://github.com/pyqtgraph/pyqtgraph The nice thing about this one is that it offers PyQt widget that could be easily integrated: http://pyqtgraph.org/documentation/widgets/index.html

The only thing that's a bit less convenient is that it brings a few dependencies with it, like numpy, and in an ideal world we should try to use as many "pure python" packages as possible, as it greatly simplifies the packaging.

Sorry I haven't really replied until now, I had this thread in my inbox for quite a while, and maybe you have already rolled your own widget. Hopefully it could still be useful even if it's just for reference, perhaps their version is less cpu heavy.

About the PyQt5 dependencies, at the moment the Windows AppVeyor builds will still be using Python 3.4, and therefore stuck with PyQt 5.5.1. This is not really by choice, but because if Python 3.5 or higher is used in AppVeyor it needs to also pack the WinCRT runtime dlls for it to work on Windows 7/8, which PyInstaller doesn't do out of the box. The official solution to this issue (http://pyinstaller.readthedocs.io/en/stable/usage.html#windows) is to either include a Visual C++ Redistributable Package installer, a Windows SDK installer, ask the users to update their OS via Windows update (as it looks like there is an update that could install these dlls anyway), or build the .exe in Windows 7. The first 3 are not really an option, and to do the last we would have to manually build Mu on a Windows 7 VM (I must admit at this point, this is starting to look more and more like the easiest solution if we do it only for official releases).

As far as the REPL output data goes, tuples or lists sound like the most pythonic way, and perhaps we can include the option to add a "magic string" for general configuration (i.e., mu_plotter_config = {x_min: 0, x_max: 100, ... etc}. Nevertheless, just printing a value per plot and have the graph dynamically configure itself with just that would definitely be a requirement for simplicity.

What would the code on the MicroPython board look like? We might want to consider this, in order to be able to make this as efficient as possible, as some users will definitely want to get as much "real time" data out as they can. Even though everything has to eventually become a string before it is sent to the UART, we might want to make sure the number of string operations are minimised.

ladyada commented 6 years ago

hiya i can build for win 7, i run that OS by default - it only takes a few minutes to build!

ladyada commented 6 years ago

oops i just saw there's a bigger note above - now im caffinated and awake!

Rename some things (the plotter isn't just useful for MicroPython - I imagine RPi's GPIOZero library could also use it in standard Python3 mode).

OK!

Refactor the handling of incoming serial data so QT signals are emitted when data for the REPL and/or plotter is received.

good idea :)

Update the REPL and Plotter panes to handle the expected signals.

ok!

Swanky new "Plotter" button image... DONE (see attached).

lol my mspaint job didnt impress? :)

Thoughts and comments most welcome. I wonder about what Mu should be listening for as a signal to plot something..? In your code you mention CSV like output. How about Mu watch for output that looks like a Python tuple of arbitrary (but consistent) length..? For example: (234, 567, 987) to represent three values to be plotted at the current timestamp. I also wonder about some way to let users export the current "session" as CSV so they can play with the data in other apps (such as a Jupyter notebook or a spreadsheet). Thoughts..?

ok so i think a () tuple that starts at the begining of a new line is probably the easiest to support. also if the line doesnt start with "(" you can just skip till the next new line. FYI Arduino just looks for CSV, and you cant use the plotter + serial console at the same time which i'd like to avoid (that's why the plotter pane doesnt replace the REPL its off to the side). Also with Arduino you also cant change the scaling, only autoscaling, but i'd like to add manual scaling support either thru a magic word or Qt sliders

i thought for saving data, best bet would to have the plotter pane only plot n points (250?) but keep a log of the full session history and you could export it with a button.

the thing i got stuck on last with the plotter is resizing the pane. i got it to work one but then not again, and also i couldnt figure out how to make it plot < n points, so the plot would 'crawl' across the screen.

ntoll commented 6 years ago

@ladyada we're on the same page here and I agree with you regarding simply using a tuple that starts on a new line as the easiest to support.

I'll have a play around with Qt signals and listeners so we don't get the plotter OR REPL situation as per Arduino. I agree that a user should be able to see both.

A quick question, the REPL is activated it sends a KeyboardInterrupt to the device so you always find yourself looking at the >>> Python prompt. My guess is this isn't what you'd want if you had the plotter open first.. Ergo, I propose if you start the REPL before the plotter, it sends the KeyboardInterrupt as is currently the case, and you can later start the plotter pane and CTRL-D soft restart the device to have both visible. Alternatively, if you have the plotter visible first, when the REPL is activated it does NOT send the KeyboardInterrupt (since you're probably interested in still seeing the plot). If you want the plotting to stop, just type CTRL-C in the REPL to send the KeyboardInterrupt "manually". Does this make sense / sound sensible?

As always, I'm open to ideas, comments and constructive critique!

In any case, I'll let you know when I have something to show for all of this.

ladyada commented 6 years ago

sounds very good to me - right now i require REPL open but thats mostly because i couldn't figure out how to separate the logic :)

ladyada commented 6 years ago

@ntoll heya nick, are there any updates on the plotter or are you working on other stuff right now? if you're paused on the plotter we can take another look at it about now.

ntoll commented 6 years ago

@ladyada today is the last day of a (hectic) three week block of work on Mu for RPi. I'm not doing anything from Tuesday of next week and had planned to spend the rest of the week getting it into shape so it can be merged to master.

I don't want to stop anyone from contributing or making progress... so how about you add the features you're interested in but leave the Qt / Mu related stuff (e.g. resizing, themes and so on) to me..?

In terms of features I was going to add: I was simply going to make it possible to have multiple plots appear on the chart to reflect the number of items in the emitted tuple (in my mind's eye, I imagined getting the x, y and z readings from an accelerometer and plotting all three value). Does this make sense?

ntoll commented 6 years ago

Also, apologies for the silence on my part.... I've been up to my eyes. But that changes from Tuesday when I have more free time to work on fun things.

ladyada commented 6 years ago

lets catch up next week :) that way we can make the most of our efforts! i'll hold back, thank you for the update and good luck with the finishing up & please ping when you need a tester - we have a range of computer/OSes we can test installers with

ntoll commented 6 years ago

This is now in master and landed in beta.15.

ladyada commented 6 years ago

yayy

CedarGroveStudios commented 6 years ago

Love the plotter. Very, very useful! An x-y version (not based on samples or time) would be valuable for plotting sets of position sensor data, as well.