This commit is contained in:
2025-10-20 18:31:30 +02:00
parent e06e297540
commit 4beb01118a
+102 -49
View File
@@ -35,6 +35,10 @@ try:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
HAS_MPL = True
import matplotlib as mpl
mpl.rcParams["path.simplify"] = True
mpl.rcParams["agg.path.chunksize"] = 10000
except Exception:
HAS_MPL = False
@@ -1011,6 +1015,84 @@ class App(tk.Tk):
self.actions_tree.selection_set(kept)
if focus_before and self.actions_tree.exists(focus_before):
self.actions_tree.focus(focus_before)
def _ensure_plot_timer(self):
if self._plot_timer is None:
# odświeżanie wykresów zsynchronizowane z głównym intervalem
try:
ms = int(max(100, float(self.refresh_var.get()) * 1000))
except Exception:
ms = 500
self._plot_timer = self.after(ms, self._plot_tick)
def _plot_tick(self):
# jeśli nie ma żadnych okien zatrzymaj timer
windows = [w for w in getattr(self, "_plot_windows", {}).values() if w.winfo_exists()]
if not windows:
self._plot_timer = None
return
for win in windows:
key = getattr(win, "_key", None)
vi = self.vars.get(key)
if not vi:
continue
# ostatnie 100 próbek
vals = list(vi.history)[-100:] if hasattr(vi, "history") else []
dels = list(vi.history_delta)[-100:] if hasattr(vi, "history_delta") else []
# --- GÓRNY wykres: linia ---
xs_v = list(range(len(vals)))
win._line.set_data(xs_v, vals)
if vals:
vmin, vmax = min(vals), max(vals)
span = (vmax - vmin) or 1.0
m = 0.08 * span
win._ax_val.set_xlim(0, max(1, len(xs_v) - 1))
win._ax_val.set_ylim(vmin - m, vmax + m)
win._ax_val.set_ylabel("value")
# --- DOLNY wykres: słupki (bez ponownego tworzenia barów jeśli długość ta sama) ---
if len(dels) != win._bars_len:
# przebuduj tylko gdy zmienia się liczba słupków
for b in win._bars:
b.remove()
win._bars = win._ax_delta.bar(range(len(dels)), dels)
win._bars_len = len(dels)
else:
# szybka ścieżka zaktualizuj wysokości
for b, h in zip(win._bars, dels):
b.set_height(h)
if dels:
dmin, dmax = min(dels), max(dels)
span = (dmax - dmin) or 1.0
m = 0.08 * span
win._ax_delta.set_xlim(0, max(1, len(dels) - 1))
win._ax_delta.set_ylim(dmin - m, dmax + m)
win._ax_delta.set_ylabel("Δ")
win._ax_delta.set_xlabel("last 100 samples")
# tight_layout tylko po resize (ustawiane flagą)
if getattr(win, "_layout_dirty", False):
try:
win._fig.tight_layout()
except Exception:
pass
win._layout_dirty = False
# draw_idle = koalescencja
try:
win._canvas.draw_idle()
except Exception:
pass
# zaplanuj kolejny tick
try:
ms = int(max(100, float(self.refresh_var.get()) * 1000))
except Exception:
ms = 500
self._plot_timer = self.after(ms, self._plot_tick)
# --- Poller control / rendering ---
def start_polling(self) -> None:
@@ -1168,6 +1250,7 @@ class App(tk.Tk):
if not hasattr(self, "_plot_windows"):
self._plot_windows = {}
self._plot_timer = None # globalny timer do wykresów
w = self._plot_windows.get(key)
if w and w.winfo_exists():
@@ -1188,65 +1271,29 @@ class App(tk.Tk):
canvas = FigureCanvasTkAgg(fig, master=win)
canvas.draw()
# ... masz już fig, ax_val, ax_delta, canvas ...
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
self._plot_windows[key] = win
win._fig = fig
win._ax_val = ax_val
win._ax_delta = ax_delta
win._canvas = canvas
win._key = key
win._layout_dirty = True
def refresh_plot():
if not win.winfo_exists():
return
vinfo = self.vars.get(key)
if not vinfo:
win.after(1000, refresh_plot)
return
vals = list(vinfo.history)[-100:] if hasattr(vinfo, "history") else []
dels = list(vinfo.history_delta)[-100:] if hasattr(vinfo, "history_delta") else []
xs_v = list(range(len(vals)))
xs_d = list(range(len(dels)))
ax_val.clear(); ax_delta.clear()
# prealokuj „artystów” jedna linia i zestaw słupków
(win._line,) = ax_val.plot([], [], linewidth=1.2)
win._bars = ax_delta.bar([], [])
win._bars_len = 0 # ile aktualnie słupków
if vals:
ax_val.plot(xs_v, vals, linewidth=1.2)
mv = sum(vals) / len(vals)
ax_val.axhline(mv, linestyle="--", linewidth=1.2)
vmin, vmax = min(vals), max(vals)
span = (vmax - vmin) or 1.0
margin = 0.08 * span
ax_val.set_ylim(vmin - margin, vmax + margin)
ax_val.set_ylabel("value")
else:
ax_val.set_ylabel("value (no data)")
if dels:
ax_delta.bar(xs_d, dels)
md = sum(dels) / len(dels)
ax_delta.axhline(md, linestyle="--", linewidth=1.2)
dmin, dmax = min(dels), max(dels)
span = (dmax - dmin) or 1.0
margin = 0.08 * span
ax_delta.set_ylim(dmin - margin, dmax + margin)
ax_delta.set_ylabel("Δ")
else:
ax_delta.set_ylabel("Δ (no data)")
ax_delta.set_xlabel("last 100 samples")
fig.tight_layout()
try:
canvas.draw()
except Exception:
pass
try:
interval_ms = int(max(250, float(self.refresh_var.get()) * 1000))
except Exception:
interval_ms = 1000
win.after(interval_ms, refresh_plot)
win.after(200, refresh_plot)
# tight_layout tylko po resize
def _on_resize(_evt=None):
win._layout_dirty = True
canvas.mpl_connect("resize_event", _on_resize)
# uruchom globalny tick jeśli nie działa
self._ensure_plot_timer()
def open_thresholds_dialog(self):
sel = self.tree.selection()
@@ -2007,6 +2054,12 @@ class App(tk.Tk):
self.scheduler.stop()
except Exception:
pass
try:
if self._plot_timer is not None:
self.after_cancel(self._plot_timer)
self._plot_timer = None
except Exception:
pass
self.destroy()
# --- Utils ---