11from collections import defaultdict
22
3+ import petab .v1 .C as PETAB_C
34import qtawesome as qta
45from matplotlib import pyplot as plt
56from matplotlib .backends .backend_qtagg import FigureCanvasQTAgg as FigureCanvas
@@ -77,6 +78,42 @@ def __init__(self, parent=None):
7778 self .observable_to_subplot = {}
7879 self .no_plotting_rn = False
7980
81+ # DataFrame caching system for performance optimization
82+ self ._df_cache = {
83+ "measurements" : None ,
84+ "simulations" : None ,
85+ "conditions" : None ,
86+ "visualization" : None ,
87+ }
88+ self ._cache_valid = {
89+ "measurements" : False ,
90+ "simulations" : False ,
91+ "conditions" : False ,
92+ "visualization" : False ,
93+ }
94+
95+ def _invalidate_cache (self , table_name ):
96+ """Invalidate cache for specific table."""
97+ self ._cache_valid [table_name ] = False
98+
99+ def _get_cached_df (self , table_name , proxy_model ):
100+ """Get cached DataFrame or convert if invalid."""
101+ if not self ._cache_valid [table_name ]:
102+ self ._df_cache [table_name ] = proxy_to_dataframe (proxy_model )
103+ self ._cache_valid [table_name ] = True
104+ return self ._df_cache [table_name ]
105+
106+ def _connect_proxy_signals (self , proxy , cache_key ):
107+ """Connect proxy signals for cache invalidation and plotting."""
108+
109+ def on_data_change (* args , ** kwargs ):
110+ self ._invalidate_cache (cache_key )
111+ self ._debounced_plot ()
112+
113+ proxy .dataChanged .connect (on_data_change )
114+ proxy .rowsInserted .connect (on_data_change )
115+ proxy .rowsRemoved .connect (on_data_change )
116+
80117 def initialize (
81118 self , meas_proxy , sim_proxy , cond_proxy , vis_proxy , petab_model
82119 ):
@@ -86,20 +123,19 @@ def initialize(
86123 self .vis_proxy = vis_proxy
87124 self .petab_model = petab_model
88125
89- # Connect data changes
126+ # Clear all cache when reinitializing
127+ for key in self ._cache_valid :
128+ self ._cache_valid [key ] = False
129+
130+ # Connect cache invalidation and data changes
90131 self .options_manager .option_changed .connect (self ._debounced_plot )
91- self .meas_proxy .dataChanged .connect (self ._debounced_plot )
92- self .meas_proxy .rowsInserted .connect (self ._debounced_plot )
93- self .meas_proxy .rowsRemoved .connect (self ._debounced_plot )
94- self .cond_proxy .dataChanged .connect (self ._debounced_plot )
95- self .cond_proxy .rowsInserted .connect (self ._debounced_plot )
96- self .cond_proxy .rowsRemoved .connect (self ._debounced_plot )
97- self .sim_proxy .dataChanged .connect (self ._debounced_plot )
98- self .sim_proxy .rowsInserted .connect (self ._debounced_plot )
99- self .sim_proxy .rowsRemoved .connect (self ._debounced_plot )
100- self .vis_proxy .dataChanged .connect (self ._debounced_plot )
101- self .vis_proxy .rowsInserted .connect (self ._debounced_plot )
102- self .vis_proxy .rowsRemoved .connect (self ._debounced_plot )
132+
133+ # Connect proxy signals for all tables
134+ self ._connect_proxy_signals (self .meas_proxy , "measurements" )
135+ self ._connect_proxy_signals (self .cond_proxy , "conditions" )
136+ self ._connect_proxy_signals (self .sim_proxy , "simulations" )
137+ self ._connect_proxy_signals (self .vis_proxy , "visualization" )
138+
103139 self .visibilityChanged .connect (self ._debounced_plot )
104140
105141 self .plot_it ()
@@ -113,10 +149,11 @@ def plot_it(self):
113149 # If the dock is not visible, do not plot
114150 return
115151
116- measurements_df = proxy_to_dataframe (self .meas_proxy )
117- simulations_df = proxy_to_dataframe (self .sim_proxy )
118- conditions_df = proxy_to_dataframe (self .cond_proxy )
119- visualisation_df = proxy_to_dataframe (self .vis_proxy )
152+ # Use cached DataFrames for performance
153+ measurements_df = self ._get_cached_df ("measurements" , self .meas_proxy )
154+ simulations_df = self ._get_cached_df ("simulations" , self .sim_proxy )
155+ conditions_df = self ._get_cached_df ("conditions" , self .cond_proxy )
156+ visualisation_df = self ._get_cached_df ("visualization" , self .vis_proxy )
120157 group_by = self .options_manager .get_option ()
121158 # group_by different value in petab.visualize
122159 if group_by == "condition" :
@@ -184,6 +221,9 @@ def _render_on_main_thread(self, payload):
184221 self ._update_tabs (fig )
185222
186223 def _update_tabs (self , fig : plt .Figure ):
224+ # Save current tab index before clearing
225+ current_tab_index = self .tab_widget .currentIndex ()
226+
187227 # Clean previous tabs
188228 self .tab_widget .clear ()
189229 # Clear Highlighter
@@ -295,15 +335,19 @@ def _update_tabs(self, fig: plt.Figure):
295335 # Plot residuals if necessary
296336 self .plot_residuals ()
297337
338+ # Restore the previously selected tab (if valid)
339+ if 0 <= current_tab_index < self .tab_widget .count ():
340+ self .tab_widget .setCurrentIndex (current_tab_index )
341+
298342 def highlight_from_selection (
299343 self , selected_rows : list [int ], proxy = None , y_axis_col = "measurement"
300344 ):
301345 proxy = proxy or self .meas_proxy
302346 if not proxy :
303347 return
304348
305- x_axis_col = "time"
306- observable_col = "observableId"
349+ x_axis_col = PETAB_C . TIME
350+ observable_col = PETAB_C . OBSERVABLE_ID
307351
308352 def column_index (name ):
309353 for col in range (proxy .columnCount ()):
@@ -345,7 +389,8 @@ def plot_residuals(self):
345389 return
346390
347391 problem = self .petab_model .current_petab_problem
348- simulations_df = proxy_to_dataframe (self .sim_proxy )
392+ # Reuse cached DataFrame instead of converting again
393+ simulations_df = self ._get_cached_df ("simulations" , self .sim_proxy )
349394
350395 if simulations_df .empty :
351396 return
@@ -521,8 +566,13 @@ def __init__(self, canvas, parent):
521566 self .addWidget (self .settings_btn )
522567
523568 def update_checked_state (self , selected_option ):
524- for action in self .groupy_by_options .values ():
525- action .setChecked (action .text () == f"Groupy by { selected_option } " )
569+ for grp , action in self .groupy_by_options .items ():
570+ if grp == "vis_df" :
571+ action .setChecked (selected_option == "vis_df" )
572+ else :
573+ action .setChecked (
574+ action .text () == f"Group by { selected_option } "
575+ )
526576
527577
528578def create_plot_tab (
0 commit comments