2222
2323from ..models import PEtabModel
2424from ..settings_manager import SettingsDialog , settings_manager
25- from ..utils import CaptureLogHandler , process_file
25+ from ..utils import (
26+ CaptureLogHandler ,
27+ get_selected ,
28+ process_file ,
29+ )
2630from ..views import TaskBar
2731from .logger_controller import LoggerController
2832from .sbml_controller import SbmlController
3135 MeasurementController ,
3236 ObservableController ,
3337 ParameterController ,
38+ VisualizationController ,
3439)
3540from .utils import (
3641 RecentFilesManager ,
@@ -91,6 +96,20 @@ def __init__(self, view, model: PEtabModel):
9196 self .undo_stack ,
9297 self ,
9398 )
99+ self .visualization_controller = VisualizationController (
100+ self .view .visualization_dock ,
101+ self .model .visualization ,
102+ self .logger ,
103+ self .undo_stack ,
104+ self ,
105+ )
106+ self .simulation_controller = MeasurementController (
107+ self .view .simulation_dock ,
108+ self .model .simulation ,
109+ self .logger ,
110+ self .undo_stack ,
111+ self ,
112+ )
94113 self .sbml_controller = SbmlController (
95114 self .view .sbml_viewer , self .model .sbml , self .logger , self
96115 )
@@ -100,6 +119,8 @@ def __init__(self, view, model: PEtabModel):
100119 self .parameter_controller ,
101120 self .condition_controller ,
102121 self .sbml_controller ,
122+ self .visualization_controller ,
123+ self .simulation_controller ,
103124 ]
104125 # Recent Files
105126 self .recent_files_manager = RecentFilesManager (max_files = 10 )
@@ -109,6 +130,8 @@ def __init__(self, view, model: PEtabModel):
109130 "observable" : False ,
110131 "parameter" : False ,
111132 "condition" : False ,
133+ "visualization" : False ,
134+ "simulation" : False ,
112135 }
113136 self .sbml_checkbox_states = {"sbml" : False , "antimony" : False }
114137 self .unsaved_changes = False
@@ -120,13 +143,15 @@ def __init__(self, view, model: PEtabModel):
120143 self .setup_connections ()
121144 self .setup_task_bar ()
122145 self .setup_context_menu ()
146+ self .plotter = None
147+ self .init_plotter ()
123148
124149 def setup_context_menu (self ):
125150 """Sets up context menus for the tables."""
126- self .measurement_controller . setup_context_menu ( self . actions )
127- self .observable_controller . setup_context_menu ( self . actions )
128- self . parameter_controller . setup_context_menu ( self . actions )
129- self . condition_controller .setup_context_menu (self .actions )
151+ for controller in self .controllers :
152+ if controller == self .sbml_controller :
153+ continue
154+ controller .setup_context_menu (self .actions )
130155
131156 def setup_task_bar (self ):
132157 """Create shortcuts for the main window."""
@@ -169,9 +194,11 @@ def setup_connections(self):
169194 )
170195 # Maybe Move to a Plot Model
171196 self .view .measurement_dock .table_view .selectionModel ().selectionChanged .connect (
172- self .handle_selection_changed
197+ self ._on_table_selection_changed
198+ )
199+ self .view .simulation_dock .table_view .selectionModel ().selectionChanged .connect (
200+ self ._on_simulation_selection_changed
173201 )
174- self .model .measurement .dataChanged .connect (self .handle_data_changed )
175202 # Unsaved Changes
176203 self .model .measurement .something_changed .connect (
177204 self .unsaved_changes_change
@@ -185,6 +212,12 @@ def setup_connections(self):
185212 self .model .condition .something_changed .connect (
186213 self .unsaved_changes_change
187214 )
215+ self .model .visualization .something_changed .connect (
216+ self .unsaved_changes_change
217+ )
218+ self .model .simulation .something_changed .connect (
219+ self .unsaved_changes_change
220+ )
188221 self .model .sbml .something_changed .connect (self .unsaved_changes_change )
189222 # Visibility
190223 self .sync_visibility_with_actions ()
@@ -198,6 +231,14 @@ def setup_connections(self):
198231 self .sbml_controller .overwritten_model .connect (
199232 self .parameter_controller .update_handler_sbml
200233 )
234+ # overwrite signals
235+ for controller in [
236+ # self.measurement_controller,
237+ self .condition_controller
238+ ]:
239+ controller .overwritten_df .connect (
240+ self .init_plotter
241+ )
201242
202243 def setup_actions (self ):
203244 """Setup actions for the main controller."""
@@ -301,8 +342,9 @@ def setup_actions(self):
301342 self .filter_input .setPlaceholderText ("Filter..." )
302343 filter_layout .addWidget (self .filter_input )
303344 for table_n , table_name in zip (
304- ["m" , "p" , "o" , "c" ],
305- ["measurement" , "parameter" , "observable" , "condition" ],
345+ ["m" , "p" , "o" , "c" , "v" , "s" ],
346+ ["measurement" , "parameter" , "observable" , "condition" ,
347+ "visualization" , "simulation" ],
306348 strict = False ,
307349 ):
308350 tool_button = QToolButton ()
@@ -325,7 +367,8 @@ def setup_actions(self):
325367 self .filter_input .textChanged .connect (self .filter_table )
326368
327369 # show/hide elements
328- for element in ["measurement" , "observable" , "parameter" , "condition" ]:
370+ for element in ["measurement" , "observable" , "parameter" ,
371+ "condition" , "visualization" , "simulation" ]:
329372 actions [f"show_{ element } " ] = QAction (
330373 f"{ element .capitalize ()} Table" , self .view
331374 )
@@ -396,6 +439,8 @@ def sync_visibility_with_actions(self):
396439 "condition" : self .view .condition_dock ,
397440 "logger" : self .view .logger_dock ,
398441 "plot" : self .view .plot_dock ,
442+ "visualization" : self .view .visualization_dock ,
443+ "simulation" : self .view .simulation_dock ,
399444 }
400445
401446 for key , dock in dock_map .items ():
@@ -558,6 +603,10 @@ def _open_file(self, actionable, file_path, sep, mode):
558603 self .parameter_controller .open_table (file_path , sep , mode )
559604 elif actionable == "condition" :
560605 self .condition_controller .open_table (file_path , sep , mode )
606+ elif actionable == "visualization" :
607+ self .visualization_controller .open_table (file_path , sep , mode )
608+ elif actionable == "simulation" :
609+ self .simulation_controller .open_table (file_path , sep , mode )
561610 elif actionable == "data_matrix" :
562611 self .measurement_controller .process_data_matrix_file (
563612 file_path , mode , sep
@@ -604,6 +653,14 @@ def open_yaml_and_load_files(self, yaml_path=None, mode="overwrite"):
604653 self .condition_controller .open_table (
605654 yaml_dir / yaml_content ["problems" ][0 ]["condition_files" ][0 ]
606655 )
656+ # Visualization is optional
657+ vis_path = yaml_content ["problems" ][0 ].get ("visualization_files" )
658+ if vis_path :
659+ self .visualization_controller .open_table (
660+ yaml_dir / vis_path [0 ]
661+ )
662+ else :
663+ self .visualization_controller .clear_table ()
607664 self .logger .log_message (
608665 "All files opened successfully from the YAML configuration." ,
609666 color = "green" ,
@@ -721,6 +778,10 @@ def active_controller(self):
721778 return self .parameter_controller
722779 if active_widget == self .view .condition_dock .table_view :
723780 return self .condition_controller
781+ if active_widget == self .view .visualization_dock .table_view :
782+ return self .visualization_controller
783+ if active_widget == self .view .simulation_dock .table_view :
784+ return self .simulation_controller
724785 return None
725786
726787 def delete_rows (self ):
@@ -799,3 +860,62 @@ def replace(self):
799860 if self .view .find_replace_bar is None :
800861 self .view .create_find_replace_bar ()
801862 self .view .toggle_replace ()
863+
864+ def init_plotter (self ):
865+ """(Re-)initialize the plotter."""
866+ self .view .plot_dock .initialize (
867+ self .measurement_controller .proxy_model ,
868+ self .simulation_controller .proxy_model ,
869+ self .condition_controller .proxy_model ,
870+ )
871+ self .plotter = self .view .plot_dock
872+ self .plotter .highlighter .click_callback = self ._on_plot_point_clicked
873+
874+ def _on_plot_point_clicked (self , x , y , label ):
875+ # Extract observable ID from label, if formatted like 'obsId (label)'
876+ meas_proxy = self .measurement_controller .proxy_model
877+ obs = label
878+
879+ x_axis_col = "time"
880+ y_axis_col = "measurement"
881+ observable_col = "observableId"
882+
883+ def column_index (name ):
884+ for col in range (meas_proxy .columnCount ()):
885+ if (
886+ meas_proxy .headerData (col , Qt .Horizontal )
887+ == name
888+ ):
889+ return col
890+ raise ValueError (f"Column '{ name } ' not found." )
891+
892+ x_col = column_index (x_axis_col )
893+ y_col = column_index (y_axis_col )
894+ obs_col = column_index (observable_col )
895+
896+ for row in range (meas_proxy .rowCount ()):
897+ row_obs = meas_proxy .index (row , obs_col ).data ()
898+ row_x = meas_proxy .index (row , x_col ).data ()
899+ row_y = meas_proxy .index (row , y_col ).data ()
900+ try :
901+ row_x , row_y = float (row_x ), float (row_y )
902+ except ValueError :
903+ continue
904+ if row_obs == obs and row_x == x and row_y == y :
905+ self .measurement_controller .view .table_view .selectRow (row )
906+ break
907+
908+ def _on_table_selection_changed (self , selected , deselected ):
909+ """Highlight the cells selected in measurement table."""
910+ selected_rows = get_selected (
911+ self .measurement_controller .view .table_view
912+ )
913+ self .plotter .highlight_from_selection (selected_rows )
914+
915+ def _on_simulation_selection_changed (self , selected , deselected ):
916+ selected_rows = get_selected (self .simulation_controller .view .table_view )
917+ self .plotter .highlight_from_selection (
918+ selected_rows ,
919+ proxy = self .simulation_controller .proxy_model ,
920+ y_axis_col = "simulation"
921+ )
0 commit comments