2022-08-08 22:51:36 +00:00
|
|
|
from datetime import datetime
|
2022-08-18 09:56:58 +00:00
|
|
|
from matplotlib.animation import FuncAnimation
|
|
|
|
import os, json, traceback, wx
|
2022-08-08 22:51:36 +00:00
|
|
|
import numpy as np
|
|
|
|
import matplotlib
|
|
|
|
import matplotlib.pyplot as plt
|
2022-12-04 22:40:46 +00:00
|
|
|
import decimal
|
2022-12-15 23:22:33 +00:00
|
|
|
from itertools import islice
|
2022-08-18 09:56:58 +00:00
|
|
|
matplotlib.use("WXAgg") # for JetBrains IDE to force use wxPython as backend UI for plotting
|
|
|
|
|
2022-08-08 22:51:36 +00:00
|
|
|
class SerialPlotter:
|
2022-08-18 09:56:58 +00:00
|
|
|
def __init__(self, parent: wx.Frame = None) -> None:
|
|
|
|
"""
|
|
|
|
Dynamically plot a graph with moving window, if a finite window size set by the user. It reads the .csv file
|
|
|
|
read_arduino generated, use the last line as the latest data and update the graph accordingly. It will plot
|
|
|
|
multiple lines on the graph if there are more than one values on the last row of the .csv file
|
|
|
|
:param parent: optional, parent frame
|
|
|
|
"""
|
|
|
|
self.parent = parent
|
2022-08-08 22:51:36 +00:00
|
|
|
self.settings = json.load(open('settings.json', 'r'))
|
|
|
|
self.sensors = len(self.settings['sensor_ports'])
|
2022-08-18 09:56:58 +00:00
|
|
|
self.windowsize = self.settings['winSize']
|
2023-03-07 05:15:07 +00:00
|
|
|
self.fontSize = self.settings['fontSize']
|
2022-08-08 22:51:36 +00:00
|
|
|
self.delay = self.settings["delay"] / 1000
|
2022-08-18 09:56:58 +00:00
|
|
|
|
2022-08-08 22:51:36 +00:00
|
|
|
self.colors = ['blue', 'orange', 'green', 'yellow']
|
|
|
|
|
2022-08-18 09:56:58 +00:00
|
|
|
# TODO: make the figure size an UI option and pass into the settings.json
|
2023-05-01 00:57:42 +00:00
|
|
|
#self.fig, self.axs = plt.subplots(figsize=(9, 6))
|
|
|
|
# self.fig_1 = plt.figure(1)
|
|
|
|
# self.axs_1 = plt.axes()
|
|
|
|
|
|
|
|
# self.fig_2 = plt.figure(2)
|
|
|
|
# self.axs_2 = plt.axes()
|
|
|
|
if (self.sensors == 1) :
|
|
|
|
self.fig_1, self.axs_1 = plt.subplots()
|
|
|
|
|
|
|
|
self.fig_1.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
elif (self.sensors == 2) :
|
|
|
|
self.fig_1, self.axs_1 = plt.subplots()
|
|
|
|
self.fig_2, self.axs_2 = plt.subplots()
|
|
|
|
|
|
|
|
self.fig_1.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
self.fig_2.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
elif (self.sensors == 3) :
|
|
|
|
self.fig_1, self.axs_1 = plt.subplots()
|
|
|
|
self.fig_2, self.axs_2 = plt.subplots()
|
|
|
|
self.fig_3, self.axs_3 = plt.subplots()
|
|
|
|
|
|
|
|
self.fig_1.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
self.fig_2.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
self.fig_3.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
elif (self.sensors == 4) :
|
|
|
|
self.fig_1, self.axs_1 = plt.subplots()
|
|
|
|
self.fig_2, self.axs_2 = plt.subplots()
|
|
|
|
self.fig_3, self.axs_3 = plt.subplots()
|
|
|
|
self.fig_4, self.axs_4 = plt.subplots()
|
|
|
|
|
|
|
|
self.fig_1.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
self.fig_2.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
self.fig_3.canvas.mpl_connect('close_event', self.event_close)
|
|
|
|
self.fig_4.canvas.mpl_connect('close_event', self.event_close)
|
2022-08-08 22:51:36 +00:00
|
|
|
|
|
|
|
self.timeStamps = {}
|
|
|
|
self.sensorsData = {}
|
2022-08-18 09:56:58 +00:00
|
|
|
self.timeElapsed = 0 # time have passed since the graph started, in seconds
|
2022-08-08 22:51:36 +00:00
|
|
|
|
|
|
|
for i in range(self.sensors):
|
2022-08-18 09:56:58 +00:00
|
|
|
self.timeStamps[i] = ['']
|
|
|
|
self.sensorsData[i] = [0]
|
2022-08-08 22:51:36 +00:00
|
|
|
|
2022-08-18 09:56:58 +00:00
|
|
|
def event_close(self, event) -> None:
|
|
|
|
"""
|
2023-05-01 00:57:42 +00:00
|
|
|
Actions need to be executed when the graph has closed. Start a new .csv file to get read for new graph and
|
2022-08-18 09:56:58 +00:00
|
|
|
bring back the UI, if exist
|
|
|
|
"""
|
2022-08-08 22:51:36 +00:00
|
|
|
file = self.settings["file_name"]
|
|
|
|
wx.MessageBox(f"File has saved as {os.path.split(file)[1]} under {os.path.split(file)[0]} directory!\n")
|
2022-08-18 09:56:58 +00:00
|
|
|
if self.parent:
|
|
|
|
self.parent.Show()
|
2022-08-08 22:51:36 +00:00
|
|
|
|
2023-05-01 00:57:42 +00:00
|
|
|
def animation_1(self, t) -> None:
|
2022-08-18 09:56:58 +00:00
|
|
|
"""
|
2023-04-14 11:20:34 +00:00
|
|
|
render a frame of the animated graph.
|
2022-08-18 09:56:58 +00:00
|
|
|
"""
|
2023-04-14 11:20:34 +00:00
|
|
|
# Some of them copied from the old code, so even Eric may not 100% know what they do. ChatGPT may give a better explaination
|
2022-08-08 22:51:36 +00:00
|
|
|
try:
|
|
|
|
plt.cla() # clear previous frame
|
2022-08-18 09:56:58 +00:00
|
|
|
# read the last line from the .csv file, the data start from the second column so omit index #0
|
2022-08-08 22:51:36 +00:00
|
|
|
file = open(self.settings["file_name"], "r")
|
2022-12-15 23:22:33 +00:00
|
|
|
ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in islice(file, 1, None)])#read from the second row of the file ignoring headers
|
2022-08-08 22:51:36 +00:00
|
|
|
if len(ndata) > 0:
|
|
|
|
row = ndata[-1]
|
|
|
|
i = 0
|
2023-05-01 00:57:42 +00:00
|
|
|
if 0 < self.windowsize < len(self.timeStamps[i]):
|
|
|
|
self.timeStamps[i].pop(0)
|
|
|
|
self.sensorsData[i].pop(0)
|
2022-08-08 22:51:36 +00:00
|
|
|
|
2022-08-18 09:56:58 +00:00
|
|
|
# self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # version 1
|
|
|
|
|
|
|
|
# version 2, if we decide to go with this one change the list type to list[int] from list[str]
|
2023-05-01 00:57:42 +00:00
|
|
|
self.timeStamps[i].append(str(self.timeElapsed))
|
2022-08-18 09:56:58 +00:00
|
|
|
|
2023-05-01 00:57:42 +00:00
|
|
|
self.sensorsData[i].append(row[i])
|
2022-08-08 22:51:36 +00:00
|
|
|
# plot a line
|
2022-12-04 22:40:46 +00:00
|
|
|
# round the number to scientific notation
|
2023-05-01 00:57:42 +00:00
|
|
|
self.axs_1.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i],
|
2022-12-04 22:40:46 +00:00
|
|
|
label=f'sensor {i + 1}, latest: {np.format_float_scientific(self.sensorsData[i][-1], precision = 2)} $\Omega$')
|
2023-05-01 00:57:42 +00:00
|
|
|
# TODO: make the font size an UI option from the settings.json
|
|
|
|
# Make the font size of values placed on x-axis and y-aixs equal to 16 (large enough)
|
|
|
|
#plt.xticks(fontsize = max(self.fontSize, 10))
|
|
|
|
#plt.yticks(fontsize = max(self.fontSize, 10))
|
|
|
|
self.axs_1.tick_params(axis='both', which='both', labelsize=max(self.fontSize, 10))
|
2022-12-04 22:40:46 +00:00
|
|
|
|
2023-05-01 00:57:42 +00:00
|
|
|
# make the label of two axes large enough
|
|
|
|
self.axs_1.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10))
|
|
|
|
self.axs_1.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10))
|
|
|
|
#i += 1
|
|
|
|
self.timeElapsed += 1 # increment time
|
|
|
|
|
|
|
|
# Acknowledgement: https://stackoverflow.com/a/13589144 (can you imagine how long it took me to find this before ChatGPT?)
|
|
|
|
#handles, labels = self.axs.get_legend_handles_labels()
|
|
|
|
#by_label = dict(zip(labels, handles))
|
|
|
|
#self.axs.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough
|
|
|
|
|
|
|
|
# handles, labels = self.axs_1.get_legend_handles_labels()
|
|
|
|
# by_label = dict(zip(labels, handles))
|
|
|
|
# self.axs_1.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10))
|
|
|
|
|
|
|
|
handles, labels = plt.gca().get_legend_handles_labels()
|
|
|
|
by_label = dict(zip(labels, handles))
|
|
|
|
plt.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10))
|
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
def animation_2(self, t: int) -> None:
|
|
|
|
"""
|
|
|
|
render a frame of the animated graph.
|
|
|
|
"""
|
|
|
|
# Some of them copied from the old code, so even Eric may not 100% know what they do. ChatGPT may give a better explaination
|
|
|
|
try:
|
|
|
|
plt.cla() # clear previous frame
|
|
|
|
# read the last line from the .csv file, the data start from the second column so omit index #0
|
|
|
|
file = open(self.settings["file_name"], "r")
|
|
|
|
ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in islice(file, 1, None)])#read from the second row of the file ignoring headers
|
|
|
|
if len(ndata) > 0:
|
|
|
|
row = ndata[-1]
|
|
|
|
i = 1
|
|
|
|
if 0 < self.windowsize < len(self.timeStamps[i]):
|
|
|
|
self.timeStamps[i].pop(0)
|
|
|
|
self.sensorsData[i].pop(0)
|
|
|
|
|
|
|
|
# self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # version 1
|
|
|
|
|
|
|
|
# version 2, if we decide to go with this one change the list type to list[int] from list[str]
|
|
|
|
self.timeStamps[i].append(str(self.timeElapsed))
|
|
|
|
|
|
|
|
self.sensorsData[i].append(row[i])
|
|
|
|
# plot a line
|
|
|
|
# round the number to scientific notation
|
|
|
|
self.axs_2.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i],
|
|
|
|
label=f'sensor {i + 1}, latest: {np.format_float_scientific(self.sensorsData[i][-1], precision = 2)} $\Omega$')
|
2023-03-06 06:14:29 +00:00
|
|
|
# TODO: make the font size an UI option from the settings.json
|
2023-03-06 00:36:10 +00:00
|
|
|
# Make the font size of values placed on x-axis and y-aixs equal to 16 (large enough)
|
2023-05-01 00:57:42 +00:00
|
|
|
#self.axs_2.set_xticks(fontsize = max(self.fontSize, 10))
|
|
|
|
#self.axs_2.set_yticks(fontsize = max(self.fontSize, 10))
|
|
|
|
self.axs_2.tick_params(axis='both', which='both', labelsize=max(self.fontSize, 10))
|
2023-03-06 00:36:10 +00:00
|
|
|
|
|
|
|
# make the label of two axes large enough
|
2023-05-01 00:57:42 +00:00
|
|
|
self.axs_2.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10))
|
|
|
|
self.axs_2.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10))
|
|
|
|
#i += 1
|
2022-08-18 09:56:58 +00:00
|
|
|
self.timeElapsed += 1 # increment time
|
|
|
|
|
2023-04-14 11:20:34 +00:00
|
|
|
# Acknowledgement: https://stackoverflow.com/a/13589144 (can you imagine how long it took me to find this before ChatGPT?)
|
2023-05-01 00:57:42 +00:00
|
|
|
#handles, labels = self.axs.get_legend_handles_labels()
|
|
|
|
#by_label = dict(zip(labels, handles))
|
|
|
|
#self.axs.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough
|
|
|
|
|
|
|
|
# handles, labels = self.axs_2.get_legend_handles_labels()
|
|
|
|
# by_label = dict(zip(labels, handles))
|
|
|
|
# self.axs_2.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10))
|
|
|
|
handles, labels = plt.gca().get_legend_handles_labels()
|
2022-08-08 22:51:36 +00:00
|
|
|
by_label = dict(zip(labels, handles))
|
2023-05-01 00:57:42 +00:00
|
|
|
plt.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10))
|
2022-08-08 22:51:36 +00:00
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
2023-05-01 00:57:42 +00:00
|
|
|
|
|
|
|
def animation_3(self, t: int) -> None:
|
|
|
|
"""
|
|
|
|
render a frame of the animated graph.
|
|
|
|
"""
|
|
|
|
# Some of them copied from the old code, so even Eric may not 100% know what they do. ChatGPT may give a better explaination
|
|
|
|
try:
|
|
|
|
plt.cla() # clear previous frame
|
|
|
|
# read the last line from the .csv file, the data start from the second column so omit index #0
|
|
|
|
file = open(self.settings["file_name"], "r")
|
|
|
|
ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in islice(file, 1, None)])#read from the second row of the file ignoring headers
|
|
|
|
if len(ndata) > 0:
|
|
|
|
row = ndata[-1]
|
|
|
|
i = 2
|
|
|
|
if 0 < self.windowsize < len(self.timeStamps[i]):
|
|
|
|
self.timeStamps[i].pop(0)
|
|
|
|
self.sensorsData[i].pop(0)
|
2022-08-08 22:51:36 +00:00
|
|
|
|
2023-05-01 00:57:42 +00:00
|
|
|
# self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # version 1
|
|
|
|
|
|
|
|
# version 2, if we decide to go with this one change the list type to list[int] from list[str]
|
|
|
|
self.timeStamps[i].append(str(self.timeElapsed))
|
|
|
|
|
|
|
|
self.sensorsData[i].append(row[i])
|
|
|
|
# plot a line
|
|
|
|
# round the number to scientific notation
|
|
|
|
self.axs_3.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i],
|
|
|
|
label=f'sensor {i + 1}, latest: {np.format_float_scientific(self.sensorsData[i][-1], precision = 2)} $\Omega$')
|
|
|
|
# TODO: make the font size an UI option from the settings.json
|
|
|
|
# Make the font size of values placed on x-axis and y-aixs equal to 16 (large enough)
|
|
|
|
#self.axs_2.set_xticks(fontsize = max(self.fontSize, 10))
|
|
|
|
#self.axs_2.set_yticks(fontsize = max(self.fontSize, 10))
|
|
|
|
self.axs_3.tick_params(axis='both', which='both', labelsize=max(self.fontSize, 10))
|
|
|
|
|
|
|
|
# make the label of two axes large enough
|
|
|
|
self.axs_3.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10))
|
|
|
|
self.axs_3.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10))
|
|
|
|
#i += 1
|
|
|
|
self.timeElapsed += 1 # increment time
|
|
|
|
|
|
|
|
# Acknowledgement: https://stackoverflow.com/a/13589144 (can you imagine how long it took me to find this before ChatGPT?)
|
|
|
|
#handles, labels = self.axs.get_legend_handles_labels()
|
|
|
|
#by_label = dict(zip(labels, handles))
|
|
|
|
#self.axs.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough
|
|
|
|
|
|
|
|
# handles, labels = self.axs_2.get_legend_handles_labels()
|
|
|
|
# by_label = dict(zip(labels, handles))
|
|
|
|
# self.axs_2.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10))
|
|
|
|
handles, labels = plt.gca().get_legend_handles_labels()
|
|
|
|
by_label = dict(zip(labels, handles))
|
|
|
|
plt.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10))
|
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
def animation_4(self, t: int) -> None:
|
|
|
|
"""
|
|
|
|
render a frame of the animated graph.
|
|
|
|
"""
|
|
|
|
# Some of them copied from the old code, so even Eric may not 100% know what they do. ChatGPT may give a better explaination
|
|
|
|
try:
|
|
|
|
plt.cla() # clear previous frame
|
|
|
|
# read the last line from the .csv file, the data start from the second column so omit index #0
|
|
|
|
file = open(self.settings["file_name"], "r")
|
|
|
|
ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in islice(file, 1, None)])#read from the second row of the file ignoring headers
|
|
|
|
if len(ndata) > 0:
|
|
|
|
row = ndata[-1]
|
|
|
|
i = 3
|
|
|
|
if 0 < self.windowsize < len(self.timeStamps[i]):
|
|
|
|
self.timeStamps[i].pop(0)
|
|
|
|
self.sensorsData[i].pop(0)
|
|
|
|
|
|
|
|
# self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # version 1
|
|
|
|
|
|
|
|
# version 2, if we decide to go with this one change the list type to list[int] from list[str]
|
|
|
|
self.timeStamps[i].append(str(self.timeElapsed))
|
|
|
|
|
|
|
|
self.sensorsData[i].append(row[i])
|
|
|
|
# plot a line
|
|
|
|
# round the number to scientific notation
|
|
|
|
self.axs_4.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i],
|
|
|
|
label=f'sensor {i + 1}, latest: {np.format_float_scientific(self.sensorsData[i][-1], precision = 2)} $\Omega$')
|
|
|
|
# TODO: make the font size an UI option from the settings.json
|
|
|
|
# Make the font size of values placed on x-axis and y-aixs equal to 16 (large enough)
|
|
|
|
#self.axs_2.set_xticks(fontsize = max(self.fontSize, 10))
|
|
|
|
#self.axs_2.set_yticks(fontsize = max(self.fontSize, 10))
|
|
|
|
self.axs_4.tick_params(axis='both', which='both', labelsize=max(self.fontSize, 10))
|
|
|
|
|
|
|
|
# make the label of two axes large enough
|
|
|
|
self.axs_4.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10))
|
|
|
|
self.axs_4.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10))
|
|
|
|
#i += 1
|
|
|
|
self.timeElapsed += 1 # increment time
|
|
|
|
|
|
|
|
# Acknowledgement: https://stackoverflow.com/a/13589144 (can you imagine how long it took me to find this before ChatGPT?)
|
|
|
|
#handles, labels = self.axs.get_legend_handles_labels()
|
|
|
|
#by_label = dict(zip(labels, handles))
|
|
|
|
#self.axs.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough
|
|
|
|
|
|
|
|
# handles, labels = self.axs_2.get_legend_handles_labels()
|
|
|
|
# by_label = dict(zip(labels, handles))
|
|
|
|
# self.axs_2.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10))
|
|
|
|
handles, labels = plt.gca().get_legend_handles_labels()
|
|
|
|
by_label = dict(zip(labels, handles))
|
|
|
|
plt.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10))
|
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
def plotting_1(self) -> FuncAnimation:
|
|
|
|
""" animate the dynamic plot """
|
|
|
|
ani_1 = FuncAnimation(self.fig_1, self.animation_1, blit=False, interval=self.delay * 1000, save_count=60)
|
|
|
|
return ani_1
|
|
|
|
|
|
|
|
def plotting_2(self) -> FuncAnimation:
|
|
|
|
""" animate the dynamic plot """
|
|
|
|
ani_2 = FuncAnimation(self.fig_2, self.animation_2, blit=False, interval=self.delay * 1000, save_count=60)
|
|
|
|
return ani_2
|
|
|
|
|
|
|
|
def plotting_3(self) -> FuncAnimation:
|
|
|
|
""" animate the dynamic plot """
|
|
|
|
ani_3 = FuncAnimation(self.fig_3, self.animation_3, blit=False, interval=self.delay * 1000, save_count=60)
|
|
|
|
return ani_3
|
|
|
|
|
|
|
|
def plotting_4(self) -> FuncAnimation:
|
2022-08-18 09:56:58 +00:00
|
|
|
""" animate the dynamic plot """
|
2023-05-01 00:57:42 +00:00
|
|
|
ani_4 = FuncAnimation(self.fig_3, self.animation_3, blit=False, interval=self.delay * 1000, save_count=60)
|
|
|
|
return ani_4
|