The code basically works like this:
- Initial setup
- Update data
- Update plots
- Goto 2
Let's go through each of these.
Initial Setup
The initial setup creates the lists (actually deques) the data will be stored in, the TFT setup, and the plot setup including the axis and the plot lines. This ends up being a fair amount of the code. It's all this:
# Setup X data storage x_time = [x * REFRESH_RATE for x in range(HIST_SIZE)] x_time.reverse() # Setup Y data storage y_data = [ [deque([None] * HIST_SIZE, maxlen=HIST_SIZE) for _ in plot['line_config']] for plot in PLOT_CONFIG ] # Setup display disp = ili9341.ILI9341(board.SPI(), baudrate = 24000000, cs = digitalio.DigitalInOut(board.D4), dc = digitalio.DigitalInOut(board.D5), rst = digitalio.DigitalInOut(board.D6)) # Setup plot figure plt.style.use('dark_background') fig, ax = plt.subplots(2, 1, figsize=(disp.width / 100, disp.height / 100)) # Setup plot axis ax[0].xaxis.set_ticklabels([]) for plot, a in enumerate(ax): # add grid to all plots a.grid(True, linestyle=':') # limit and invert x time axis a.set_xlim(min(x_time), max(x_time)) a.invert_xaxis() # custom settings if 'title' in PLOT_CONFIG[plot]: a.set_title(PLOT_CONFIG[plot]['title'], position=(0.5, 0.8)) if 'ylim' in PLOT_CONFIG[plot]: a.set_ylim(PLOT_CONFIG[plot]['ylim']) # Setup plot lines #pylint: disable=redefined-outer-name plot_lines = [] for plot, config in enumerate(PLOT_CONFIG): lines = [] for index, line_config in enumerate(config['line_config']): # create line line, = ax[plot].plot(x_time, y_data[plot][index]) # custom settings if 'color' in line_config: line.set_color(line_config['color']) if 'width' in line_config: line.set_linewidth(line_config['width']) if 'style' in line_config: line.set_linestyle(line_config['style']) # add line to list lines.append(line) plot_lines.append(lines)
Update Data
Updating the data is done in the function update_data()
. You would change this to be whatever you want (more info below). Wherever the new data comes from, you would then just add each new data point to the data stores. For the basic example, this is just random numbers and a sine curve:
def update_data(): ''' Do whatever to update your data here. General form is: y_data[plot][line].append(new_data_point) ''' # upper plot data for data in y_data[0]: data.append(random.random()) # lower plot data y_data[1][0].append(math.sin(0.5 * time.monotonic()))
Each line of the plot has a dedicated deque to store its data. Each plot stores these deques in a list. These lists are then stored in the variable y_data
. Therefore, to access the line
for a given plot
, you'd use the syntax y_data[plot][line]
. You'll generally want to use the append()
method to add each new data point. The result is something like:
y_data[plot][line].append(new_data_point)
You can also use Python iterator syntax if that works for your scenario. That's how the random data is appended in the example above.
Also remember that indexing is 0 based. So the top plot is 0 and the bottom plot is 1.
Update Plots
Updating the plots is where Matplotlib and Pillow are used. This is all done in the function update_plot()
. The basic idea is to update the ydata
for each line with the current data. The plot is then re-rendered. Pillow is then used to generate an image object that can be sent to the TFT display.
You generally won't have to deal with this function.
def update_plot(): # update lines with latest data for plot, lines in enumerate(plot_lines): for index, line in enumerate(lines): line.set_ydata(y_data[plot][index]) # autoscale if not specified if 'ylim' not in PLOT_CONFIG[plot].keys(): ax[plot].relim() ax[plot].autoscale_view() # draw the plots canvas = plt.get_current_fig_manager().canvas plt.tight_layout() canvas.draw() # transfer into PIL image and display image = Image.frombytes('RGB', canvas.get_width_height(), canvas.tostring_rgb()) disp.image(image)
print("looping") while True: update_data() update_plot() time.sleep(REFRESH_RATE)
Customization
We skipped over a bunch of additional "initial setup" at the top of the code. This is all the code between the two comment lines:
#==| User Config |======================
This is where you'll edit the code to customize it for your use. There are two general parts:
- Configure behavior and aesthetics via the
CONSTANTS
- Change the
update_data()
function for your use case
Behavior and Aesthetics
The two constants REFRESH_RATE
and HIST_SIZE
are pretty straight forward. They determine how often the data plot is updated and how much total data to store. The two together define the total time window for the plot as REFRESH_RATE * HIST_SIZE
.
The more interesting constant is the PLOT_CONFIG
structure. As the name implies, this is how you will configure the two plots. It's a bit of a nested mess of tuples and dictionaries. But hopefully it's laid out in a way to make it easy to edit.
At a minimum, you need the line_config
entry for each plot with one empty dictionary for each line of the plot. So the absolute minimum PLOT_CONFIG
would look like:
PLOT_CONFIG = ( #-------------------- # PLOT 1 (upper plot) #-------------------- { 'line_config' : ( { }, ) }, #-------------------- # PLOT 2 (lower plot) #-------------------- { 'line_config' : ( { }, ) } )
This would create a single plot line for both the upper and lower plots with default values used. A plot line is created for each dictionary entry in line_config
. So if you wanted to add a second plot line to the upper plot, the above would become:
PLOT_CONFIG = ( #-------------------- # PLOT 1 (upper plot) #-------------------- { 'line_config' : ( { }, { }, ) }, #-------------------- # PLOT 2 (lower plot) #-------------------- { 'line_config' : ( { }, ) } )
The only change is one additional line with an empty dictionary { }
.
The PLOT_CONFIG
in the basic test example tries to demonstrate your various customization options. For any of them, if they are not specified, default values are used. So, looking at the basic test example:
PLOT_CONFIG = ( #-------------------- # PLOT 1 (upper plot) #-------------------- { 'line_config' : ( {'color' : '#FF0000', 'width' : 2, 'style' : '--'}, { }, ) }, #-------------------- # PLOT 2 (lower plot) #-------------------- { 'title' : 'sin()', 'ylim' : (-1.5, 1.5), 'line_config' : ( {'color' : '#00FF00', 'width' : 4}, ) } )
The upper plot has no title or y axis limits set. The y axis will autoscale. Two plot lines are setup in line_config
. The first line has color
, width
, and style
specified. The second line has nothing specified so will use default settings.
The lower plot specifies a plot title
. This is simply some text that will be shown at the top of the plot. It also species specific y axis limits via ylim
, therefore the y axis will not autoscale. A single plot line is setup in line_config
with color
and width
specified. A default style
will be used.
In general, you should match up the number of lines configured in line_config
with the data that is updated in update_data()
.
Custom Data
The update_data()
function can be whatever you want. As mentioned above, the general syntax for a given line
of a given plot
is:
y_data[plot][line].append(new_data_point)
The system status examples that follow will provide more examples on how this function is used.
Text editor powered by tinymce.