"""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)