0

I am on python 3.7. I am trying to read data from a serial port, it would be 7 different bytes. Then I would like to plot each different byte on a different subplot. I want to read the serial port every 500ms and each time I read add the new data to the subplots. Every read is giving one more data to plot on every subplot. That's basically sensor reading.

Here is the code I have written:

from time import sleep import serial import matplotlib.pyplot as plt

f=plt.figure(1)
ax=[0 for x in range(7)]
for i in range(0,7):
    ax[i]=f.add_subplot(4,2,1+i)

ser = serial.Serial('COM3', 115200) # Establish the connection on a specific port
counter = 0 
byte=ser.readline() #first line not to be plotted
while True:
    counter +=1
    ser.write(b'9') # send a command to the arduino
    byte=ser.read(7) #read 7 bytes back
    for i in range(0,7):
        ax[i].plot(counter, byte[i]) # Trying to plot the new values to each different subplots
    plt.pause(0.01)
    sleep(.5) # Delay for one half of a second

The figure is showing and the x axis and y axis are adapting to the value I want to plt but there is no data at all on the plot. If I use scatter instead of plot it works, but then it is less versatile and I can't draw te type of graph I want. I also try to reproduce the problem without using a serial data but just displaying points of a list one after the other like that:

import matplotlib.pyplot as plt
from time import sleep

f=plt.figure()
series=[[4,3,2,1],[8,7,6,5],[12,11,10,9]]
counter=0
ax=[0 for x in range(7)]

for i in range(0,3):
    ax[i]=f.add_subplot(4,2,1+i)


for j in range (0,4):
    counter=counter+1
    for i in range(0,3):
        ax[i].plot(counter,series[i][j])
    plt.pause(0.01)
    sleep(1)

And it is doing exactly the same thing, the final image I have on the graph is that: enter image description here Which shows axis took what I wanted to plot but did not plot anything. The point is I do not want to clear the full plot and redraw everything because for the data sensor I will have about 30days of data to display in continuous. What am I doing wrong with the code I have written?

EDIT: After comment of ImportanceOfBeingErnest I have tried implementing the answer given here. The code is then:

from time import sleep
import serial
import matplotlib.pyplot as plt
import numpy

plt.ion()
f=plt.figure()
ax=[0 for x in range(7)]
lines=[0 for x in range(7)]
for i in range(0,7):
    ax[i]=f.add_subplot(4,2,1+i)
    lines[i]=ax[0].plot([],[])


def update_line(hl, new_datax, new_datay):
    hl.set_xdata(numpy.append(hl.get_xdata(), new_datax))
    hl.set_ydata(numpy.append(hl.get_ydata(), new_datay))
    plt.draw()

ser = serial.Serial('COM3', 115200) # Establish the connection on a specific port
counter = 0 
byte=ser.readline() #first line not to be plotted
while True:
    counter +=1
    ser.write(b'9') # send a command to the arduino
    byte=ser.read(7) #read 7 bytes back
    for i in range(0,7):
        update_line(lines[i][0], counter, byte[i]) # Trying to plot the new values to each different subplots
    plt.pause(0.01)
    sleep(.5) # Delay for one half of a second

But it still does not show anything. I kind of guess I am missing a plot and/or clear somewhere but after trying several options can't get it to work.

