@@ -256,7 +256,7 @@ def __init__(self, root):
256256 self .logo_image = None # Attribute to hold the logo image reference
257257 self .data_queue = queue .Queue ()
258258 self .measurement_thread = None
259- self .plot_backgrounds = None # For blitting
259+ self ._plot_dirty = False
260260
261261 self .setup_styles ()
262262 self .create_widgets ()
@@ -694,7 +694,7 @@ def create_graph_frame(self, parent):
694694 self .ax_sub2 = self .figure .add_subplot (
695695 gs [1 , 1 ]) # Temp vs Time has its own X-axis
696696 self .line_main , = self .ax_main .plot (
697- [], [], color = self .CLR_ACCENT_RED , marker = 'o' , markersize = 3 , linestyle = '-' , animated = True )
697+ [], [], color = self .CLR_ACCENT_RED , marker = 'o' , markersize = 3 , linestyle = '-' )
698698 self .ax_main .set_title ("Resistance vs. Temperature" , fontweight = 'bold' )
699699 self .ax_main .set_ylabel ("Resistance (Ω)" )
700700 if self .log_scale_var .get ():
@@ -703,35 +703,22 @@ def create_graph_frame(self, parent):
703703 self .ax_main .set_yscale ('linear' )
704704 self .ax_main .grid (True , which = "both" , linestyle = '--' , alpha = 0.6 )
705705 self .line_sub1 , = self .ax_sub1 .plot (
706- [], [], color = self .CLR_ACCENT_GOLD , marker = '.' , markersize = 3 , linestyle = '-' , animated = True )
706+ [], [], color = self .CLR_ACCENT_GOLD , marker = '.' , markersize = 3 , linestyle = '-' )
707707 self .ax_sub1 .set_xlabel ("Temperature (K)" )
708708 self .ax_sub1 .set_ylabel ("Current (A)" )
709709 self .ax_sub1 .grid (True , linestyle = '--' , alpha = 0.6 )
710710 self .line_sub2 , = self .ax_sub2 .plot (
711- [], [], color = self .CLR_ACCENT_GREEN , marker = '.' , markersize = 3 , linestyle = '-' , animated = True )
711+ [], [], color = self .CLR_ACCENT_GREEN , marker = '.' , markersize = 3 , linestyle = '-' )
712712 self .ax_sub2 .set_xlabel ("Time (s)" )
713713 self .ax_sub2 .set_ylabel ("Temperature (K)" )
714714 self .ax_sub2 .grid (True , linestyle = '--' , alpha = 0.6 )
715715 self .figure .tight_layout (pad = 3.0 )
716716 self .canvas .get_tk_widget ().pack (fill = tk .BOTH , expand = True , padx = 5 , pady = 5 )
717717
718718 def _update_y_scale (self ):
719- if self .log_scale_var .get ():
720- self .ax_main .set_yscale ('log' )
721- else :
722- self .ax_main .set_yscale ('linear' )
723- # If the measurement is running, we need to redraw and recapture the
724- # background
725- if self .is_running and self .plot_backgrounds :
726- self .canvas .draw ()
727- self .plot_backgrounds = [
728- self .canvas .copy_from_bbox (
729- ax .bbox ) for ax in [
730- self .ax_main ,
731- self .ax_sub1 ,
732- self .ax_sub2 ]]
733- else :
734- self .canvas .draw_idle ()
719+ self .ax_main .set_yscale ('log' if self .log_scale_var .get () else 'linear' )
720+ self ._plot_dirty = True # force a rescale on next refresh
721+ self .canvas .draw_idle ()
735722
736723 def log (self , message ):
737724 timestamp = datetime .now ().strftime ("%H:%M:%S" )
@@ -789,17 +776,7 @@ def start_measurement(self):
789776 f"R-T Curve: { params ['sample_name' ]} " ,
790777 fontweight = 'bold' )
791778
792- # --- MODIFIED: Setup for blitting ---
793- self .canvas .draw () # Full draw to prepare background
794- self .plot_backgrounds = [
795- self .canvas .copy_from_bbox (
796- ax .bbox ) for ax in [
797- self .ax_main ,
798- self .ax_sub1 ,
799- self .ax_sub2 ]]
800- for line in [self .line_main , self .line_sub1 , self .line_sub2 ]:
801- line .set_animated (True )
802- self .log ("Blitting enabled for fast graph updates." )
779+ self .canvas .draw_idle ()
803780
804781 self .log ("Starting passive data logging..." )
805782 self .start_time = time .time ()
@@ -808,6 +785,7 @@ def start_measurement(self):
808785 target = self ._measurement_worker , daemon = True )
809786 self .measurement_thread .start ()
810787 self .root .after (100 , self ._process_data_queue )
788+ self .root .after (250 , self ._refresh_plot )
811789
812790 except Exception as e :
813791 self .log (f"ERROR during startup: { traceback .format_exc ()} " )
@@ -819,10 +797,6 @@ def stop_measurement(self, from_user=True):
819797 if self .is_running :
820798 self .is_running = False
821799 self .log ("Measurement stopped by user." )
822- # --- MODIFIED: Disable blitting on stop ---
823- for line in [self .line_main , self .line_sub1 , self .line_sub2 ]:
824- line .set_animated (False )
825- self .plot_backgrounds = None
826800 self .canvas .draw_idle ()
827801 self .start_button .config (state = 'normal' )
828802 self .stop_button .config (state = 'disabled' )
@@ -872,45 +846,76 @@ def _process_data_queue(self):
872846 self .data_storage ['temperature' ].append (temp )
873847 self .data_storage ['current' ].append (cur )
874848 self .data_storage ['resistance' ].append (res )
875-
876- # --- MODIFIED: Use blitting for fast graph updates ---
877- if self .plot_backgrounds :
878- # Restore the clean backgrounds
879- for bg in self .plot_backgrounds :
880- self .canvas .restore_region (bg )
881-
882- # Update data for all lines
883- self .line_main .set_data (
884- self .data_storage ['temperature' ],
885- self .data_storage ['resistance' ])
886- self .line_sub1 .set_data (
887- self .data_storage ['temperature' ],
888- self .data_storage ['current' ])
889- self .line_sub2 .set_data (
890- self .data_storage ['time' ],
891- self .data_storage ['temperature' ])
892-
893- # Redraw only the artists and blit the changes
894- for ax , line in zip ([self .ax_main , self .ax_sub1 , self .ax_sub2 ], [
895- self .line_main , self .line_sub1 , self .line_sub2 ]):
896- ax .relim ()
897- ax .autoscale_view ()
898- ax .draw_artist (line )
899-
900- self .canvas .blit (self .figure .bbox )
901- else : # Fallback to full redraw if blitting isn't ready
902- for ax in [self .ax_main , self .ax_sub1 , self .ax_sub2 ]:
903- ax .relim ()
904- ax .autoscale_view ()
905- self .figure .tight_layout (pad = 3.0 )
906- self .canvas .draw_idle ()
849+ # Mark that the plot needs a refresh; actual redraw is
850+ # decoupled and throttled (see _refresh_plot).
851+ self ._plot_dirty = True
907852
908853 except queue .Empty :
909854 pass
910855
911856 if self .is_running :
912857 self .root .after (200 , self ._process_data_queue )
913858
859+ def _refresh_plot (self ):
860+ """Redraws the plots at a fixed cadence, independent of data rate.
861+
862+ A normal (non-blitted) draw is used so that the axes — ticks,
863+ limits, gridlines and scale — always stay in sync with the data.
864+ """
865+ if self ._plot_dirty :
866+ self ._plot_dirty = False
867+
868+ temps = self .data_storage ['temperature' ]
869+ res = self .data_storage ['resistance' ]
870+ cur = self .data_storage ['current' ]
871+ t = self .data_storage ['time' ]
872+
873+ self .line_main .set_data (temps , res )
874+ self .line_sub1 .set_data (temps , cur )
875+ self .line_sub2 .set_data (t , temps )
876+
877+ # Recompute and apply limits on every axis.
878+ self ._autoscale_axis (self .ax_main , x = temps , y = res ,
879+ log_y = self .log_scale_var .get ())
880+ self ._autoscale_axis (self .ax_sub1 , x = temps , y = cur )
881+ self ._autoscale_axis (self .ax_sub2 , x = t , y = temps )
882+
883+ # Full redraw keeps ticks/labels/gridlines correct and is
884+ # resize-proof. draw_idle() coalesces redraws efficiently.
885+ self .canvas .draw_idle ()
886+
887+ if self .is_running :
888+ self .root .after (250 , self ._refresh_plot )
889+
890+ def _autoscale_axis (self , ax , x , y , log_y = False , margin = 0.05 ):
891+ """Rescale an axis, ignoring non-finite and (for log) non-positive
892+ values so the axis never collapses or freezes."""
893+ import math
894+
895+ xs = [v for v in x if v is not None and math .isfinite (v )]
896+ if log_y :
897+ ys = [v for v in y if v is not None and math .isfinite (v ) and v > 0 ]
898+ else :
899+ ys = [v for v in y if v is not None and math .isfinite (v )]
900+
901+ if not xs or not ys :
902+ return
903+
904+ xmin , xmax = min (xs ), max (xs )
905+ ymin , ymax = min (ys ), max (ys )
906+
907+ # X padding (linear)
908+ xpad = (xmax - xmin ) * margin or 0.5
909+ ax .set_xlim (xmin - xpad , xmax + xpad )
910+
911+ # Y padding
912+ if log_y :
913+ # pad multiplicatively in log space
914+ ax .set_ylim (ymin / (1 + margin ), ymax * (1 + margin ))
915+ else :
916+ ypad = (ymax - ymin ) * margin or abs (ymax ) * margin or 1e-12
917+ ax .set_ylim (ymin - ypad , ymax + ypad )
918+
914919 def _scan_for_visa_instruments (self ):
915920 if not pyvisa :
916921 self .log ("ERROR: PyVISA is not installed." )
0 commit comments