我在使用 matplotlib 在 python 中以某种方式绘图时遇到了麻烦。 我正在制作一个接口,从芯片接收数据,然后将数据以 csv 格式保存,每天一个 csv(简历名称的格式为 YYYY-MM-DD),列是时间戳(HH -MM-SS)、DO、pH、温度,最后三个是浮点数。
目前,我有一个 4H 图表选项卡,其中绘制了三个图表(每个类别一个),另一个选项卡用于显示每个图表每天的平均值。我想更改最后一张图表,而不是平均每天一个 csv 的值,我想每天每 2 小时显示一个值,对应于每天/csv 12 个值,除非我真的不知道如何为此,我进行了很多搜索,但找不到类似的东西,有人知道该怎么做吗?另外,准确地说,我不想对这些值进行平均,它必须是每 2 小时保存一次的值。
这是我的代码,我截断了 Graph4H 类部分,因为它没有用,它更多地与 Graph24H 类有关:
from settings import *
from matplotlib.figure import Figure
from matplotlib.widgets import RectangleSelector
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from settings_menu import Widget
class Graph(ctk.CTkFrame):
def __init__(self, master: Misc, type: Literal["4H", "24H"], title: str, ylabel: str, color: str, graphs: list[FigureCanvasTkAgg], data: dict[str, list[float]]):
super().__init__(master=master, corner_radius=0)
self.app = master.master.master
self._type = type # Type de graphique 4H ou 24H
self.title = title # Titre du graphique
self.ylabel = ylabel # Nom de l'axe des ordonnées
self.data = data
self.color = color
# Extraction de la catégorie du graphique à partir du titre pour pouvoir mettre les bonnes données dans les bons graphs
self.category: Literal["DO", "pH", "Temperature"] = self.title.split("Graph ")[1]
# Variables de contrôle pour le zoom des données affichées
self.zoom_value: ctk.IntVar = self.master.master.zoom_value
self.delta: ctk.IntVar = self.master.master.delta # For the values
self.graphs: list[FigureCanvasTkAgg] = graphs # Liste des graphiques existants
# self.ready = ctk.BooleanVar(value=False)
def initialize_data(self):
if self.data is None:
self.app.log_error(ValueError(f"Graph {self.category} data is None")) # Affiche un message d'erreur indiquant que les données du graphique sont nulles
return
self.timestamps = self.data["time"]
self.values = self.data["values"]
# Obtention des limites de la catégorie (DO, pH, Température)
self.category_value = get_category_boundaries(self.app, self.category)
# Obtention des limites supérieure et inférieure (seuils) de la catégorie du graphique
self.upper_boundary, self.lower_boundary = self.category_value
self.create_graph()
# Fonction appelée lors du zoom sur le graphique
def on_zoom(self, event):
x_min, x_max, y_min, y_max = event.artist.xy
self.ax.set_xlim(x_min, x_max)
self.ax.set_ylim(y_min, y_max)
self.fig.canvas.draw() # Update the canvas after zoom
def create_graph(self):
value_amount = 14400//self.delta.get() if self._type == "4H" else len(self.timestamps)
x_axis = [i // (self.delta.get() / 60) for i in range(value_amount)] if self._type == "4H" else self.timestamps # Convert to hours
data_skip = {"DO":2, "pH":3, "Temperature":5}[self.category]
# Création du graphique
ax: plt.Axes
fig: Figure
fig, ax = plt.subplots(dpi=self.zoom_value.get()) #dpi = zoom amount
if self._type == "4H":
# Set major ticks every hour
def hour_formatter(x, pos):
"""Custom formatter to display hours with "H" suffix"""
hours = int(x / (self.delta.get())/3)
return f"{hours}H"
ax.xaxis.set_major_formatter(hour_formatter)
ax.set_ylabel(ylabel=self.ylabel, fontsize = 25)
ax.tick_params(axis='both', which='major', labelsize=25) # Set tick font size
ax.grid(True)
plt.plot(x_axis,self.values, color=self.color, linewidth=0.6) # Tracé du graphique
# Configuration des limites et des seuils sur l'axe des ordonnées
ax.set_yticks([i for i in range(int(self.lower_boundary)-2, int(self.upper_boundary)+3, data_skip)])
ax.set_xlim(0, value_amount) # Set x-axis limits based on number of hours
ax.set_ylim(self.lower_boundary-2, self.upper_boundary+2)
ax.plot([self.lower_boundary for _ in range(value_amount)], "--", color=PURE_RED)
ax.plot([self.upper_boundary for _ in range(value_amount)], "--", color=PURE_RED)
# Création du widget Tkinter pour afficher le graphique
try:
graph = FigureCanvasTkAgg(fig, master=self)
graph.get_tk_widget().place(relx=0, rely=0, relwidth=1, relheight=1)
except Exception as e:
return
# Ajout du graphique dans la liste des graphiques existants
self.graphs.append(graph)
if len(self.graphs) > 3:
plt.close(self.graphs[0].figure)
del self.graphs[0]
graph.draw()# Mise à jour de l'affichage du graphique
# self.ready.set(True)
class GraphMenu(Menu):
def __init__(self, master, title: str, func: Callable):
super().__init__(master=master, title=title)
self.get_data = func # Fonction pour obtenir les données du graphique
self.graphs: list[FigureCanvasTkAgg] = [] # Liste des graphiques existants
# Variables de contrôle pour le zoom des données affichées
self.delta = ctk.IntVar(value=30)
self.zoom_value = ctk.IntVar(value=70)
self.create_widgets()
def create_widgets(self) -> None:
self.loading_frame = ctk.CTkFrame(master=self, corner_radius=0)
self.loading_label = ctk.CTkLabel(master=self.loading_frame, text="Chargement des données...")
self.loading_label.place(relx=0.5, rely=0.5, anchor="center")
self.graph_frame = ctk.CTkFrame(master=self, corner_radius=0)
# Création des graphiques en fonction du type spécifié 4H ou 24H
def create_graphs(self, _type: Literal["4H", "24H"]) -> None:
self._type = _type
self.graph_frame.place_forget()
data = self.get_data()
# Création des graphiques pour DO, pH et Température
self.graph_do = Graph(
master=self.graph_frame,
type=self._type,
title="Graph DO",
ylabel="DO \n (mg/L)",
color=PURE_BLUE,
graphs=self.graphs,
data={"time": data["time"], "values": data["values"]["DO"]}
)
self.graph_ph = Graph(
master=self.graph_frame,
type=self._type,
title="Graph pH",
ylabel="pH",
color=PURE_GREEN,
graphs=self.graphs,
data={"time": data["time"], "values": data["values"]["pH"]}
)
self.graph_temp = Graph(
master=self.graph_frame,
type=self._type,
title="Graph Temperature",
ylabel="Température \n (°C)",
color=DARK_RED,
graphs=self.graphs,
data={"time": data["time"], "values": data["values"]["Temperature"]}
)
# Placement des sous-frames dans le frame principal
self.graph_do.place(relx=0.05, rely=0, relwidth=0.9, relheight=0.3)
self.graph_ph.place(relx=0.05, rely=0.35, relwidth=0.9, relheight=0.3)
self.graph_temp.place(relx=0.05, rely=0.7, relwidth=0.9, relheight=0.3)
self.loading_frame.place(relx=0, rely=0, relwidth=1, relheight=1)
self.update()
self.graph_do.initialize_data()
self.graph_ph.initialize_data()
self.graph_temp.initialize_data()
_rely = 0.1 if _type == "4H" else 0.05
_relheight = 0.85 if _type == "4H" else 0.9
self.after(1000, lambda: self.graph_frame.place(relx=0, rely=_rely, relwidth=1, relheight=_relheight))
self.update()
class Graph4H(GraphMenu):…
class Graph24H(GraphMenu):
def __init__(self, master, title: str):
super().__init__(master=master, title=title, func=self.get_data_for_last_24h)
self.extra = lambda: self.create_graphs("24H")
def get_data_for_last_24h(self) -> dict[str, dict[Literal["DO", "pH", "Temperature"], list[int]]]:
# Fonction pour obtenir les données pour la catégorie spécifiée (DO, pH, Température) sur les 4 dernières heures
data = {"time": [], "values": {"DO": [], "pH": [], "Temperature": []}}
loaded_data: dict[str, dict[str, list[float]]] = self.master.load_data(get_data_filenames())
for day, day_values in loaded_data.items():
data["time"].append(day)
for category in ["DO", "pH", "Temperature"]:
data["values"][category].append(day_values[category])
return data
我也尝试用这个替换 def get_data_for_last_24H 但它没有产生任何结果:
def get_data_for_last_24h(self) -> dict[str, dict[Literal["DO", "pH", "Temperature"], list[int]]]:
# Function to get data for the last 24 hours with points every 2 hours
data = {"time": [], "values": {"DO": [], "pH": [], "Temperature": []}}
loaded_data: dict[str, dict[str, list[float]]] = self.master.load_data(get_data_filenames())
for day, day_values in loaded_data.items():
if "time" in day_values:
for timestamp in day_values["time"]:
# Extract hour from timestamp (assuming format HH:mm:ss)
hour = int(timestamp.split(":")[0])
# Check if hour is divisible by 2 (represents data point every 2 hours)
if hour % 2 == 0:
data["time"].append(timestamp)
for category in ["DO", "pH", "Temperature"]:
data["values"][category].append(day_values[category])
else:
# Handle the case where there are no timestamps for this day
print(f"Warning: No timestamps found for day {day}")
我尝试展示如何使用下面的一些每小时测试数据来格式化这样的图。
每隔一小时采样一次的函数是
.asfreq('2h')
。它只是选择行;它不平均。
大部分日期格式是由
matplotlib.dates.ConciseDateFormatter()
完成的,它具有良好的默认值。为了更紧密地匹配您的格式,我们又花了几行告诉格式化程序将小时渲染为“6H”而不是默认的“06:00”。
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
#Data for testing
dates = pd.date_range('1 May 2024', '4 May 2024', freq='h')
df = pd.DataFrame(
{
'DO': np.random.uniform(5, 16, dates.size),
'pH': np.random.uniform(7, 7.5, size=dates.size),
'Temperature': np.random.uniform(28, 34, size=dates.size)
},
index=dates
)
df
#
# Resample down to a 2H frequency - this will keep every 2h data point
#
df_resampled = df.asfreq('2h')
df_resampled
#
# Plot
#
from matplotlib import dates as mdates
#Plot the data and guide lines
plot_col = 'DO'
f, ax = plt.subplots(figsize=(11, 2.5), layout='tight')
ax.plot(
df_resampled.index, df_resampled[plot_col],
marker='s', markersize=5, linewidth=1.5, color='dodgerblue', label=plot_col
)
[ax.axhline(y, color='tab:red', linestyle=':', linewidth=1) for y in [7, 13]]
#Set the plot date range
ax.set_xlim(mdates.datestr2num(['1 May 2024', '3 May 2024']))
#Add grids and labels
ax.grid(color='gray', linestyle=':', linewidth=0.5)
ax.set(xlabel='', ylabel=plot_col)
#Configure the x axis ticks and formats
ax.xaxis.set_major_locator(mdates.HourLocator(interval=2))
#Default would format the time as "06:00". OP requires "6H".
formatter = mdates.ConciseDateFormatter(
locator=ax.xaxis.get_major_locator(),
formats=['%Y', '%b', '%d', '%-HH', '%-HH', '%S.%f'], #changed the time to %-HH
zero_formats=['', '%Y', '%b', '%b-%d', '%H:%M', '%H:%M'] #added ""-%d" to %b
)
ax.xaxis.set_major_formatter(formatter)
#Final formatting
ax.spines[['right', 'top']].set_visible(False)
ax.tick_params(axis='x', rotation=25)