11import numpy as np
22import matplotlib .pyplot as plt
33from matplotlib import cm
4- from matplotlib .widgets import Button
4+ from matplotlib .widgets import Button , Slider
55import warnings
66warnings .simplefilter ('always' )
77
88color_cycle = plt .cm .tab10 .colors [:10 ]
99
10- def eprplot (eprdata_list , plot_type = 'stacked' , slices = 'all' , spacing = 0.5 ,plot_imag = True ,g_scale = False ):
10+ def eprplot (eprdata_list , plot_type = 'stacked' ,
11+ slices = 'all' , spacing = 0.5 ,
12+ plot_imag = True ,g_scale = False , interactive = False ):
1113
1214 """
1315 Plot one or multiple EPR data objects for visualization and comparison.
@@ -76,16 +78,20 @@ def eprplot(eprdata_list, plot_type='stacked', slices='all', spacing=0.5,plot_im
7678 elif ndim == 2 :
7779 fig ,ax = plot_2d (eprdata_list ,g_scale ,plot_type , slices , spacing )
7880
79- fig .set_size_inches ((4 ,3 ))
80- fig .tight_layout ()
81+ if plot_type not in ['slider' ,'surf' ]:
82+ fig .set_size_inches ((4 ,3 ))
83+ fig .tight_layout ()
84+
8185 if g_scale and eprdata_list [0 ].g is not None :
8286 ax .invert_xaxis ()
8387
88+ if interactive :
89+ data_cursor (fig )
90+
8491 plt .show ()
8592
8693 return fig ,ax
8794
88-
8995def plot_1d (eprdata_list ,g_scale ,plot_imag = True ):
9096
9197 """
@@ -142,7 +148,7 @@ def plot_1d(eprdata_list,g_scale,plot_imag=True):
142148 x = eprdata .x
143149
144150 if np .iscomplexobj (data ):
145- ax .plot (x ,np .real (data ), label = f 'Real' ,color = color_cycle [c_idx ])
151+ ax .plot (x ,np .real (data ), label = 'Real' ,color = color_cycle [c_idx ])
146152 if plot_imag :
147153 ax .plot (x ,np .imag (data ), '--' , alpha = 0.5 , label = 'Imaginary' ,color = color_cycle [c_idx ])
148154 else :
@@ -225,7 +231,7 @@ def plot_2d(eprdata_list,g_scale,plot_type='stacked',slices='all', spacing=0.5):
225231 # Set color scheme for slices
226232 num_selected_slices = len (selected_slices )
227233
228- slice_colors = cm .winter (np .linspace (0 , 1 , num_selected_slices )) # Gradual colors for larger slices
234+ slice_colors = cm .winter (np .linspace (0 , 1 , num_selected_slices ))
229235
230236 if plot_type == 'surf' :
231237 fig ,ax = surf_plot (data ,x ,y ,slice_len ,selected_slices )
@@ -235,7 +241,9 @@ def plot_2d(eprdata_list,g_scale,plot_type='stacked',slices='all', spacing=0.5):
235241 fig ,ax = stack_plot (data ,x ,y ,selected_slices ,slice_colors ,spacing )
236242 elif plot_type == 'pcolor' :
237243 fig ,ax = pcolor_plot (data ,x ,y ,slice_len ,selected_slices )
238-
244+ elif plot_type == 'slider' :
245+ fig ,ax = slider_plot (data ,x ,y ,slice_len ,selected_slices )
246+
239247 return fig ,ax
240248
241249def stack_plot (data ,x ,y ,selected_slices ,slice_colors ,spacing ):
@@ -317,7 +325,7 @@ def surf_plot(data,x,y,slice_len,selected_slices):
317325 fig ,ax = plt .subplots (subplot_kw = {"projection" : "3d" })
318326 X , Y = np .meshgrid (x [range (slice_len )], y [selected_slices ])
319327 Z = np .real (data [selected_slices , :]) if np .iscomplexobj (data ) else data [selected_slices , :]
320- surf = ax .plot_surface (X , Y , Z , cmap = 'viridis' )
328+ surf = ax .plot_surface (X , Y , Z , cmap = 'jet' , rstride = 1 , cstride = 1 )
321329 fig .colorbar (surf , ax = ax , shrink = 0.5 , aspect = 10 )
322330
323331 return fig ,ax
@@ -404,6 +412,125 @@ def pcolor_plot(data,x,y,slice_len,selected_slices):
404412
405413 return fig ,ax
406414
415+
416+ def slider_plot (data , x , y , slice_len , selected_slices ):
417+
418+ """
419+ Create a plot for selected slices of 2D EPR data with a slider.
420+
421+ Parameters
422+ ----------
423+ data : ndarray
424+ The 2D data array from which the slices will be plotted.
425+ x : ndarray
426+ The x-axis values corresponding to the data.
427+ y : ndarray
428+ The y-axis values corresponding to the data.
429+ slice_len : int
430+ The length of each slice along the x-axis.
431+ selected_slices : list of int
432+ A list of slice indices to be plotted.
433+
434+ Returns
435+ -------
436+ fig : matplotlib.figure.Figure
437+ The figure object containing the pseudocolor plot.
438+ ax : matplotlib.axes._axes.Axes
439+ The axes object of the plot.
440+
441+ """
442+
443+ fig , ax = plt .subplots (figsize = (8 , 6 ))
444+ plt .subplots_adjust (left = 0.15 , bottom = 0.3 )
445+
446+ X , Y = x [:slice_len ], y [selected_slices ]
447+ Z = np .real (data [selected_slices , :]) if np .iscomplexobj (data ) else data [selected_slices , :]
448+
449+ m , n = Z .shape
450+ swap_axes = [False ]
451+ current_x_lim = None
452+
453+ # Initial plot (first row)
454+ line , = ax .plot (X , Z [0 , :])
455+ ax .set_ylim (Z .min (), Z .max ())
456+ ax .autoscale (False )
457+
458+ # Slider
459+ ax_slider = plt .axes ([0.15 , 0.15 , 0.7 , 0.03 ])
460+ slider = Slider (ax_slider , "Index" , 0 , m - 1 , valinit = 0 , valstep = 1 ,handle_style = {'facecolor' :'red' ,'size' :15 },track_color = 'black' )
461+ fig .slider = slider
462+
463+ # Swap Axes Button
464+ ax_button = plt .axes ([0.15 , 0.07 , 0.2 , 0.05 ])
465+ button = Button (ax_button , "Swap Axes" )
466+ fig .button = button
467+
468+ # Value label
469+ ax_label = plt .axes ([0.4 , 0.07 , 0.3 , 0.05 ])
470+ ax_label .set_xticks ([])
471+ ax_label .set_yticks ([])
472+ label_text = ax_label .annotate (
473+ f"{ Y [0 ]} " , xy = (0.5 , 0.5 ), ha = "center" , va = "center" , fontsize = 10
474+ )
475+
476+ def update (val ):
477+ """Updates the plot based on slider value and axis mode."""
478+ index = int (slider .val )
479+ is_row_mode = not swap_axes [0 ]
480+
481+ if is_row_mode :
482+ x_data = X
483+ y_data = Z [index , :]
484+ ax .set_xlim (min (X ), max (X ))
485+ label_text .set_text (f"{ Y [index ]} " )
486+ else :
487+ x_data = Y
488+ y_data = Z [:, index ]
489+ ax .set_xlim (min (Y ), max (Y ))
490+ label_text .set_text (f"{ X [index ]} " )
491+
492+ if current_x_lim :
493+ ax .set_xlim (current_x_lim )
494+ else :
495+ ax .autoscale (False )
496+
497+ line .set_xdata (x_data )
498+ line .set_ydata (y_data )
499+
500+ fig .canvas .draw_idle ()
501+
502+ def swap_axes_clicked (event ):
503+ """Swaps row/column mode and updates slider limits."""
504+ swap_axes [0 ] = not swap_axes [0 ]
505+ new_max = n - 1 if swap_axes [0 ] else m - 1
506+
507+ slider .valmax = new_max
508+ slider .ax .set_xlim (slider .valmin , slider .valmax )
509+ slider .set_val (slider .valmin )
510+ update (0 ) # Force plot update
511+
512+ if swap_axes [0 ]:
513+ ax .set_xlim (min (Y ), max (Y ))
514+ else :
515+ ax .set_xlim (min (X ), max (X ))
516+
517+ fig .canvas .draw_idle ()
518+
519+ def on_zoom (event ):
520+ """Handles zooming by the user."""
521+ nonlocal current_x_lim
522+ current_x_lim = ax .get_xlim ()
523+
524+ # Attach event listeners
525+ slider .on_changed (update )
526+ button .on_clicked (swap_axes_clicked )
527+
528+ # Add zoom event listener
529+ fig .canvas .mpl_connect ('button_release_event' , on_zoom )
530+
531+ return fig , ax
532+
533+
407534def interactive_points_selector (x ,y ):
408535
409536 """
@@ -457,3 +584,105 @@ def done(event):
457584 selected_points_sorted = sorted (selected_points , key = lambda idx : x [idx ])
458585
459586 return np .unique (np .array (selected_points_sorted , dtype = int ))
587+
588+ def data_cursor (fig = None ):
589+
590+ """
591+ Adds an interactive data cursor to a Matplotlib figure.
592+
593+ This function enables a crosshair cursor that tracks mouse movement
594+ within a given figure and displays the current x and y coordinates.
595+ Additionally, it supports measuring horizontal distances between
596+ two x-coordinates using right-click dragging or Ctrl + Left Click.
597+
598+ Parameters
599+ ----------
600+ fig : matplotlib.figure.Figure, optional
601+ The Matplotlib figure to which the data cursor will be added.
602+ If None, the current active figure (`plt.gcf()`) is used.
603+
604+ Notes
605+ -----
606+ - A red dashed crosshair follows the mouse position.
607+ - Clicking (Left Click) sets a reference vertical line.
608+ - Right Click or Ctrl + Left Click enables measuring horizontal distance.
609+ - The measured distance (ΔX) is displayed in the bottom-left of the figure.
610+ - The reference vertical line is removed on releasing the mouse button.
611+ """
612+
613+ if fig is None :
614+ fig = plt .gcf ()
615+
616+ ax = fig .gca ()
617+ xlim = ax .get_xlim ()
618+ ylim = ax .get_ylim ()
619+
620+ h_line , = ax .plot ([xlim [0 ], xlim [1 ]], [(ylim [0 ] + ylim [1 ]) / 2 ] * 2 , 'r--' , lw = 1 )
621+ v_line , = ax .plot ([(xlim [0 ] + xlim [1 ]) / 2 ] * 2 , [ylim [0 ], ylim [1 ]], 'r--' , lw = 1 )
622+
623+ coord_text = ax .text (0.02 , 0.95 , '' , transform = ax .transAxes , fontsize = 10 , color = 'red' )
624+ dist_text = ax .text (0.02 , 0.02 , '' , transform = ax .transAxes , fontsize = 10 , color = 'blue' )
625+ ref_v_line , = ax .plot ([], [], 'b-' , lw = 1 )
626+ drag_v_line , = ax .plot ([], [], 'b-' , lw = 1 )
627+
628+ ref_x = None
629+ ctrl_pressed = False
630+
631+ def on_mouse_move (event ):
632+ nonlocal ref_x
633+ if event .inaxes :
634+ x , y = event .xdata , event .ydata
635+ h_line .set_ydata ([y , y ])
636+ v_line .set_xdata ([x , x ])
637+ coord_text .set_text (f'X: { x :.2f} , Y: { y :.2f} ' )
638+
639+ # Handling dragging for right-click OR Ctrl + Left Click
640+ if (event .button == 3 or (ctrl_pressed and event .button is None )):
641+ if ref_x is None :
642+ ref_x = x
643+ ref_v_line .set_xdata ([ref_x , ref_x ])
644+ ref_v_line .set_ydata (ax .get_ylim ())
645+
646+ dist_text .set_text (f'ΔX: { abs (x - ref_x ):.2f} ' )
647+ drag_v_line .set_xdata ([x , x ])
648+ drag_v_line .set_ydata (ax .get_ylim ())
649+ else :
650+ dist_text .set_text ('' )
651+
652+ fig .canvas .draw_idle ()
653+
654+ def on_mouse_press (event ):
655+ nonlocal ref_x
656+ if event .button == 1 and event .inaxes and not ctrl_pressed :
657+ ref_x = event .xdata
658+ ref_v_line .set_xdata ([ref_x , ref_x ])
659+ ref_v_line .set_ydata (ax .get_ylim ())
660+
661+ def on_mouse_release (event ):
662+ nonlocal ref_x
663+ if event .button == 1 or not ctrl_pressed :
664+ ref_x = None
665+ ref_v_line .set_xdata ([])
666+ ref_v_line .set_ydata ([])
667+ drag_v_line .set_xdata ([])
668+ drag_v_line .set_ydata ([])
669+ dist_text .set_text ('' )
670+ fig .canvas .draw_idle ()
671+
672+ def on_key_press (event ):
673+ nonlocal ctrl_pressed
674+ if event .key == 'control' :
675+ ctrl_pressed = True
676+
677+ def on_key_release (event ):
678+ nonlocal ctrl_pressed
679+ if event .key == 'control' :
680+ ctrl_pressed = False
681+
682+ fig .canvas .mpl_connect ('motion_notify_event' , on_mouse_move )
683+ fig .canvas .mpl_connect ('button_press_event' , on_mouse_press )
684+ fig .canvas .mpl_connect ('button_release_event' , on_mouse_release )
685+ fig .canvas .mpl_connect ('key_press_event' , on_key_press )
686+ fig .canvas .mpl_connect ('key_release_event' , on_key_release )
687+
688+ plt .show ()
0 commit comments