diff --git a/nucleares_monitor/control_board_monitor.py b/nucleares_monitor/control_board_monitor.py index 896fb81..d641fbc 100644 --- a/nucleares_monitor/control_board_monitor.py +++ b/nucleares_monitor/control_board_monitor.py @@ -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 ---