algomancy-gui 0.3.16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. algomancy_gui/__init__.py +0 -0
  2. algomancy_gui/admin_page/__init__.py +1 -0
  3. algomancy_gui/admin_page/admin.py +362 -0
  4. algomancy_gui/admin_page/sessions.py +57 -0
  5. algomancy_gui/appconfiguration.py +291 -0
  6. algomancy_gui/compare_page/__init__.py +1 -0
  7. algomancy_gui/compare_page/compare.py +360 -0
  8. algomancy_gui/compare_page/kpicard.py +236 -0
  9. algomancy_gui/compare_page/scenarioselector.py +99 -0
  10. algomancy_gui/componentids.py +177 -0
  11. algomancy_gui/contentregistry.py +167 -0
  12. algomancy_gui/cqmloader.py +58 -0
  13. algomancy_gui/data_page/__init__.py +1 -0
  14. algomancy_gui/data_page/data.py +77 -0
  15. algomancy_gui/data_page/datamanagementdeletemodal.py +260 -0
  16. algomancy_gui/data_page/datamanagementderivemodal.py +201 -0
  17. algomancy_gui/data_page/datamanagementdownloadmodal.py +193 -0
  18. algomancy_gui/data_page/datamanagementimportmodal.py +438 -0
  19. algomancy_gui/data_page/datamanagementsavemodal.py +191 -0
  20. algomancy_gui/data_page/datamanagementtopbar.py +123 -0
  21. algomancy_gui/data_page/datamanagementuploadmodal.py +366 -0
  22. algomancy_gui/data_page/dialogcallbacks.py +51 -0
  23. algomancy_gui/data_page/filenamematcher.py +109 -0
  24. algomancy_gui/defaultloader.py +36 -0
  25. algomancy_gui/gui_launcher.py +183 -0
  26. algomancy_gui/home_page/__init__.py +1 -0
  27. algomancy_gui/home_page/home.py +16 -0
  28. algomancy_gui/layout.py +199 -0
  29. algomancy_gui/layouthelpers.py +30 -0
  30. algomancy_gui/managergetters.py +28 -0
  31. algomancy_gui/overview_page/__init__.py +1 -0
  32. algomancy_gui/overview_page/overview.py +20 -0
  33. algomancy_gui/py.typed +0 -0
  34. algomancy_gui/scenario_page/__init__.py +0 -0
  35. algomancy_gui/scenario_page/delete_confirmation.py +29 -0
  36. algomancy_gui/scenario_page/new_scenario_creator.py +104 -0
  37. algomancy_gui/scenario_page/new_scenario_parameters_window.py +154 -0
  38. algomancy_gui/scenario_page/scenario_badge.py +36 -0
  39. algomancy_gui/scenario_page/scenario_cards.py +119 -0
  40. algomancy_gui/scenario_page/scenarios.py +596 -0
  41. algomancy_gui/sessionmanager.py +168 -0
  42. algomancy_gui/settingsmanager.py +43 -0
  43. algomancy_gui/stylingconfigurator.py +740 -0
  44. algomancy_gui-0.3.16.dist-info/METADATA +71 -0
  45. algomancy_gui-0.3.16.dist-info/RECORD +46 -0
  46. algomancy_gui-0.3.16.dist-info/WHEEL +4 -0
