Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions PQEnalyzer/plots/plot_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
"""
Expand Down
61 changes: 60 additions & 1 deletion tests/plots/test_gui_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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])
Expand Down
Loading