1010import pyqtgraph as pg # type: ignore
1111from qtpy import QtCore , QtWidgets # type: ignore
1212
13+ # --- Global pyqtgraph performance tuning ---
14+ pg .setConfigOptions (antialias = False , useOpenGL = True )
15+
1316from robot_log_visualizer .signal_provider .signal_provider import ProviderType
1417from robot_log_visualizer .utils .utils import ColorPalette
1518
@@ -55,6 +58,7 @@ def __init__(
5558
5659 # data structures
5760 self ._curves : Dict [str , pg .PlotDataItem ] = {}
61+ self ._curve_fulldata : Dict [str , tuple ] = {} # key -> (x, y) full-res
5862 self ._annotations : Dict [Point , pg .TextItem ] = {}
5963 self ._markers : Dict [Point , pg .ScatterPlotItem ] = {}
6064 self ._palette : Iterable = ColorPalette ()
@@ -129,7 +133,12 @@ def resume_animation(self) -> None: # noqa: D401
129133 def quit_animation (self ) -> None : # noqa: D401
130134 """Permanently stop the animation (e.g. when closing the tab)."""
131135 self ._timer .stop ()
132-
136+ def update_vline_value (self , value : float ) -> None :
137+ """Directly set the vline position (called from update_index)."""
138+ if value == getattr (self , '_last_vline_val' , None ):
139+ return
140+ self ._last_vline_val = value
141+ self ._vline .setValue (value )
133142 # -------------------------------------------------------------#
134143 # Qt event handlers #
135144 # -------------------------------------------------------------#
@@ -147,6 +156,11 @@ def _init_ui(self) -> None:
147156 self ._plot : pg .PlotWidget = pg .PlotWidget (background = "w" )
148157 self ._layout .addWidget (self ._plot )
149158
159+ # --- Performance: only render points visible in the current
160+ # view-port and auto-downsample when zoomed out.
161+ self ._plot .setClipToView (True )
162+ self ._plot .setDownsampling (mode = "peak" )
163+
150164 label_style = {"color" : "#000000" , "font-size" : "12pt" }
151165 self ._plot .setLabel ("bottom" , "Time [s]" , ** label_style )
152166 self ._plot .setLabel ("left" , "Value" , ** label_style )
@@ -193,13 +207,18 @@ def _add_missing_curves(
193207 palette_color = next (self ._palette )
194208 pen = pg .mkPen (palette_color .as_hex (), width = 2 )
195209
210+ # Pre-downsample for fast rendering (207k → ~4000 points)
211+ x_ds , y_ds = self ._downsample_peak (x , y )
212+
196213 self ._curves [key ] = self ._plot .plot (
197- x ,
198- y ,
214+ x_ds ,
215+ y_ds ,
199216 pen = pen ,
200217 name = "/" .join (legend [1 :]),
201218 symbol = None ,
219+ clipToView = True ,
202220 )
221+ self ._curve_fulldata [key ] = (x , y )
203222
204223 # For real-time mode, disable curve clickability to avoid interfering with live updates
205224 if self ._signal_provider .provider_type == ProviderType .REALTIME :
@@ -211,13 +230,46 @@ def _remove_obsolete_curves(self, paths: Sequence[Path]) -> None:
211230 valid = {"/" .join (p ) for p in paths }
212231 for key in [k for k in self ._curves if k not in valid ]:
213232 self ._plot .removeItem (self ._curves .pop (key ))
233+ self ._curve_fulldata .pop (key , None )
234+
235+ @staticmethod
236+ def _downsample_peak (x , y , max_points : int = 4000 ):
237+ """Peak-preserving downsample: keep min/max per window."""
238+ n = len (x )
239+ if n <= max_points :
240+ return x , y
241+ factor = max (1 , n // (max_points // 2 ))
242+ trim = (n // factor ) * factor
243+ yr = y [:trim ].reshape (- 1 , factor )
244+ xr = x [:trim ].reshape (- 1 , factor )
245+ stx = factor // 2
246+ x_mid = xr [:, stx ]
247+ y_max_v = yr .max (axis = 1 )
248+ y_min_v = yr .min (axis = 1 )
249+ out_n = len (x_mid ) * 2
250+ x_out = np .empty (out_n , dtype = x .dtype )
251+ y_out = np .empty (out_n , dtype = y .dtype )
252+ x_out [0 ::2 ] = x_mid
253+ x_out [1 ::2 ] = x_mid
254+ y_out [0 ::2 ] = y_max_v
255+ y_out [1 ::2 ] = y_min_v
256+ if trim < n :
257+ x_out = np .concatenate ([x_out , x [trim :]])
258+ y_out = np .concatenate ([y_out , y [trim :]])
259+ return x_out , y_out
214260
215261 def _update_vline (self ) -> None :
216262 """Move the vertical line to ``current_time``."""
217263 if self ._signal_provider is None :
218264 return
219265
220- self ._vline .setValue (self ._signal_provider .current_time )
266+ new_val = self ._signal_provider .current_time
267+ # Skip the update (and the expensive repaint it triggers) when the
268+ # position has not actually changed.
269+ if new_val == getattr (self , '_last_vline_val' , None ):
270+ return
271+ self ._last_vline_val = new_val
272+ self ._vline .setValue (new_val )
221273
222274 def _update_realtime_curves (self ):
223275 """Update all curves with the latest data from the signal provider."""
0 commit comments