@@ -586,8 +586,13 @@ def create_pyqtgraph_section(self) -> QWidget:
586586 # Style CPU/GPU plot - minimal padding
587587 self .cpu_gpu_plot .setBackground (self .color_scheme .to_hex (self .color_scheme .panel_bg ))
588588 self .cpu_gpu_plot .setYRange (0 , 100 )
589- self .cpu_gpu_plot .setXRange (0 , self .monitor_config .history_duration_seconds )
589+ # X axis shows "seconds ago", so range is (-history, 0) with 0 = now (right edge)
590+ self .cpu_gpu_plot .setXRange (- self .monitor_config .history_duration_seconds , 0 )
590591 self .cpu_gpu_plot .showGrid (x = self .monitor_config .show_grid , y = self .monitor_config .show_grid , alpha = 0.3 )
592+ # Disable auto-ranging so manual panning works reliably
593+ _cpu_vb = self .cpu_gpu_plot .getPlotItem ().getViewBox ()
594+ _cpu_vb .setAutoPan (x = False , y = False )
595+ _cpu_vb .disableAutoRange ()
591596
592597 # Minimize left axis
593598 self .cpu_gpu_plot .getAxis ('left' ).setTextPen ('white' )
@@ -605,8 +610,13 @@ def create_pyqtgraph_section(self) -> QWidget:
605610 # Style RAM/VRAM plot - minimal padding
606611 self .ram_vram_plot .setBackground (self .color_scheme .to_hex (self .color_scheme .panel_bg ))
607612 self .ram_vram_plot .setYRange (0 , 100 )
608- self .ram_vram_plot .setXRange (0 , self .monitor_config .history_duration_seconds )
613+ # X axis shows "seconds ago", so range is (-history, 0) with 0 = now (right edge)
614+ self .ram_vram_plot .setXRange (- self .monitor_config .history_duration_seconds , 0 )
609615 self .ram_vram_plot .showGrid (x = self .monitor_config .show_grid , y = self .monitor_config .show_grid , alpha = 0.3 )
616+ # Disable auto-ranging so manual panning works reliably
617+ _ram_vb = self .ram_vram_plot .getPlotItem ().getViewBox ()
618+ _ram_vb .setAutoPan (x = False , y = False )
619+ _ram_vb .disableAutoRange ()
610620
611621 # Minimize left axis
612622 self .ram_vram_plot .getAxis ('left' ).setTextPen ('white' )
@@ -889,56 +899,123 @@ def _get_interpolated_metrics(self) -> Optional[dict]:
889899 def update_pyqtgraph_plots (self , metrics : Optional [dict ] = None ):
890900 """Update consolidated PyQtGraph plots with current data - non-blocking and fast."""
891901 try :
892- # Convert data point indices to time values in seconds
893902 data_length = len (self .monitor .cpu_history )
894903 if data_length == 0 :
895904 return
896905
897- # Create time axis: each data point represents update_interval_seconds
898- update_interval = self .monitor_config .update_interval_seconds
899- x_time = [i * update_interval for i in range (data_length )]
900-
901- # Get current data
902- cpu_data = list (self .monitor .cpu_history )
903- ram_data = list (self .monitor .ram_history )
904- gpu_data = list (self .monitor .gpu_history )
905- vram_data = list (self .monitor .vram_history )
906-
907- # Update CPU/GPU consolidated plot
908- self .cpu_curve .setData (x_time , cpu_data )
909-
910- # Handle GPU data (may not be available)
911- if any (gpu_data ):
912- self .gpu_curve .setData (x_time , gpu_data )
906+ import numpy as _np
907+
908+ update_interval = float (self .monitor_config .update_interval_seconds )
909+ history = float (self .monitor_config .history_duration_seconds )
910+ now = time .time ()
911+
912+ # Snapshot histories
913+ cpu_hist = _np .asarray (self .monitor .cpu_history , dtype = _np .float32 )
914+ ram_hist = _np .asarray (self .monitor .ram_history , dtype = _np .float32 )
915+ gpu_hist = _np .asarray (self .monitor .gpu_history , dtype = _np .float32 )
916+ vram_hist = _np .asarray (self .monitor .vram_history , dtype = _np .float32 )
917+ ts = _np .asarray (self .monitor .time_stamps , dtype = _np .float64 )
918+
919+ if update_interval <= 0 :
920+ update_interval = 0.2
921+
922+ # The timestamp deque is prefilled with zeros. Fill those zeros with
923+ # synthetic timestamps spaced by update_interval so the plot is
924+ # visible immediately.
925+ nz = _np .nonzero (ts > 0 )[0 ]
926+ if nz .size :
927+ last_i = int (nz [- 1 ])
928+ # Fill backwards
929+ for i in range (last_i - 1 , - 1 , - 1 ):
930+ if ts [i ] <= 0 :
931+ ts [i ] = ts [i + 1 ] - update_interval
932+ # Fill forwards (rare, but keep monotonic)
933+ for i in range (last_i + 1 , data_length ):
934+ if ts [i ] <= 0 :
935+ ts [i ] = ts [i - 1 ] + update_interval
913936 else :
914- self .gpu_curve .setData ([], []) # Clear data
937+ idx = _np .arange (data_length , dtype = _np .float64 )
938+ ts = now - (data_length - 1 - idx ) * update_interval
939+
940+ # Convert to relative x in seconds (negative = older; 0 = now)
941+ base_x = ts - now
942+
943+ # Densify tail from newest sample -> now so x scrolls continuously
944+ try :
945+ rf = float (getattr (self .monitor_config , "render_fps" , 60.0 ))
946+ uf = float (getattr (self .monitor_config , "update_fps" , 5.0 ))
947+ subdivisions = max (4 , int (min (64 , round (rf / max (1.0 , uf )))))
948+ except Exception :
949+ subdivisions = 8
950+
951+ def _series (hist : _np .ndarray , key : str ) -> tuple [_np .ndarray , _np .ndarray ]:
952+ if data_length == 1 :
953+ x0 = float (base_x [- 1 ])
954+ y0 = float (hist [- 1 ])
955+ y1 = float (metrics .get (key , y0 )) if metrics is not None else y0
956+ x_tail = _np .linspace (x0 , 0.0 , subdivisions , dtype = _np .float64 )
957+ y_tail = _np .linspace (y0 , y1 , subdivisions , dtype = _np .float32 )
958+ return x_tail , y_tail
959+
960+ x0 = float (base_x [- 1 ])
961+ y0 = float (hist [- 1 ])
962+ y1 = float (metrics .get (key , y0 )) if metrics is not None else y0
963+ x_tail = _np .linspace (x0 , 0.0 , subdivisions , dtype = _np .float64 )
964+ y_tail = _np .linspace (y0 , y1 , subdivisions , dtype = _np .float32 )
965+ x_full = _np .concatenate ((base_x [:- 1 ], x_tail ))
966+ y_full = _np .concatenate ((hist [:- 1 ], y_tail ))
967+ return x_full , y_full
968+
969+ x_cpu , y_cpu = _series (cpu_hist , "cpu_percent" )
970+ _ , y_gpu = _series (gpu_hist , "gpu_percent" )
971+ _ , y_ram = _series (ram_hist , "ram_percent" )
972+ _ , y_vram = _series (vram_hist , "vram_percent" )
973+
974+ # Keep x range fixed to the last `history` seconds.
975+ try :
976+ self .cpu_gpu_plot .setXRange (- history , 0.0 , padding = 0 )
977+ self .ram_vram_plot .setXRange (- history , 0.0 , padding = 0 )
978+ except Exception :
979+ pass
980+
981+ self .cpu_curve .setData (x_cpu , y_cpu )
982+ if _np .any (y_gpu ):
983+ self .gpu_curve .setData (x_cpu , y_gpu )
984+ else :
985+ self .gpu_curve .setData ([], [])
915986
916987 # Update CPU/GPU plot title with current values
917- if metrics :
918- cpu_status = f"{ metrics .get ('cpu_percent' , 0.0 ):.1f} %"
919- gpu_status = f"{ metrics .get ('gpu_percent' , 0.0 ):.1f} %" if any (gpu_data ) else "Not Available"
988+ if metrics is not None :
989+ cpu_status = f"{ float (metrics .get ('cpu_percent' , 0.0 )):.1f} %"
990+ if _np .any (y_gpu ):
991+ gpu_status = f"{ float (metrics .get ('gpu_percent' , 0.0 )):.1f} %"
992+ else :
993+ gpu_status = "Not Available"
920994 else :
921- cpu_status = f' { cpu_data [- 1 ]:.1f} %' if cpu_data else ' N/A'
922- gpu_status = f' { gpu_data [- 1 ]:.1f} %' if gpu_data else ' Not Available'
923- self .cpu_gpu_plot .setTitle (f' CPU: { cpu_status } , GPU: { gpu_status } ' )
995+ cpu_status = f" { float ( y_cpu [- 1 ]) if y_cpu . size else 0.0 :.1f} %" if y_cpu . size else " N/A"
996+ gpu_status = f" { float ( y_gpu [- 1 ]) if y_gpu . size else 0.0 :.1f} %" if _np . any ( y_gpu ) else " Not Available"
997+ self .cpu_gpu_plot .setTitle (f" CPU: { cpu_status } , GPU: { gpu_status } " )
924998
925- # Update RAM/VRAM consolidated plot
926- self .ram_curve .setData (x_time , ram_data )
999+ # Update RAM/VRAM consolidated plot using densified arrays
1000+ self .ram_curve .setData (x_cpu , y_ram )
9271001
9281002 # Handle VRAM data (may not be available)
929- if any (vram_data ):
930- self .vram_curve .setData (x_time , vram_data )
1003+ if _np . any (y_vram ):
1004+ self .vram_curve .setData (x_cpu , y_vram )
9311005 else :
9321006 self .vram_curve .setData ([], []) # Clear data
9331007
9341008 # Update RAM/VRAM plot title with current values
935- if metrics :
936- ram_status = f"{ metrics .get ('ram_percent' , 0.0 ):.1f} %"
937- vram_status = f"{ metrics .get ('vram_percent' , 0.0 ):.1f} %" if any (vram_data ) else "Not Available"
1009+ if metrics is not None :
1010+ ram_status = f"{ float (metrics .get ('ram_percent' , 0.0 )):.1f} %"
1011+ if _np .any (y_vram ):
1012+ vram_status = f"{ float (metrics .get ('vram_percent' , 0.0 )):.1f} %"
1013+ else :
1014+ vram_status = "Not Available"
9381015 else :
939- ram_status = f' { ram_data [- 1 ]:.1f} %' if ram_data else ' N/A'
940- vram_status = f' { vram_data [- 1 ]:.1f} %' if vram_data else ' Not Available'
941- self .ram_vram_plot .setTitle (f' RAM: { ram_status } , VRAM: { vram_status } ' )
1016+ ram_status = f" { float ( y_ram [- 1 ]) if y_ram . size else 0.0 :.1f} %" if y_ram . size else " N/A"
1017+ vram_status = f" { float ( y_vram [- 1 ]) if y_vram . size else 0.0 :.1f} %" if _np . any ( y_vram ) else " Not Available"
1018+ self .ram_vram_plot .setTitle (f" RAM: { ram_status } , VRAM: { vram_status } " )
9421019
9431020 except Exception as e :
9441021 logger .warning (f"Failed to update PyQtGraph plots: { e } " )
0 commit comments