@@ -0,0 +1,291 @@
1
+ import platform
2
+ from typing import Any, Dict, List, Type
3
+ import os
4
+
5
+ from algomancy_content import LibraryManager as library
6
+ from algomancy_data import InputFileConfiguration, BASE_DATA_BOUND, DataSource
7
+ from algomancy_content.pages.page import (
8
+ HomePage,
9
+ ScenarioPage,
10
+ ComparePage,
11
+ OverviewPage,
12
+ DataPage,
13
+ )
14
+ from algomancy_gui.stylingconfigurator import StylingConfigurator
15
+ from algomancy_scenario import ALGORITHM, BASE_KPI
16
+ from algomancy_scenario.core_configuration import CoreConfiguration
17
+
18
+
19
+ class AppConfiguration(CoreConfiguration):
20
+ """
21
+ Central configuration object for the Algomancy dashboard.
22
+
23
+ Construct with your choices, validation runs on creation. Use `as_dict()`
24
+ to obtain the dictionary expected by `DashLauncher.build()` and
25
+ `SettingsManager`.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ # === session manager configuration ===
31
+ use_sessions: bool = False,
32
+ # === path specifications ===
33
+ assets_path: str = "assets", # gui
34
+ data_path: str = "data",
35
+ # === data manager configuration ===
36
+ has_persistent_state: bool = False,
37
+ save_type: str | None = "json",
38
+ data_object_type: type[BASE_DATA_BOUND] | None = DataSource,
39
+ # === scenario manager configuration ===
40
+ etl_factory: Any | None = None,
41
+ kpi_templates: Dict[str, Type[BASE_KPI]] | None = None,
42
+ algo_templates: Dict[str, Type[ALGORITHM]] | None = None,
43
+ input_configs: List[InputFileConfiguration] | None = None,
44
+ # === auto start/create features ===
45
+ autocreate: bool | None = False,
46
+ default_algo: str | None = None,
47
+ default_algo_params_values: Dict[str, Any] | None = None,
48
+ autorun: bool | None = False,
49
+ # === content functions ===
50
+ home_page: HomePage | str = "standard", # gui
51
+ data_page: DataPage | str = "placeholder", # gui
52
+ scenario_page: ScenarioPage | str = "placeholder", # gui
53
+ compare_page: ComparePage | str = "placeholder", # gui
54
+ overview_page: OverviewPage | str = "standard", # gui
55
+ # === styling configuration ===
56
+ styling_config: Any | None = StylingConfigurator.get_cqm_config(), # gui
57
+ use_cqm_loader: bool = False, # gui
58
+ # === misc dashboard configurations ===
59
+ title: str = "Algomancy Dashboard",
60
+ host: str | None = None, # gui/api
61
+ port: int | None = None, # gui/api
62
+ # === page configurations ===
63
+ compare_default_open: List[str] | None = None, # gui
64
+ compare_ordered_list_components: List[str] | None = None, # gui
65
+ use_data_page_spinner: bool = True, # gui
66
+ use_scenario_page_spinner: bool = True, # gui
67
+ use_compare_page_spinner: bool = True, # gui
68
+ allow_parameter_upload_from_file: bool = False, # gui
69
+ # === authentication ===
70
+ use_authentication: bool = False, # gui
71
+ ):
72
+ # initialize core part
73
+ super().__init__(
74
+ use_sessions=use_sessions,
75
+ data_path=data_path,
76
+ has_persistent_state=has_persistent_state,
77
+ save_type=save_type,
78
+ data_object_type=data_object_type,
79
+ etl_factory=etl_factory,
80
+ kpi_templates=kpi_templates,
81
+ algo_templates=algo_templates,
82
+ input_configs=input_configs,
83
+ autocreate=autocreate,
84
+ default_algo=default_algo,
85
+ default_algo_params_values=default_algo_params_values,
86
+ autorun=autorun,
87
+ title=title,
88
+ )
89
+
90
+ # paths (GUI)
91
+ self.assets_path = assets_path
92
+
93
+ # content + callbacks
94
+ self.home_page = home_page
95
+ self.data_page = data_page
96
+ self.scenario_page = scenario_page
97
+ self.compare_page = compare_page
98
+ self.overview_page = overview_page
99
+
100
+ # styling + misc
101
+ self.styling_config = styling_config
102
+ self.use_cqm_loader = use_cqm_loader
103
+ self.host = host or self._get_default_host()
104
+ self.port = port or 8050
105
+
106
+ # settings pages
107
+ self.compare_default_open = compare_default_open or []
108
+ self.compare_ordered_list_components = compare_ordered_list_components or []
109
+ self.show_loading_on_datapage = use_data_page_spinner
110
+ self.show_loading_on_scenariopage = use_scenario_page_spinner
111
+ self.show_loading_on_comparepage = use_compare_page_spinner
112
+ self.allow_parameter_upload_from_file = allow_parameter_upload_from_file
113
+
114
+ # auth
115
+ self.use_authentication = use_authentication
116
+
117
+ # validate GUI-specific pieces immediately (core validated in super())
118
+ self._validate_gui()
119
+
120
+ # public API
121
+ def as_dict(self) -> Dict[str, Any]:
122
+ return {
123
+ # === session manager configuration ===
124
+ "use_sessions": self.use_sessions,
125
+ # === path specifications ===
126
+ "assets_path": self.assets_path,
127
+ "data_path": self.data_path,
128
+ # === data manager configuration ===
129
+ "has_persistent_state": self.has_persistent_state,
130
+ "save_type": self.save_type,
131
+ "data_object_type": self.data_object_type,
132
+ # === scenario manager configuration ===
133
+ "etl_factory": self.etl_factory,
134
+ "kpi_templates": self.kpi_templates,
135
+ "algo_templates": self.algo_templates,
136
+ "input_configs": self.input_configs,
137
+ "autocreate": self.autocreate,
138
+ "default_algo": self.default_algo,
139
+ "default_algo_params_values": self.default_algo_params_values,
140
+ "autorun": self.autorun,
141
+ # === content functions ===
142
+ "home_page": self.home_page,
143
+ "data_page": self.data_page,
144
+ "scenario_page": self.scenario_page,
145
+ "compare_page": self.compare_page,
146
+ "overview_page": self.overview_page,
147
+ # === styling configuration ===
148
+ "styling_config": self.styling_config,
149
+ "use_cqm_loader": self.use_cqm_loader,
150
+ # === misc dashboard configurations ===
151
+ "title": self.title,
152
+ "host": self.host,
153
+ "port": self.port,
154
+ # === page configurations ===
155
+ "compare_default_open": self.compare_default_open,
156
+ "compare_ordered_list_components": self.compare_ordered_list_components,
157
+ "show_loading_on_datapage": self.show_loading_on_datapage,
158
+ "show_loading_on_scenariopage": self.show_loading_on_scenariopage,
159
+ "show_loading_on_comparepage": self.show_loading_on_comparepage,
160
+ "allow_param_upload_by_file": self.allow_parameter_upload_from_file,
161
+ # === authentication ===
162
+ "use_authentication": self.use_authentication,
163
+ }
164
+
165
+ # validation helpers (GUI layer)
166
+ def _validate_gui(self) -> None:
167
+ self._validate_paths_gui()
168
+ self._validate_values_gui()
169
+ self._validate_pages()
170
+ self._validate_page_configurations()
171
+
172
+ def _validate_pages(self):
173
+ # fetch pages that were passed as str
174
+ home, data, scenario, compare, overview = library.get_pages(self.as_dict())
175
+
176
+ # check home page attributes
177
+ assert hasattr(home, "create_content")
178
+ assert hasattr(home, "register_callbacks"), (
179
+ "home_page.register_callbacks must be a function"
180
+ )
181
+
182
+ # check data page attributes
183
+ assert hasattr(data, "create_content"), (
184
+ "data_page.create_content must be a function"
185
+ )
186
+ assert hasattr(data, "register_callbacks"), (
187
+ "data_page.register_callbacks must be a function"
188
+ )
189
+
190
+ # check scenario page attributes
191
+ assert hasattr(scenario, "create_content"), (
192
+ "scenario_page.create_content must be a function"
193
+ )
194
+ assert hasattr(scenario, "register_callbacks"), (
195
+ "scenario_page.register_callbacks must be a function"
196
+ )
197
+
198
+ # check compare page attributes
199
+ assert hasattr(compare, "create_side_by_side_content"), (
200
+ "compare_page.create_side_by_side_content must be a function"
201
+ )
202
+ assert hasattr(compare, "create_compare_section"), (
203
+ "compare_page.create_compare_section must be a function"
204
+ )
205
+ assert hasattr(compare, "create_details_section"), (
206
+ "compare_page.create_details_section must be a function"
207
+ )
208
+ assert hasattr(compare, "register_callbacks"), (
209
+ "compare_page.register_callbacks must be a function"
210
+ )
211
+
212
+ # check overview page attributes
213
+ assert hasattr(overview, "create_content"), (
214
+ "overview_page.create_content must be a function"
215
+ )
216
+ assert hasattr(overview, "register_callbacks"), (
217
+ "scenario_page.register_callbacks must be a function"
218
+ )
219
+
220
+ def _validate_page_configurations(self) -> None:
221
+ # basic type checks for collections
222
+ if not isinstance(self.compare_default_open, list):
223
+ raise ValueError("compare_default_open must be a list of strings")
224
+ if not isinstance(self.compare_ordered_list_components, list):
225
+ raise ValueError(
226
+ "compare_ordered_list_components must be a list of strings"
227
+ )
228
+
229
+ # ensure all strings are valid
230
+ admissible_values = ["side-by-side", "kpis", "compare", "details"]
231
+ for component in self.compare_default_open:
232
+ if not isinstance(component, str):
233
+ raise ValueError(
234
+ f"compare_default_open must be a list of strings, but contains {component}"
235
+ )
236
+ if component not in admissible_values:
237
+ raise ValueError(
238
+ f"compare_default_open contains invalid component: {component}"
239
+ )
240
+
241
+ for component in self.compare_ordered_list_components:
242
+ if not isinstance(component, str):
243
+ raise ValueError(
244
+ f"compare_ordered_list_components must be a list of strings, but contains {component}"
245
+ )
246
+ if component not in admissible_values:
247
+ raise ValueError(
248
+ f"compare_ordered_list_components contains invalid component: {component}"
249
+ )
250
+
251
+ # ensure all strings are unique
252
+ if len(self.compare_default_open) != len(set(self.compare_default_open)):
253
+ raise ValueError("compare_default_open contains duplicate values")
254
+ if len(self.compare_ordered_list_components) != len(
255
+ set(self.compare_ordered_list_components)
256
+ ):
257
+ raise ValueError(
258
+ "compare_ordered_list_components contains duplicate values"
259
+ )
260
+
261
+ def _validate_paths_gui(self) -> None:
262
+ if self.assets_path is None or self.assets_path == "":
263
+ raise ValueError("assets_path must be provided")
264
+ if not os.path.isdir(self.assets_path):
265
+ raise ValueError(
266
+ f"assets_path does not exist or is not a directory: {self.assets_path}"
267
+ )
268
+
269
+ def _validate_values_gui(self) -> None:
270
+ # booleans allowed to be False, but must not be None if specified
271
+ if self.use_authentication is None:
272
+ raise ValueError(
273
+ "Boolean configuration 'use_authentication' must be set to True or False, not None"
274
+ )
275
+
276
+ # host and port (host may be filled elsewhere; allow None)
277
+ if self.port is not None:
278
+ if not isinstance(self.port, int) or not (1 <= self.port <= 65535):
279
+ raise ValueError("port must be an integer between 1 and 65535")
280
+
281
+ @staticmethod
282
+ def _get_default_host() -> str:
283
+ if platform.system() == "Windows":
284
+ host = "127.0.0.1" # default host for windows
285
+ else:
286
+ host = "0.0.0.1" # default host for linux
287
+ return host
288
+
289
+ @classmethod
290
+ def from_dict(cls, config: Dict[str, Any]) -> "AppConfiguration":
291
+ return cls(**config)
@@ -0,0 +1 @@
1
+ # This file is intentionally left empty to make the directory a Python package.
@@ -0,0 +1,360 @@
1
+ """
2
+ compare.py - Compare Dashboard Page
3
+
4
+ This module defines the layout and components for the compare dashboard page.
5
+ It includes scenario selection, KPI improvement displays, and secondary results sections.
6
+ """
7
+
8
+ from typing import Any, Optional
9
+
10
+ from dash import html, get_app, callback, Output, Input, State
11
+ import dash_bootstrap_components as dbc
12
+
13
+ from .kpicard import kpi_card
14
+ from ..componentids import (
15
+ PERF_DETAILS_COLLAPSE,
16
+ COMPARE_DETAIL_VIEW,
17
+ PERF_COMPARE_COLLAPSE,
18
+ PERF_PRIMARY_RESULTS,
19
+ PERF_KPI_COLLAPSE,
20
+ KPI_IMPROVEMENT_SECTION,
21
+ PERF_TOGGLE_CHECKLIST_LEFT,
22
+ PERF_TOGGLE_CHECKLIST_RIGHT,
23
+ LEFT_SCENARIO_OVERVIEW,
24
+ LEFT_SCENARIO_DROPDOWN,
25
+ ACTIVE_SESSION,
26
+ RIGHT_SCENARIO_OVERVIEW,
27
+ RIGHT_SCENARIO_DROPDOWN,
28
+ COMPARE_PAGE,
29
+ PERF_SBS_RIGHT_COLLAPSE,
30
+ PERF_SBS_LEFT_COLLAPSE,
31
+ )
32
+ from ..compare_page.scenarioselector import (
33
+ create_side_by_side_viewer,
34
+ create_side_by_side_selector,
35
+ )
36
+ from algomancy_gui.managergetters import get_scenario_manager
37
+
38
+ from ..settingsmanager import SettingsManager
39
+ from ..contentregistry import ContentRegistry
40
+ from algomancy_scenario import ScenarioManager, Scenario
41
+
42
+
43
+ def compare_page():
44
+ return html.Div(id=COMPARE_PAGE, className="compare-page")
45
+
46
+
47
+ @callback(
48
+ Output(COMPARE_PAGE, "children"),
49
+ Input(ACTIVE_SESSION, "data"),
50
+ )
51
+ def render_ordered_components(active_session_name):
52
+ """Creates the compare page layout with scenario management functionality."""
53
+
54
+ sm: ScenarioManager = get_scenario_manager(get_app().server, active_session_name)
55
+
56
+ settings: SettingsManager = get_app().server.settings
57
+
58
+ header = create_header(settings)
59
+ selector = create_side_by_side_selector(sm)
60
+
61
+ orderable_components = {
62
+ "kpis": create_kpi_viewer(),
63
+ "side-by-side": create_side_by_side_viewer(),
64
+ "compare": create_primary_viewer(),
65
+ "details": create_details_viewer(),
66
+ }
67
+
68
+ order = get_component_order(orderable_components, settings, sm)
69
+
70
+ ordered_components = order_components(header, order, orderable_components, selector)
71
+
72
+ page = html.Div(ordered_components, className="compare-page")
73
+ return page
74
+
75
+
76
+ def order_components(
77
+ header: dbc.Row,
78
+ order: list[str],
79
+ orderable_components: dict[str, dbc.Row | Any],
80
+ selector: dbc.Row,
81
+ ) -> list[dbc.Row]:
82
+ # construct component list
83
+ ordered_components = [
84
+ header,
85
+ selector,
86
+ ]
87
+ for comp_id in order:
88
+ ordered_components.append(orderable_components[comp_id])
89
+ return ordered_components
90
+
91
+
92
+ def validate(configured_order, orderable_components, sm: ScenarioManager) -> bool:
93
+ for comp_id in configured_order:
94
+ if comp_id not in orderable_components:
95
+ sm.logger.warning(
96
+ f"Invalid component id '{comp_id}' in compare page order list."
97
+ )
98
+ sm.logger.warning(
99
+ f"Expected (possibly a a subset of) {list(orderable_components.keys())}."
100
+ )
101
+ sm.logger.warning("Reverting to default component order.")
102
+ return False
103
+ return True
104
+
105
+
106
+ def get_component_order(
107
+ orderable_components: dict[str, dbc.Row | Any],
108
+ settings: SettingsManager,
109
+ sm: ScenarioManager,
110
+ ) -> list[str]:
111
+ # set a default
112
+ default_order = list(orderable_components.keys())
113
+
114
+ # retrieve any custom setting
115
+ configured_order = settings.compare_ordered_list_components
116
+
117
+ # verify the custom setting is valid
118
+ if configured_order and not validate(configured_order, orderable_components, sm):
119
+ configured_order = None
120
+
121
+ # choose ordering
122
+ used_order = configured_order if configured_order else default_order
123
+ return used_order
124
+
125
+
126
+ def create_details_viewer() -> dbc.Collapse:
127
+ return dbc.Collapse(
128
+ id=PERF_DETAILS_COLLAPSE,
129
+ children=[
130
+ html.H5("Detail view"),
131
+ html.Div(id=COMPARE_DETAIL_VIEW, className="details-view"),
132
+ ],
133
+ )
134
+
135
+
136
+ def create_primary_viewer() -> dbc.Collapse:
137
+ return dbc.Collapse(
138
+ id=PERF_COMPARE_COLLAPSE,
139
+ children=[
140
+ html.H4("Compare Results"),
141
+ html.Div(id=PERF_PRIMARY_RESULTS, className="compare-view"),
142
+ ],
143
+ )
144
+
145
+
146
+ def create_kpi_viewer() -> dbc.Collapse:
147
+ return dbc.Collapse(
148
+ id=PERF_KPI_COLLAPSE,
149
+ children=[
150
+ html.H4("KPI Improvements"),
151
+ html.Div(id=KPI_IMPROVEMENT_SECTION, className="kpi-cards"),
152
+ ],
153
+ )
154
+
155
+
156
+ def create_header(settings: SettingsManager) -> dbc.Row:
157
+ default_open = settings.compare_default_open
158
+
159
+ return dbc.Row(
160
+ [
161
+ dbc.Col(html.H1("Compare"), width=9),
162
+ dbc.Col(
163
+ dbc.Row(
164
+ [
165
+ dbc.Col(
166
+ dbc.Checklist(
167
+ options=[
168
+ {
169
+ "label": "Show side-by-side",
170
+ "value": "side-by-side",
171
+ },
172
+ {"label": "Show KPI cards", "value": "kpis"},
173
+ ],
174
+ id=PERF_TOGGLE_CHECKLIST_LEFT,
175
+ class_name="styled-toggle",
176
+ switch=True,
177
+ value=default_open,
178
+ ),
179
+ width=6,
180
+ ),
181
+ dbc.Col(
182
+ dbc.Checklist(
183
+ options=[
184
+ {"label": "Show compare view", "value": "compare"},
185
+ {"label": "Show details", "value": "details"},
186
+ ],
187
+ id=PERF_TOGGLE_CHECKLIST_RIGHT,
188
+ class_name="styled-toggle",
189
+ switch=True,
190
+ value=default_open,
191
+ ),
192
+ width=6,
193
+ ),
194
+ ]
195
+ ),
196
+ width=3,
197
+ ),
198
+ ]
199
+ )
200
+
201
+
202
+ @callback(
203
+ Output(LEFT_SCENARIO_OVERVIEW, "children"),
204
+ Input(LEFT_SCENARIO_DROPDOWN, "value"),
205
+ State(ACTIVE_SESSION, "data"),
206
+ prevent_initial_call=True,
207
+ )
208
+ def update_left_scenario_overview(scenario_id: str, session_id: str) -> html.Div | str:
209
+ if not scenario_id:
210
+ return "No scenario selected."
211
+ scenario_manager = get_scenario_manager(
212
+ server=get_app().server, active_session_name=session_id
213
+ )
214
+ s: Optional[Scenario] = scenario_manager.get_by_id(scenario_id)
215
+
216
+ cr: ContentRegistry = get_app().server.content_registry
217
+
218
+ if not s:
219
+ return "Scenario not found."
220
+
221
+ return cr.compare_side_by_side(s, "left")
222
+
223
+
224
+ @callback(
225
+ Output(RIGHT_SCENARIO_OVERVIEW, "children"),
226
+ Input(RIGHT_SCENARIO_DROPDOWN, "value"),
227
+ State(ACTIVE_SESSION, "data"),
228
+ prevent_initial_call=True,
229
+ )
230
+ def update_right_scenario_overview(scenario_id, session_id) -> html.Div | str:
231
+ if not scenario_id:
232
+ return "No scenario selected."
233
+ scenario_manager: ScenarioManager = get_scenario_manager(
234
+ get_app().server, session_id
235
+ )
236
+ s = scenario_manager.get_by_id(scenario_id)
237
+ cr: ContentRegistry = get_app().server.content_registry
238
+
239
+ if not s:
240
+ return "Scenario not found."
241
+
242
+ return cr.compare_side_by_side(s, "right")
243
+
244
+
245
+ @callback(
246
+ Output(PERF_PRIMARY_RESULTS, "children"),
247
+ Input(LEFT_SCENARIO_DROPDOWN, "value"),
248
+ Input(RIGHT_SCENARIO_DROPDOWN, "value"),
249
+ State(ACTIVE_SESSION, "data"),
250
+ prevent_initial_call=True,
251
+ )
252
+ def update_right_scenario_overview_primary(
253
+ left_scenario_id, right_scenario_id, session_id
254
+ ) -> html.Div:
255
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
256
+ cr: ContentRegistry = get_app().server.content_registry
257
+
258
+ # check the inputs
259
+ if not left_scenario_id or not right_scenario_id:
260
+ return html.Div("Select both scenarios to create a detail view.")
261
+
262
+ # retrieve the scenarios
263
+ left_scenario = sm.get_by_id(left_scenario_id)
264
+ right_scenario = sm.get_by_id(right_scenario_id)
265
+
266
+ # check if the scenarios were found
267
+ if not left_scenario or not right_scenario:
268
+ return html.Div("One of the scenarios was not found.")
269
+
270
+ # apply the function
271
+ return cr.compare_compare(left_scenario, right_scenario)
272
+
273
+
274
+ @callback(
275
+ Output(COMPARE_DETAIL_VIEW, "children"),
276
+ Input(LEFT_SCENARIO_DROPDOWN, "value"),
277
+ Input(RIGHT_SCENARIO_DROPDOWN, "value"),
278
+ State(ACTIVE_SESSION, "data"),
279
+ prevent_initial_call=True,
280
+ )
281
+ def update_right_scenario_overview_detail(
282
+ left_scenario_id,
283
+ right_scenario_id,
284
+ session_id: str,
285
+ ) -> html.Div | str:
286
+ sm: ScenarioManager = get_scenario_manager(get_app().server, session_id)
287
+ cr: ContentRegistry = get_app().server.content_registry
288
+
289
+ if not left_scenario_id or not right_scenario_id:
290
+ return "Select both scenarios to create a detail view."
291
+
292
+ if left_scenario_id == right_scenario_id:
293
+ return "Select two different scenarios to create a detail view."
294
+
295
+ left_scenario = sm.get_by_id(left_scenario_id)
296
+ right_scenario = sm.get_by_id(right_scenario_id)
297
+ if not left_scenario or not right_scenario:
298
+ return "One of the scenarios was not found."
299
+
300
+ return cr.compare_details(left_scenario, right_scenario)
301
+
302
+
303
+ @callback(
304
+ Output(KPI_IMPROVEMENT_SECTION, "children"),
305
+ Input(LEFT_SCENARIO_DROPDOWN, "value"),
306
+ Input(RIGHT_SCENARIO_DROPDOWN, "value"),
307
+ State(ACTIVE_SESSION, "data"),
308
+ )
309
+ def update_kpi_comparison(left_id, right_id, active_session_name):
310
+ if not left_id or not right_id:
311
+ return html.P("Select two completed scenarios to compare KPIs.")
312
+ sm: ScenarioManager = get_scenario_manager(get_app().server, active_session_name)
313
+
314
+ left = sm.get_by_id(left_id)
315
+ right = sm.get_by_id(right_id)
316
+
317
+ if not left or not right:
318
+ return html.P("One or both scenarios not found.")
319
+
320
+ # Example KPI dictionaries
321
+ left_kpis = left.kpis
322
+ right_kpis = right.kpis
323
+
324
+ assert len(left_kpis) == len(right_kpis), "KPIs do not match."
325
+
326
+ cards = []
327
+ for tag, left_kpi in left_kpis.items():
328
+ right_kpi = right_kpis.get(tag)
329
+
330
+ card = kpi_card(
331
+ left_kpi=left_kpi,
332
+ right_kpi=right_kpi,
333
+ )
334
+
335
+ cards.append(html.Div(card, className="kpi-card-wrapper"))
336
+
337
+ return html.Div(cards, className="kpi-cards")
338
+
339
+
340
+ @callback(
341
+ Output(PERF_SBS_LEFT_COLLAPSE, "is_open"),
342
+ Output(PERF_SBS_RIGHT_COLLAPSE, "is_open"),
343
+ Output(PERF_KPI_COLLAPSE, "is_open"),
344
+ Input(PERF_TOGGLE_CHECKLIST_LEFT, "value"),
345
+ )
346
+ def listen_to_left_checklist(checked):
347
+ sbs_open = True if "side-by-side" in checked else False
348
+ kpi_open = True if "kpis" in checked else False
349
+ return sbs_open, sbs_open, kpi_open
350
+
351
+
352
+ @callback(
353
+ Output(PERF_COMPARE_COLLAPSE, "is_open"),
354
+ Output(PERF_DETAILS_COLLAPSE, "is_open"),
355
+ Input(PERF_TOGGLE_CHECKLIST_RIGHT, "value"),
356
+ )
357
+ def listen_to_right_checklist(checked):
358
+ compare_open = True if "compare" in checked else False
359
+ details_open = True if "details" in checked else False
360
+ return compare_open, details_open