damien
  • 415
  • 1
  • 6
  • 13
  • You plot a line plot of a single point. But no line can be established between a point and itself, rather lines need at least two points to be a line. Of course you can add a marker to the line (marker="o") to see the points (that would be the same as a scatter). But if you really want a line, you should plot all the points. – ImportanceOfBeingErnest Feb 06 '19 at 15:55
  • Oh I see I understand better. Is there no way to do a line from the previous point displayed before then? Do you have to replot everything? – damien Feb 06 '19 at 16:03
  • Well, usually you would update the line with more and more points. See e.g. [this question](https://stackoverflow.com/questions/10944621/dynamically-updating-plot-in-matplotlib) on how that can be done. – ImportanceOfBeingErnest Feb 06 '19 at 16:07
  • I tried the example given in the question you linked but it still won't display anything, I probably miss a plot and or clear at some point but struggling knowing where and what to put in the plot – damien Feb 06 '19 at 17:20
  • Note how answers in that linked thread use `ax.relim(); ax.autoscale_view()` to scale the plot limits. – ImportanceOfBeingErnest Feb 06 '19 at 17:56
  • See my solution below, but also try calling `f.canvas.draw()` (aka `fig.canvas.draw()`) after you call `plt.pause(0.01)` – clockelliptic Jul 05 '19 at 16:32
  • 1
    A side remark - since plotting could take an unpredictable amount of time, it might be a good idea to use the `multiprocessing` module and read the data in a separate process. You can then pipe collected data to the plotting process. – Hristo Iliev Jul 05 '19 at 16:36

1 Answers1

1

As someone who worked in an optics lab and struggled to get Matplotlib to perform real-time plotting, I feel your pain and I strongly suggest choosing something other than Matplotlib for this purpose (such as pyqtgraph).

That said, I've gotten Matplotlib to perform some real-time plotting from sensor data. I've found it to be buggy. Here are some thoughts as well as a solution that uses matplotlib:

Use dictionaries where possible. Why? Because accessing dictionaries is fast, and I find that a dictionary key is easier to use than a list index for these purposes.

Use lists instead of NumPy arrays. Why? Because every time you resize or append a NumPy array it must be completely rewritten as a new object in memory. This is very costly. Lists can be resized and appended for negligible cost.

The code below uses random data to simulate incoming sensor data and to make troubleshooting easier.

1. Imports

from time import sleep
import matplotlib.pyplot as plt
import numpy as np
#import serial

2. Setup your matplotlib objects and data containers

# specify how many points to show on the x-axis
xwidth = 10

# use real-time plotting
plt.ion()

# setup each of the subplots
ax = []
fig, ax[0:7] = plt.subplots(7, 1, sharex=False, sharey=False)

# set up each of the lines/curves to be plotted on their respective subplots
lines = {index: Axes_object.plot([],[])[0] for index, Axes_object in enumerate(ax)}

# cache background of each plot for fast re-drawing, AKA Blit
ax_bgs = {index: fig.canvas.copy_from_bbox(Axes_object.bbox) 
          for index, Axes_object in enumerate(ax)}

# initial drawing of the canvas
fig.canvas.draw()

# setup variable to contain incoming serial port data
y_data = {index:[0] for index in range(len(ax))}
x_data = [-1]

3. Write functions for update the plot and for updating your data containers

def update_data(new_byte, ):
    x_data.append(x_data[-1] + 1)
    for i, val in enumerate(new_byte): 
        y_data[i].append(val)

def update_graph():
    for i in y_data.keys():
        # update each line object
        lines[i].set_data(x_data, y_data[i])

        # try to set new axes limits
        try:
            ax[i].set_xlim([x_data[-1] - xwidth, x_data[-1]])
            if max(y_data[i][-xwidth:]) > ax[i].get_ylim()[1]:
                new_min = min(y_data[i][-xwidth:])
                new_max = max(y_data[i][-xwidth:])
                ax[i].set_ylim([new_min-abs(new_min)*0.2, new_max+abs(new_max)*0.2])
        except:
            continue

    fig.canvas.draw()

4. Finally, run the loop

#ser = serial.Serial('COM3', 115200) # Establish the connection on a specific port
#byte=ser.readline() #first line not to be plotted


while x_data[-1] < 30:
    # ser.write(b'9') # send a command to the arduino
    # byte=ser.read(7) #read 7 bytes back
    byte = np.random.rand(7)

    update_data(byte)
    update_graph()

    sleep(.1) # Delay for an arbitrary amount of time

I hope that helps.

clockelliptic
  • 455
  • 4
  • 14