Files
pyGUI/tests/test_graph_quick_item.py
2026-07-10 20:54:24 -07:00

290 lines
9.0 KiB
Python

"""Tests for src/pygui/backend/visualization/graph_quick_item.py."""
import pytest
pytestmark = pytest.mark.usefixtures("qapp")
def test_no_data_initially():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
assert item.seriesData == []
assert item.sensorNames == []
def test_series_data_setter_stores_series():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.seriesData = [[150.0, 151.0], [148.0, 147.5]]
assert item.seriesData == [[150.0, 151.0], [148.0, 147.5]]
def test_series_data_auto_ranges_y_with_padding():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.seriesData = [[150.0, 100.0], [200.0, 120.0]]
# min=100, max=200, range=100, 10% pad => [90, 210]
assert item._min_y == pytest.approx(90.0)
assert item._max_y == pytest.approx(210.0)
def test_series_data_empty_keeps_default_range():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.seriesData = []
assert item._min_y == 0.0
assert item._max_y == 150.0
def test_sensor_names_setter_stores_names():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.sensorNames = ["Sensor 1", "Sensor 2"]
assert item.sensorNames == ["Sensor 1", "Sensor 2"]
def test_non_numeric_values_are_skipped_not_crashing():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.seriesData = [[150.0, "bad", 151.0]]
assert item._min_y == pytest.approx(149.9)
assert item._max_y == pytest.approx(151.1)
# ---- viewport_stats (replay chart min/max/avg readouts) ----
# Independently hand-computed against PopupChartForm.cs's recalc_stats
# semantics: aggregate across every sensor's points in [start, end], plus
# which sensor achieved each extreme.
def test_viewport_stats_full_range_aggregate():
from pygui.backend.visualization.graph_quick_item import viewport_stats
series = [[150.0, 151.0, 152.0], [148.0, 147.5, 147.0]]
names = ["Sensor 1", "Sensor 2"]
vmin, vmax, avg, min_sensor, max_sensor = viewport_stats(series, names, 0, 2)
assert vmin == pytest.approx(147.0)
assert vmax == pytest.approx(152.0)
assert avg == pytest.approx(149.25)
assert min_sensor == "Sensor 2"
assert max_sensor == "Sensor 1"
def test_viewport_stats_restricted_to_subrange():
from pygui.backend.visualization.graph_quick_item import viewport_stats
series = [[150.0, 151.0, 152.0], [148.0, 147.5, 147.0]]
names = ["Sensor 1", "Sensor 2"]
vmin, vmax, avg, min_sensor, max_sensor = viewport_stats(series, names, 0, 1)
assert vmin == pytest.approx(147.5)
assert vmax == pytest.approx(151.0)
assert avg == pytest.approx(149.125)
assert min_sensor == "Sensor 2"
assert max_sensor == "Sensor 1"
def test_viewport_stats_empty_series_returns_zeros():
from pygui.backend.visualization.graph_quick_item import viewport_stats
assert viewport_stats([], [], 0, -1) == (0.0, 0.0, 0.0, "", "")
def test_viewport_stats_skips_non_numeric():
from pygui.backend.visualization.graph_quick_item import viewport_stats
series = [[150.0, "bad", 151.0]]
names = ["Sensor 1"]
vmin, vmax, avg, min_sensor, max_sensor = viewport_stats(series, names, 0, 2)
assert vmin == pytest.approx(150.0)
assert vmax == pytest.approx(151.0)
assert avg == pytest.approx(150.5)
assert min_sensor == "Sensor 1"
assert max_sensor == "Sensor 1"
# ---- GraphQuickItem viewport properties (replay chart) ----
# Default viewport (0, -1) means "whole series" so the already-shipped Graph
# tab (which never sets these) is unaffected — see docs/adr/0004.
def test_viewport_defaults_to_whole_range_and_markers_off():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
assert item.viewStartIndex == 0
assert item.viewEndIndex == -1
assert item.showMinMaxMarkers is False
def test_view_stats_reflect_default_full_range_viewport():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.sensorNames = ["Sensor 1", "Sensor 2"]
item.seriesData = [[150.0, 151.0, 152.0], [148.0, 147.5, 147.0]]
assert item.viewMin == pytest.approx(147.0)
assert item.viewMax == pytest.approx(152.0)
assert item.viewAvg == pytest.approx(149.25)
assert item.viewMinSensor == "Sensor 2"
assert item.viewMaxSensor == "Sensor 1"
def test_view_stats_restrict_to_narrowed_viewport():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.sensorNames = ["Sensor 1", "Sensor 2"]
item.seriesData = [[150.0, 151.0, 152.0], [148.0, 147.5, 147.0]]
item.viewStartIndex = 0
item.viewEndIndex = 1
assert item.viewMin == pytest.approx(147.5)
assert item.viewMax == pytest.approx(151.0)
assert item.viewAvg == pytest.approx(149.125)
def test_auto_range_y_restricted_to_narrowed_viewport():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.seriesData = [[150.0, 100.0], [200.0, 120.0]]
# Full-range Y (pre-existing behavior): min=100, max=200 -> [90, 210]
assert item._min_y == pytest.approx(90.0)
assert item._max_y == pytest.approx(210.0)
# Narrow the viewport to index 0 only: min=150, max=200, range=50, 10% pad
item.viewStartIndex = 0
item.viewEndIndex = 0
assert item._min_y == pytest.approx(145.0)
assert item._max_y == pytest.approx(205.0)
# ---- zoomAtFraction / panByFraction / resetZoom (replay chart interaction) ----
# 11-point series (indices 0..10) chosen so the zoom/pan arithmetic lands on
# whole numbers -- avoids rounding ambiguity in the independent hand trace.
def _eleven_point_item():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.seriesData = [[float(i) for i in range(11)]]
return item
def test_zoom_in_centers_on_fraction():
item = _eleven_point_item()
# Full range is (0, 10), width 10. Zooming to 40% width centered at the
# midpoint (frac=0.5): center=5, new_width=4 -> new_start=3, new_end=7.
item.zoomAtFraction(0.5, 0.4)
assert item.viewStartIndex == 3
assert item.viewEndIndex == 7
def test_zoom_out_clamps_to_data_bounds():
item = _eleven_point_item()
item.viewStartIndex = 3
item.viewEndIndex = 7
# width=4, zoom out 3x centered at midpoint (frac=0.5): requested
# new_width=12 exceeds the data's max width of 10, so it clamps to the
# full range (0, 10).
item.zoomAtFraction(0.5, 3.0)
assert item.viewStartIndex == 0
assert item.viewEndIndex == 10
def test_pan_shifts_viewport_by_fraction_of_width():
item = _eleven_point_item()
item.viewStartIndex = 4
item.viewEndIndex = 8
# width=4, pan by 50% of width (2 indices): (4,8) -> (6,10)
item.panByFraction(0.5)
assert item.viewStartIndex == 6
assert item.viewEndIndex == 10
def test_pan_clamps_at_right_edge_preserving_width():
item = _eleven_point_item()
item.viewStartIndex = 4
item.viewEndIndex = 8
# width=4, pan by 100% of width (4 indices) would be (8,12); index 12 is
# out of bounds (max index 10), so the whole window shifts back by 2 to
# stay in bounds while keeping width=4: (6,10).
item.panByFraction(1.0)
assert item.viewStartIndex == 6
assert item.viewEndIndex == 10
def test_reset_zoom_restores_full_range_sentinel():
item = _eleven_point_item()
item.viewStartIndex = 3
item.viewEndIndex = 7
item.resetZoom()
assert item.viewStartIndex == 0
assert item.viewEndIndex == -1
def test_zoom_noop_when_fewer_than_two_points():
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.seriesData = [[42.0]]
item.zoomAtFraction(0.5, 0.5)
assert item.viewStartIndex == 0
assert item.viewEndIndex == -1
def test_loading_new_series_while_zoomed_resets_viewport_to_full_range():
"""Regression: zooming near the tail of a long run, then loading a new
(shorter) file, left the stale viewport pointing past the new data --
every per-series slice became empty and the chart went blank with a
stale Y-range and zeroed-out min/max/avg readouts. New data must reset
the viewport to the full range (PopupChartForm.cs's SetData() parity).
"""
from pygui.backend.visualization.graph_quick_item import GraphQuickItem
item = GraphQuickItem()
item.seriesData = [[float(i) for i in range(20)]]
item.viewStartIndex = 15
item.viewEndIndex = 19
item.seriesData = [[100.0, 101.0, 102.0, 103.0, 104.0]]
assert item.viewStartIndex == 0
assert item.viewEndIndex == -1
assert item.viewMin == pytest.approx(100.0)
assert item.viewMax == pytest.approx(104.0)
assert item.viewAvg == pytest.approx(102.0)