From f81a41e4bc0e0879fa0e98130fa3b5a18b54f88b Mon Sep 17 00:00:00 2001 From: "Josef M. Gallmetzer" <64498081+galjos@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:45:41 +0200 Subject: [PATCH] perf: speed up dashboard redraws --- PQEnalyzer/plots/plot_dashboard.py | 61 ++++++++++++++++++++++++------ tests/plots/test_gui_plots.py | 61 +++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/PQEnalyzer/plots/plot_dashboard.py b/PQEnalyzer/plots/plot_dashboard.py index 797ef1c..447f3cd 100644 --- a/PQEnalyzer/plots/plot_dashboard.py +++ b/PQEnalyzer/plots/plot_dashboard.py @@ -30,7 +30,9 @@ logger = get_logger(__name__) -RESIZE_DEBOUNCE_MS = 120 +RESIZE_DEBOUNCE_MS = 60 +COMPACT_PANEL_WIDTH = 220 +COMPACT_PANEL_HEIGHT = 145 class PlotDashboard: @@ -53,6 +55,7 @@ def __init__(self, app): self.subtitle_text = None self._panel_scale = 1.0 self._compact_labels = False + self._layout_signature = None self._pending_resize = None self._resize_after_id = None @@ -161,7 +164,7 @@ def redraw(self) -> None: self.__reflow_axes() self._panel_scale = self.__panel_scale() requested_scale = getattr(self.app, "plot_scale", 1.0) - self._compact_labels = self._panel_scale < requested_scale - 0.05 + self._compact_labels = self.__uses_compact_labels(requested_scale) for ax in self.axes: ax.clear() apply_figure_theme( @@ -378,17 +381,20 @@ def __apply_shared_x_limits(self): Apply the full plotted time range to every dashboard panel. """ - x_values = [] + lower = math.inf + upper = -math.inf for ax in self.axes[:len(self.parameters)]: for line in ax.lines: values = np.asarray(line.get_xdata(), dtype=float) - x_values.extend(values[np.isfinite(values)]) + finite_values = values[np.isfinite(values)] + if finite_values.size == 0: + continue + lower = min(lower, float(np.min(finite_values))) + upper = max(upper, float(np.max(finite_values))) - if not x_values: + if not math.isfinite(lower) or not math.isfinite(upper): return - lower = min(x_values) - upper = max(x_values) if lower == upper: margin = max(abs(lower) * 0.05, 0.5) lower -= margin @@ -523,16 +529,16 @@ def __apply_pending_resize(self): grid_changed = self.__reflow_axes(width, height) panel_scale = self.__panel_scale() requested_scale = getattr(self.app, "plot_scale", 1.0) - compact_labels = panel_scale < requested_scale - 0.05 + compact_labels = self.__uses_compact_labels( + requested_scale, + panel_scale, + ) style_changed = ( panel_scale != self._panel_scale or compact_labels != self._compact_labels ) if grid_changed or style_changed: self.redraw() - else: - self.__fit_layout() - self.figure.canvas.draw_idle() def __create_axes(self, grid_shape): """ @@ -609,6 +615,24 @@ def __panel_scale(self): ) return math.floor(panel_scale * 20 + 1e-9) / 20 + def __uses_compact_labels(self, requested_scale, panel_scale=None): + """ + Keep labels readable when either text or panel density is constrained. + """ + + if panel_scale is None: + panel_scale = self._panel_scale + + width, height = self._canvas_size + nrows, ncols = self._grid_shape + panel_width = width / ncols + panel_height = height * 0.82 / nrows + return ( + panel_scale < requested_scale - 0.05 + or panel_width < COMPACT_PANEL_WIDTH + or panel_height < COMPACT_PANEL_HEIGHT + ) + def __header_scale(self, maximum): """ Return a bounded scale for shared dashboard header elements. @@ -647,20 +671,33 @@ def __reflow_axes(self, width=None, height=None): self.figure.clear() self.subtitle_text = None self.axis_parameters = {} + self._layout_signature = None self._grid_shape = grid_shape self.axes = self.__create_axes(grid_shape) return True def __fit_layout(self): """ - Refit dashboard labels to the current canvas dimensions. + Refit labels only when the dashboard structure or typography changes. """ + signature = ( + self._grid_shape, + self._panel_scale, + self._compact_labels, + self.__header_scale(1.5), + self.__header_scale(1.25), + len(self.reader.filenames), + ) + if signature == self._layout_signature: + return + self.figure.tight_layout( rect=(0, 0.03, 1, 0.92), h_pad=0.7, w_pad=0.8, ) + self._layout_signature = signature def __style_axis(self, ax, parameter): """ diff --git a/tests/plots/test_gui_plots.py b/tests/plots/test_gui_plots.py index 0c451cc..8c78754 100644 --- a/tests/plots/test_gui_plots.py +++ b/tests/plots/test_gui_plots.py @@ -682,7 +682,7 @@ def test_dashboard_coalesces_rapid_native_resize_events(): SimpleNamespace(width=1600, height=600)) _, _, final_callback = app.after_calls[-1] - assert delay == 120 + assert delay == 60 assert app.cancelled_after_ids == [first_after_id] assert plot._grid_shape == (5, 5) np.testing.assert_allclose( @@ -697,6 +697,65 @@ def test_dashboard_coalesces_rapid_native_resize_events(): assert plot._resize_after_id is None +def test_dashboard_skips_relayout_without_responsive_changes(): + energy = FakeLargeDashboardEnergy(number_of_parameters=11) + app = FakeScheduledApp([energy]) + app.info = energy.parameters + plot = PlotDashboard(app) + redraw_calls = [] + plot.redraw = lambda: redraw_calls.append(True) + + plot.figure.set_size_inches(14, 8) + plot._PlotDashboard__resize_event( + SimpleNamespace(width=1400, height=800)) + _, _, callback = app.after_calls[-1] + + callback() + + assert plot._grid_shape == (3, 4) + assert redraw_calls == [] + + +def test_dashboard_reuses_layout_until_responsive_style_changes(): + energy = FakeDashboardEnergy() + app = FakeApp([energy]) + plot = PlotDashboard(app) + layout_calls = [] + plot.figure.tight_layout = lambda **kwargs: layout_calls.append(kwargs) + + plot.redraw() + energy.data["TEMPERATURE"][-1] = 333.0 + plot.redraw() + + assert len(layout_calls) == 1 + assert plot.axes[0].texts[0].get_text() == "333 K" + + app.plot_scale = 1.25 + plot.redraw() + plot.redraw() + + assert len(layout_calls) == 2 + + +def test_dashboard_compacts_labels_when_small_without_grid_change(): + energy = FakeLargeDashboardEnergy(number_of_parameters=11) + app = FakeApp([energy]) + app.info = energy.parameters + plot = PlotDashboard(app) + plot.redraw() + + assert plot._grid_shape == (3, 4) + assert plot._compact_labels is False + + plot.figure.set_size_inches(7, 4.5) + plot._PlotDashboard__resize_event( + SimpleNamespace(width=700, height=450)) + + assert plot._grid_shape == (3, 4) + assert plot._compact_labels is True + assert plot.axes[0].get_title(loc="left") == "PARAMETER-00" + + def test_dashboard_growth_never_reduces_average_panel_area(): energy = FakeLargeDashboardEnergy(number_of_parameters=11) app = FakeApp([energy])