290 lines
9.0 KiB
Python
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)
|