Преглед изворни кода

'public_stable.py' ändern

maxscheinast пре 7 месеци
родитељ
комит
6d4840cd84
1 измењених фајлова са 330 додато и 330 уклоњено
  1. 330 330
      public.py

+ 330 - 330
public.py

@@ -1,331 +1,331 @@
-import customtkinter as ctk
-import time
-import math
-import random
-import csv
-import tkinter as tk
-import customtkinter as ctk
-import time
-
-## cc maximilian scheinast-peter 
-## last update 11.04.2024
-## dieses programm funktioniert ähnlich wie ein digitales Oszi zur Darstellung von Vitalkurven
-## Attribution-NonCommercial license
-
-
-
-def get_data():         #example heartfunction
-    timestamp = time.time()## dont change
-
-
-    ## logic for get one datapoint like a read_value function
-    t = (timestamp % 1) * 2 * math.pi
-    p_wave = 5 * math.sin(t)
-    qrs_complex = 40 * math.sin(1.5 * t) * math.exp(-0.25 * t**2)
-    t_wave = 10 * math.sin(2 * t) * math.exp(-0.5 * t**2)
-    value = 50 + p_wave + qrs_complex + t_wave
-    value = max(0, min(value, 100))
-    wait_for_next_millisecond()
-
-    return timestamp, value
-
-
-
-
-
-
-
-
-
-def wait_for_next_millisecond():
-    """Waits until the next full millisecond."""
-    current_time = time.time()
-    next_millisecond = (int(current_time * 1000) + 1) / 1000
-    time.sleep(next_millisecond - current_time)
-# Function to generate realistic ECG data
-
-
-class EKGApp:
-    def __init__(self, master):
-        self.master = master
-        master.title("EKG Visualization")
-
-        # Main Frame
-        main_frame = ctk.CTkFrame(master)
-        main_frame.pack(fill=ctk.BOTH, expand=True)
-
-        # Single Sweep Canvas
-        self.single_canvas = ctk.CTkCanvas(main_frame, width=600, height=300)
-        self.single_canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
-
-        # Multi Sweep Canvas
-        self.multi_canvas = ctk.CTkCanvas(main_frame, width=600, height=300)
-        self.multi_canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
-
-        # Input Frame
-        input_frame = ctk.CTkFrame(main_frame)
-        input_frame.pack(side=ctk.RIGHT, fill=ctk.Y)
-
-        # Data Structures
-        self.single_data = []
-        self.multi_data = []
-        self.logged_data = []
-
-        # Time Variables
-        self.start_time = time.time()
-        self.multi_sweep_duration = 30  # Initial Laufbanddauer
-
-        # Trigger Variables
-        self.trigger_level = 50
-        self.trigger_armed = False
-        self.trigger_paused = False
-        self.trigger_timestamp = None
-        self.last_trigger_timestamp = None
-        self.last_value = 0
-        self.trigger_count = 0
-
-        # Cooldown Variables
-        self.cooldown_time = 0
-        self.cooldown_active = False
-
-        # Logging Variables
-        self.logging_active = False
-
-        # Input Fields and Buttons (with consistent size)
-        self.create_input_fields(input_frame)
-
-        # Logging Frame
-        logging_frame = ctk.CTkFrame(master)
-        logging_frame.pack(fill=ctk.X)
-        self.create_logging_buttons(logging_frame)
-
-        # Update Data Periodically
-        self.update_data()
-
-        # Resize Handling
-        self.single_canvas.bind("<Configure>", self.on_resize)
-        self.multi_canvas.bind("<Configure>", self.on_resize)
-
-    def create_input_fields(self, frame):
-        # Consistent width for all input elements
-        input_width = 150
-
-        # Trigger Level
-        ctk.CTkLabel(frame, text="Trigger Level:").pack()
-        self.trigger_entry = ctk.CTkEntry(frame, width=input_width)
-        self.trigger_entry.insert(0, str(self.trigger_level))
-        self.trigger_entry.pack()
-
-        # Flank Selection
-        ctk.CTkLabel(frame, text="Flanke:").pack()
-        self.flank_var = ctk.StringVar(value="Steigende Flanke")
-        self.flank_combobox = ctk.CTkComboBox(frame, variable=self.flank_var, values=["Steigende Flanke", "Fallende Flanke"], width=input_width)
-        self.flank_combobox.pack()
-
-        # Cooldown
-        ctk.CTkLabel(frame, text="Cooldown (Sekunden):").pack()
-        self.cooldown_entry = ctk.CTkEntry(frame, width=input_width)
-        self.cooldown_entry.insert(0, "0")
-        self.cooldown_entry.pack()
-        self.cooldown_button = ctk.CTkButton(frame, text="Cooldown setzen", command=self.set_cooldown, width=input_width)
-        self.cooldown_button.pack()
-
-
-        # Ax Time (X-Achsen-Zeit)
-        ctk.CTkLabel(frame, text="Ax-Zeit (Sekunden):").pack()
-        self.ax_time_entry = ctk.CTkEntry(frame, width=input_width)
-        self.ax_time_entry.insert(0, str(self.multi_sweep_duration))  # Initial value
-        self.ax_time_entry.pack()
-        self.ax_time_button = ctk.CTkButton(frame, text="Ax-Zeit setzen", command=self.set_ax_time, width=input_width)
-        self.ax_time_button.pack()
-
-
-        # Trigger Buttons
-        self.trigger_button = ctk.CTkButton(frame, text="Trigger starten", command=self.toggle_trigger, width=input_width)
-        self.trigger_button.pack()
-        self.pause_button = ctk.CTkButton(frame, text="Trigger pausieren", command=self.pause_trigger, state=ctk.DISABLED, width=input_width)
-        self.pause_button.pack()
-
-        # Multi Sweep Duration (Laufbanddauer)
-        ctk.CTkLabel(frame, text="Laufbanddauer (Sekunden):").pack()
-        self.duration_entry = ctk.CTkEntry(frame, width=input_width)
-        self.duration_entry.insert(0, "30")
-        self.duration_entry.pack()
-        self.duration_button = ctk.CTkButton(frame, text="Dauer setzen", command=self.set_duration, width=input_width) 
-        self.duration_button.pack()
-
-
-    def set_ax_time(self):
-        try:
-            self.multi_sweep_duration = float(self.ax_time_entry.get())
-        except ValueError:
-            pass
-    def create_logging_buttons(self, frame):
-        # Consistent width for logging buttons
-        button_width = 120
-
-        self.start_logging_button = ctk.CTkButton(frame, text="Start Logging", command=self.start_logging, width=button_width)
-        self.start_logging_button.pack(side=ctk.LEFT)
-        self.end_logging_button = ctk.CTkButton(frame, text="End Logging", command=self.end_logging, state=ctk.DISABLED, width=button_width)
-        self.end_logging_button.pack(side=ctk.LEFT)
-        self.save_data_button = ctk.CTkButton(frame, text="Save Data", command=self.save_data, state=ctk.DISABLED, width=button_width)
-        self.save_data_button.pack(side=ctk.LEFT)
-
-
-    
-
-    def set_cooldown(self):
-        try:
-            self.cooldown_time = float(self.cooldown_entry.get())
-        except ValueError:
-            pass
-
-    def set_duration(self):
-        try:
-            self.multi_sweep_duration = float(self.duration_entry.get())
-        except ValueError:
-            pass
-
-    def toggle_trigger(self):
-        if self.trigger_armed:
-            self.trigger_armed = False
-            self.trigger_button.configure(text="Trigger starten")
-            self.pause_button.configure(state=ctk.DISABLED)
-            self.single_data = []
-            self.draw_single_canvas()
-        else:
-            try:
-                self.trigger_level = float(self.trigger_entry.get())
-            except ValueError:
-                pass
-            self.trigger_armed = True
-            self.trigger_button.configure(text="Trigger stoppen")
-            self.pause_button.configure(state=ctk.NORMAL)
-            self.trigger_count = 0
-
-    def pause_trigger(self):
-        self.trigger_paused = not self.trigger_paused
-        if self.trigger_paused:
-            self.pause_button.configure(text="Trigger fortsetzen")
-        else:
-            self.pause_button.configure(text="Trigger pausieren")
-
-    def start_logging(self):
-        self.logging_active = True
-        self.logged_data = []
-        self.start_logging_button.configure(state=ctk.DISABLED)
-        self.end_logging_button.configure(state=ctk.NORMAL)
-
-    def end_logging(self):
-        self.logging_active = False
-        self.start_logging_button.configure(state=ctk.NORMAL)
-        self.end_logging_button.configure(state=ctk.DISABLED)
-        self.save_data_button.configure(state=ctk.NORMAL)
-
-    def save_data(self):
-        # Popup for file name
-        print(123)
-        file_name = ctk.CTkInputDialog(text="Enter file name:", title="Save Data").get_input()
-        if file_name:
-            try:
-                with open(f"{file_name}.csv", "w", newline="", encoding='utf-8') as csvfile:
-                    writer = csv.writer(csvfile)
-                    writer.writerow(["Timestamp", "Value"])
-                    writer.writerows(self.logged_data)
-                self.save_data_button.configure(state=ctk.DISABLED)
-            except Exception as e:
-                print(f"Error saving data: {e}")
-
-    def update_data(self):
-        # Get EKG Data
-        timestamp, value = get_data()
-
-        # Update Multi Sweep Data
-        self.multi_data.append((timestamp, value))
-        if timestamp - self.multi_data[0][0] > self.multi_sweep_duration:
-            self.multi_data.pop(0)
-
-        # Trigger Logic
-        if self.trigger_armed and not self.trigger_paused and not self.cooldown_active:
-            condition = (value >= self.trigger_level and self.last_value < self.trigger_level) if self.flank_var.get() == "Steigende Flanke" else (value <= self.trigger_level and self.last_value > self.trigger_level)
-            if condition:
-                self.last_trigger_timestamp = timestamp
-                self.single_data = []
-                self.trigger_count = 0
-                if self.cooldown_time > 0:
-                    self.cooldown_active = True
-                    self.master.after(int(self.cooldown_time * 1000), self.end_cooldown)
-
-        # Update Single Sweep Data
-        if self.last_trigger_timestamp is not None and self.trigger_armed and not self.trigger_paused:
-            self.single_data.append((timestamp, value))
-
-        # Update Logged Data
-        if self.logging_active:
-            self.logged_data.append((timestamp, value))
-
-        # Draw Canvases
-        self.draw_single_canvas()
-        self.draw_multi_canvas()
-
-        # Store last value and update trigger count
-        self.last_value = value
-        if self.trigger_armed and not self.trigger_paused:
-            self.trigger_count += 1
-
-        # Call again after 1ms
-        self.master.after(1, self.update_data)
-
-    def end_cooldown(self):
-        self.cooldown_active = False
-
-    def draw_single_canvas(self):
-        self.single_canvas.delete("all")
-        if not self.single_data or self.last_trigger_timestamp is None:
-            return 
-        last_x, last_y = None, None
-        for timestamp, value in self.single_data:
-            x = (timestamp - self.last_trigger_timestamp) * self.single_canvas.winfo_width() / self.multi_sweep_duration
-            y = self.single_canvas.winfo_height() - (value * self.single_canvas.winfo_height() / 100)
-            if last_x is not None:
-                self.single_canvas.create_line(last_x, last_y, x, y, fill="blue")
-            last_x, last_y = x, y
-
-    def draw_multi_canvas(self):
-        self.multi_canvas.delete("all")
-        if not self.multi_data:
-            return 
-        duration = max(self.multi_data[-1][0] - self.multi_data[0][0], 0.001)
-        x_scale = self.multi_canvas.winfo_width() / duration
-        y_scale = self.multi_canvas.winfo_height() / 100
-        last_x, last_y = None, None
-        for timestamp, value in self.multi_data:
-            x = (timestamp - self.multi_data[0][0]) * x_scale
-            y = self.multi_canvas.winfo_height() - (value * y_scale)
-            if last_x is not None:
-                self.multi_canvas.create_line(last_x, last_y, x, y, fill="blue")
-            last_x, last_y = x, y
-        self.draw_time_axis(self.multi_canvas, self.multi_data[0][0], duration)
-        trigger_y = self.multi_canvas.winfo_height() - (self.trigger_level * y_scale)
-        self.multi_canvas.create_line(self.multi_canvas.winfo_width() - 10, trigger_y, self.multi_canvas.winfo_width(), trigger_y + 5, fill="red")
-        self.multi_canvas.create_line(self.multi_canvas.winfo_width() - 10, trigger_y, self.multi_canvas.winfo_width(), trigger_y - 5, fill="red")
-
-    def draw_time_axis(self, canvas, start_time, duration):
-        for i in range(int(duration) + 1):
-            x = i * canvas.winfo_width() / duration
-            canvas.create_line(x, 0, x, canvas.winfo_height(), fill="gray", dash=(2, 2))
-            canvas.create_text(x, canvas.winfo_height() - 10, text=f"{i:.1f}s", anchor=ctk.N)
-
-    def on_resize(self, event):
-        self.draw_single_canvas()
-        self.draw_multi_canvas()
-
-
-
-
-if __name__ == "__main__":
-    ctk.set_appearance_mode("dark")  # Modes: "System" (standard), "Dark", "Light"
-    ctk.set_default_color_theme("blue")  # Themes: "blue" (standard), "green", "dark-blue"
-    root = ctk.CTk()
-    app = EKGApp(root)
+import customtkinter as ctk
+import time
+import math
+import random
+import csv
+import tkinter as tk
+import customtkinter as ctk
+import time
+
+## cc maximilian scheinast-peter 
+## last update 11.04.2024
+## dieses programm funktioniert ähnlich wie ein digitales Oszi zur Darstellung von Vitalkurven
+## Attribution-NonCommercial license
+
+
+
+def get_data():         #example heartfunction
+    timestamp = time.time()## dont change
+
+
+    ## logic for get one datapoint like a read_value function
+    t = (timestamp % 1) * 2 * math.pi
+    p_wave = 5 * math.sin(t)
+    qrs_complex = 40 * math.sin(1.5 * t) * math.exp(-0.25 * t**2)
+    t_wave = 10 * math.sin(2 * t) * math.exp(-0.5 * t**2)
+    value = 50 + p_wave + qrs_complex + t_wave
+    value = max(0, min(value, 100))
+    wait_for_next_millisecond()
+
+    return timestamp, value
+
+
+
+
+
+
+
+
+
+def wait_for_next_millisecond():
+    """Waits until the next full millisecond."""
+    current_time = time.time()
+    next_millisecond = (int(current_time * 1000) + 1) / 1000
+    time.sleep(next_millisecond - current_time)
+# Function to generate realistic ECG data
+
+
+class EKGApp:
+    def __init__(self, master):
+        self.master = master
+        master.title("EKG Visualization")
+
+        # Main Frame
+        main_frame = ctk.CTkFrame(master)
+        main_frame.pack(fill=ctk.BOTH, expand=True)
+
+        # Single Sweep Canvas
+        self.single_canvas = ctk.CTkCanvas(main_frame, width=600, height=300)
+        self.single_canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
+
+        # Multi Sweep Canvas
+        self.multi_canvas = ctk.CTkCanvas(main_frame, width=600, height=300)
+        self.multi_canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
+
+        # Input Frame
+        input_frame = ctk.CTkFrame(main_frame)
+        input_frame.pack(side=ctk.RIGHT, fill=ctk.Y)
+
+        # Data Structures
+        self.single_data = []
+        self.multi_data = []
+        self.logged_data = []
+
+        # Time Variables
+        self.start_time = time.time()
+        self.multi_sweep_duration = 30  # Initial Laufbanddauer
+
+        # Trigger Variables
+        self.trigger_level = 50
+        self.trigger_armed = False
+        self.trigger_paused = False
+        self.trigger_timestamp = None
+        self.last_trigger_timestamp = None
+        self.last_value = 0
+        self.trigger_count = 0
+
+        # Cooldown Variables
+        self.cooldown_time = 0
+        self.cooldown_active = False
+
+        # Logging Variables
+        self.logging_active = False
+
+        # Input Fields and Buttons (with consistent size)
+        self.create_input_fields(input_frame)
+
+        # Logging Frame
+        logging_frame = ctk.CTkFrame(master)
+        logging_frame.pack(fill=ctk.X)
+        self.create_logging_buttons(logging_frame)
+
+        # Update Data Periodically
+        self.update_data()
+
+        # Resize Handling
+        self.single_canvas.bind("<Configure>", self.on_resize)
+        self.multi_canvas.bind("<Configure>", self.on_resize)
+
+    def create_input_fields(self, frame):
+        # Consistent width for all input elements
+        input_width = 150
+
+        # Trigger Level
+        ctk.CTkLabel(frame, text="Trigger Level:").pack()
+        self.trigger_entry = ctk.CTkEntry(frame, width=input_width)
+        self.trigger_entry.insert(0, str(self.trigger_level))
+        self.trigger_entry.pack()
+
+        # Flank Selection
+        ctk.CTkLabel(frame, text="Flanke:").pack()
+        self.flank_var = ctk.StringVar(value="Steigende Flanke")
+        self.flank_combobox = ctk.CTkComboBox(frame, variable=self.flank_var, values=["Steigende Flanke", "Fallende Flanke"], width=input_width)
+        self.flank_combobox.pack()
+
+        # Cooldown
+        ctk.CTkLabel(frame, text="Cooldown (Sekunden):").pack()
+        self.cooldown_entry = ctk.CTkEntry(frame, width=input_width)
+        self.cooldown_entry.insert(0, "0")
+        self.cooldown_entry.pack()
+        self.cooldown_button = ctk.CTkButton(frame, text="Cooldown setzen", command=self.set_cooldown, width=input_width)
+        self.cooldown_button.pack()
+
+
+        # Ax Time (X-Achsen-Zeit)
+        ctk.CTkLabel(frame, text="Ax-Zeit (Sekunden):").pack()
+        self.ax_time_entry = ctk.CTkEntry(frame, width=input_width)
+        self.ax_time_entry.insert(0, str(self.multi_sweep_duration))  # Initial value
+        self.ax_time_entry.pack()
+        self.ax_time_button = ctk.CTkButton(frame, text="Ax-Zeit setzen", command=self.set_ax_time, width=input_width)
+        self.ax_time_button.pack()
+
+
+        # Trigger Buttons
+        self.trigger_button = ctk.CTkButton(frame, text="Trigger starten", command=self.toggle_trigger, width=input_width)
+        self.trigger_button.pack()
+        self.pause_button = ctk.CTkButton(frame, text="Trigger pausieren", command=self.pause_trigger, state=ctk.DISABLED, width=input_width)
+        self.pause_button.pack()
+
+        # Multi Sweep Duration (Laufbanddauer)
+        ctk.CTkLabel(frame, text="Laufbanddauer (Sekunden):").pack()
+        self.duration_entry = ctk.CTkEntry(frame, width=input_width)
+        self.duration_entry.insert(0, "30")
+        self.duration_entry.pack()
+        self.duration_button = ctk.CTkButton(frame, text="Dauer setzen", command=self.set_duration, width=input_width) 
+        self.duration_button.pack()
+
+
+    def set_ax_time(self):
+        try:
+            self.multi_sweep_duration = float(self.ax_time_entry.get())
+        except ValueError:
+            pass
+    def create_logging_buttons(self, frame):
+        # Consistent width for logging buttons
+        button_width = 120
+
+        self.start_logging_button = ctk.CTkButton(frame, text="Start Logging", command=self.start_logging, width=button_width)
+        self.start_logging_button.pack(side=ctk.LEFT)
+        self.end_logging_button = ctk.CTkButton(frame, text="End Logging", command=self.end_logging, state=ctk.DISABLED, width=button_width)
+        self.end_logging_button.pack(side=ctk.LEFT)
+        self.save_data_button = ctk.CTkButton(frame, text="Save Data", command=self.save_data, state=ctk.DISABLED, width=button_width)
+        self.save_data_button.pack(side=ctk.LEFT)
+
+
+    
+
+    def set_cooldown(self):
+        try:
+            self.cooldown_time = float(self.cooldown_entry.get())
+        except ValueError:
+            pass
+
+    def set_duration(self):
+        try:
+            self.multi_sweep_duration = float(self.duration_entry.get())
+        except ValueError:
+            pass
+
+    def toggle_trigger(self):
+        if self.trigger_armed:
+            self.trigger_armed = False
+            self.trigger_button.configure(text="Trigger starten")
+            self.pause_button.configure(state=ctk.DISABLED)
+            self.single_data = []
+            self.draw_single_canvas()
+        else:
+            try:
+                self.trigger_level = float(self.trigger_entry.get())
+            except ValueError:
+                pass
+            self.trigger_armed = True
+            self.trigger_button.configure(text="Trigger stoppen")
+            self.pause_button.configure(state=ctk.NORMAL)
+            self.trigger_count = 0
+
+    def pause_trigger(self):
+        self.trigger_paused = not self.trigger_paused
+        if self.trigger_paused:
+            self.pause_button.configure(text="Trigger fortsetzen")
+        else:
+            self.pause_button.configure(text="Trigger pausieren")
+
+    def start_logging(self):
+        self.logging_active = True
+        self.logged_data = []
+        self.start_logging_button.configure(state=ctk.DISABLED)
+        self.end_logging_button.configure(state=ctk.NORMAL)
+
+    def end_logging(self):
+        self.logging_active = False
+        self.start_logging_button.configure(state=ctk.NORMAL)
+        self.end_logging_button.configure(state=ctk.DISABLED)
+        self.save_data_button.configure(state=ctk.NORMAL)
+
+    def save_data(self):
+        # Popup for file name
+        print(123)
+        file_name = ctk.CTkInputDialog(text="Enter file name:", title="Save Data").get_input()
+        if file_name:
+            try:
+                with open(f"{file_name}.csv", "w", newline="", encoding='utf-8') as csvfile:
+                    writer = csv.writer(csvfile)
+                    writer.writerow(["Timestamp", "Value"])
+                    writer.writerows(self.logged_data)
+                self.save_data_button.configure(state=ctk.DISABLED)
+            except Exception as e:
+                print(f"Error saving data: {e}")
+
+    def update_data(self):
+        # Get EKG Data
+        timestamp, value = get_data()
+
+        # Update Multi Sweep Data
+        self.multi_data.append((timestamp, value))
+        if timestamp - self.multi_data[0][0] > self.multi_sweep_duration:
+            self.multi_data.pop(0)
+
+        # Trigger Logic
+        if self.trigger_armed and not self.trigger_paused and not self.cooldown_active:
+            condition = (value >= self.trigger_level and self.last_value < self.trigger_level) if self.flank_var.get() == "Steigende Flanke" else (value <= self.trigger_level and self.last_value > self.trigger_level)
+            if condition:
+                self.last_trigger_timestamp = timestamp
+                self.single_data = []
+                self.trigger_count = 0
+                if self.cooldown_time > 0:
+                    self.cooldown_active = True
+                    self.master.after(int(self.cooldown_time * 1000), self.end_cooldown)
+
+        # Update Single Sweep Data
+        if self.last_trigger_timestamp is not None and self.trigger_armed and not self.trigger_paused:
+            self.single_data.append((timestamp, value))
+
+        # Update Logged Data
+        if self.logging_active:
+            self.logged_data.append((timestamp, value))
+
+        # Draw Canvases
+        self.draw_single_canvas()
+        self.draw_multi_canvas()
+
+        # Store last value and update trigger count
+        self.last_value = value
+        if self.trigger_armed and not self.trigger_paused:
+            self.trigger_count += 1
+
+        # Call again after 1ms
+        self.master.after(1, self.update_data)
+
+    def end_cooldown(self):
+        self.cooldown_active = False
+
+    def draw_single_canvas(self):
+        self.single_canvas.delete("all")
+        if not self.single_data or self.last_trigger_timestamp is None:
+            return 
+        last_x, last_y = None, None
+        for timestamp, value in self.single_data:
+            x = (timestamp - self.last_trigger_timestamp) * self.single_canvas.winfo_width() / self.multi_sweep_duration
+            y = self.single_canvas.winfo_height() - (value * self.single_canvas.winfo_height() / 100)
+            if last_x is not None:
+                self.single_canvas.create_line(last_x, last_y, x, y, fill="blue")
+            last_x, last_y = x, y
+
+    def draw_multi_canvas(self):
+        self.multi_canvas.delete("all")
+        if not self.multi_data:
+            return 
+        duration = max(self.multi_data[-1][0] - self.multi_data[0][0], 0.001)
+        x_scale = self.multi_canvas.winfo_width() / duration
+        y_scale = self.multi_canvas.winfo_height() / 100
+        last_x, last_y = None, None
+        for timestamp, value in self.multi_data:
+            x = (timestamp - self.multi_data[0][0]) * x_scale
+            y = self.multi_canvas.winfo_height() - (value * y_scale)
+            if last_x is not None:
+                self.multi_canvas.create_line(last_x, last_y, x, y, fill="blue")
+            last_x, last_y = x, y
+        self.draw_time_axis(self.multi_canvas, self.multi_data[0][0], duration)
+        trigger_y = self.multi_canvas.winfo_height() - (self.trigger_level * y_scale)
+        self.multi_canvas.create_line(self.multi_canvas.winfo_width() - 10, trigger_y, self.multi_canvas.winfo_width(), trigger_y + 5, fill="red")
+        self.multi_canvas.create_line(self.multi_canvas.winfo_width() - 10, trigger_y, self.multi_canvas.winfo_width(), trigger_y - 5, fill="red")
+
+    def draw_time_axis(self, canvas, start_time, duration):
+        for i in range(int(duration) + 1):
+            x = i * canvas.winfo_width() / duration
+            canvas.create_line(x, 0, x, canvas.winfo_height(), fill="gray", dash=(2, 2))
+            canvas.create_text(x, canvas.winfo_height() - 10, text=f"{i:.1f}s", anchor=ctk.N)
+
+    def on_resize(self, event):
+        self.draw_single_canvas()
+        self.draw_multi_canvas()
+
+
+
+
+if __name__ == "__main__":
+    ctk.set_appearance_mode("dark")  # Modes: "System" (standard), "Dark", "Light"
+    ctk.set_default_color_theme("blue")  # Themes: "blue" (standard), "green", "dark-blue"
+    root = ctk.CTk()
+    app = EKGApp(root)
     root.mainloop()