Coverage for agentlib_flexquant/utils/interactive.py: 14%

212 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-09-19 15:08 +0000

1import webbrowser 

2import pandas as pd 

3from typing import get_args, Union, Optional 

4from pydantic import FilePath 

5from dash import Dash, html, dcc, callback, Output, Input, ctx 

6from plotly import graph_objects as go 

7from agentlib.core.agent import AgentConfig 

8from agentlib_mpc.utils import TimeConversionTypes, TIME_CONVERSION 

9from agentlib_mpc.utils.analysis import mpc_at_time_step 

10from agentlib_mpc.utils.plotting.interactive import get_port 

11import agentlib_flexquant.data_structures.globals as glbs 

12import agentlib_flexquant.data_structures.flex_results as flex_results 

13from agentlib_flexquant.data_structures.flexquant import FlexQuantConfig 

14from agentlib_flexquant.data_structures.flex_kpis import FlexibilityKPIs 

15from agentlib_flexquant.data_structures.flex_offer import OfferStatus 

16 

17 

18class CustomBound: 

19 """Dataclass to let the user define custom bounds for the mpc variables. 

20 

21 for_variable -- The name of the variable to plot the bounds into 

22 lower bound -- The lower bound of the variable as the name of the lower bound variable in the MPC 

23 upper bound -- The upper bound of the variable as the name of the upper bound variable in the MPC 

24 

25 """ 

26 

27 for_variable: str 

28 lower_bound: Optional[str] 

29 upper_bound: Optional[str] 

30 

31 def __init__( 

32 self, 

33 for_variable: str, 

34 lb_name: Optional[str] = None, 

35 ub_name: Optional[str] = None, 

36 ): 

37 self.for_variable = for_variable 

38 self.lower_bound = lb_name 

39 self.upper_bound = ub_name 

40 

41 

42class Dashboard(flex_results.Results): 

43 """Class for the dashboard of flexquant""" 

44 # Constants for plotting variables 

45 MPC_ITERATIONS: str = "iter_count" 

46 

47 # Label for the positive and negative flexibilities 

48 label_positive: str = "positive" 

49 label_negative: str = "negative" 

50 

51 # Keys for line properties 

52 bounds_key: str = "bounds" 

53 characteristic_times_current_key: str = "characteristic_times_current" 

54 characteristic_times_accepted_key: str = "characteristic_times_accepted" 

55 

56 # Custom settings 

57 custom_bounds: list[CustomBound] = [] 

58 

59 def __init__( 

60 self, 

61 flex_config: Optional[Union[str, FilePath, FlexQuantConfig]] = None, 

62 simulator_agent_config: Optional[Union[str, FilePath, AgentConfig]] = None, 

63 generated_flex_files_base_path: Optional[Union[str, FilePath]] = None, 

64 results: Union[str, FilePath, dict[str, dict[str, pd.DataFrame]]] = None, 

65 to_timescale: TimeConversionTypes = "hours", 

66 port: int = None 

67 ): 

68 super().__init__( 

69 flex_config=flex_config, 

70 simulator_agent_config=simulator_agent_config, 

71 generated_flex_files_base_path=generated_flex_files_base_path, 

72 results=results, 

73 to_timescale=to_timescale, 

74 ) 

75 self.port = port 

76 self.current_timescale_input = self.current_timescale_of_data 

77 # remove FMU from dataclass to enable pickling/multiprocessing of Dashboard 

78 if hasattr(self.simulator_module_config, "model"): 

79 self.simulator_module_config.model_config["frozen"] = False 

80 delattr(self.simulator_module_config, "model") 

81 

82 # Define line properties 

83 self.LINE_PROPERTIES: dict = { 

84 self.simulator_agent_config.id: { 

85 "color": "black", 

86 }, 

87 self.baseline_agent_config.id: { 

88 "color": "black", 

89 }, 

90 self.neg_flex_agent_config.id: { 

91 "color": "red", 

92 }, 

93 self.pos_flex_agent_config.id: { 

94 "color": "blue", 

95 }, 

96 self.bounds_key: { 

97 "color": "grey", 

98 }, 

99 self.characteristic_times_current_key: { 

100 "color": "grey", 

101 "dash": "dash", 

102 }, 

103 self.characteristic_times_accepted_key: { 

104 "color": "yellow", 

105 }, 

106 } 

107 

108 # KPIS 

109 kpis_pos = FlexibilityKPIs(direction="positive") 

110 self.kpi_names_pos = kpis_pos.get_name_dict() 

111 kpis_neg = FlexibilityKPIs(direction="negative") 

112 self.kpi_names_neg = kpis_neg.get_name_dict() 

113 

114 # Get variables for plotting 

115 # MPC stats 

116 self.plotting_variables = [self.MPC_ITERATIONS] 

117 # MPC and sim variables 

118 self.intersection_mpcs_sim = self.get_intersection_mpcs_sim() 

119 self.plotting_variables.extend( 

120 [key for key in self.intersection_mpcs_sim.keys()] 

121 ) 

122 # Flexibility kpis 

123 self.plotting_variables.append(kpis_pos.energy_flex.name) 

124 self.plotting_variables.append(kpis_pos.costs.name) 

125 # for kpi in kpis_pos.get_kpi_dict(direction_name=False).values(): 

126 # if not isinstance(kpi.value, pd.Series): 

127 # self.plotting_variables.append(kpi.name) 

128 

129 def show(self, custom_bounds: Union[CustomBound, list[CustomBound]] = None): 

130 """Show the dashboard in a web browser containing: 

131 -- Statistics of the MPCs solver 

132 -- The states, controls, and the power variable of the MPCs and the simulator 

133 -- KPIs of the flexibility quantification 

134 -- Markings of the characteristic flexibility times 

135 

136 Args: 

137 custom_bounds: optional arguments to show the comfort bounds 

138 

139 """ 

140 if custom_bounds is None: 

141 self.custom_bounds = [] 

142 elif isinstance(custom_bounds, CustomBound): 

143 self.custom_bounds = [custom_bounds] 

144 else: 

145 self.custom_bounds = custom_bounds 

146 

147 

148 # Plotting functions 

149 def plot_mpc_stats(fig: go.Figure, variable: str) -> go.Figure: 

150 """ plot the statics of the baseline and shadow mpcs. 

151 

152 Args: 

153 fig: the figure to be updated 

154 variable: the statics variable to be plotted 

155 

156 Returns: 

157 The updated figure 

158 

159 """ 

160 fig.add_trace( 

161 go.Scatter( 

162 name=self.baseline_agent_config.id, 

163 x=self.df_baseline_stats.index, 

164 y=self.df_baseline_stats[variable], 

165 mode="markers", 

166 line=self.LINE_PROPERTIES[self.baseline_agent_config.id], 

167 ) 

168 ) 

169 fig.add_trace( 

170 go.Scatter( 

171 name=self.pos_flex_agent_config.id, 

172 x=self.df_pos_flex_stats.index, 

173 y=self.df_pos_flex_stats[variable], 

174 mode="markers", 

175 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id], 

176 ) 

177 ) 

178 fig.add_trace( 

179 go.Scatter( 

180 name=self.neg_flex_agent_config.id, 

181 x=self.df_neg_flex_stats.index, 

182 y=self.df_neg_flex_stats[variable], 

183 mode="markers", 

184 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id], 

185 ) 

186 ) 

187 return fig 

188 

189 def plot_one_mpc_variable(fig: go.Figure, variable: str, time_step: float) -> go.Figure: 

190 """Plot the mpc series for the specified variable at the specified time step. 

191 

192 Args: 

193 fig: the figure to be updated 

194 variable: the variable to be plotted 

195 time_step: the time step to be plotted 

196 

197 Returns: 

198 The updated figure 

199 

200 """ 

201 # Get the mpc data for the plot 

202 series_neg = mpc_at_time_step( 

203 data=self.df_neg_flex, 

204 time_step=time_step, 

205 variable=self.intersection_mpcs_sim[variable][ 

206 self.neg_flex_module_config.module_id 

207 ], 

208 index_offset=False, 

209 ) 

210 series_pos = mpc_at_time_step( 

211 data=self.df_pos_flex, 

212 time_step=time_step, 

213 variable=self.intersection_mpcs_sim[variable][ 

214 self.pos_flex_module_config.module_id 

215 ], 

216 index_offset=False, 

217 ) 

218 series_bas = mpc_at_time_step( 

219 data=self.df_baseline, 

220 time_step=time_step, 

221 variable=self.intersection_mpcs_sim[variable][ 

222 self.baseline_module_config.module_id 

223 ], 

224 index_offset=False, 

225 ) 

226 

227 def _add_step_to_data(s: pd.Series) -> pd.Series: 

228 """ shift the index of the series """ 

229 s_concat = s.copy().shift(periods=1) 

230 s_concat.index = s.index - 0.01 * (s.index[1] - s.index[0]) 

231 for ind, val in s_concat.items(): 

232 s[ind] = val 

233 s.sort_index(inplace=True) 

234 return s 

235 

236 # Manage nans 

237 for series in [series_neg, series_pos, series_bas]: 

238 if variable in [ 

239 control.name for control in self.baseline_module_config.controls 

240 ]: 

241 series.dropna(inplace=True) 

242 series = _add_step_to_data(s=series) 

243 series.dropna(inplace=True) 

244 

245 # Plot the data 

246 try: 

247 df_sim = self.df_simulation[ 

248 self.intersection_mpcs_sim[variable][ 

249 self.simulator_module_config.module_id 

250 ] 

251 ] 

252 fig.add_trace( 

253 go.Scatter( 

254 name=self.simulator_agent_config.id, 

255 x=df_sim.index, 

256 y=df_sim, 

257 mode="lines", 

258 line=self.LINE_PROPERTIES[self.simulator_agent_config.id], 

259 zorder=2, 

260 ) 

261 ) 

262 except KeyError: 

263 # E.g. when the simulator variable name was not found from the intersection 

264 pass 

265 

266 fig.add_trace( 

267 go.Scatter( 

268 name=self.baseline_agent_config.id, 

269 x=series_bas.index, 

270 y=series_bas, 

271 mode="lines", 

272 line=self.LINE_PROPERTIES[self.baseline_agent_config.id] 

273 | {"dash": "dash"}, 

274 zorder=3, 

275 ) 

276 ) 

277 fig.add_trace( 

278 go.Scatter( 

279 name=self.neg_flex_agent_config.id, 

280 x=series_neg.index, 

281 y=series_neg, 

282 mode="lines", 

283 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id] 

284 | {"dash": "dash"}, 

285 zorder=4, 

286 ) 

287 ) 

288 fig.add_trace( 

289 go.Scatter( 

290 name=self.pos_flex_agent_config.id, 

291 x=series_pos.index, 

292 y=series_pos, 

293 mode="lines", 

294 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id] 

295 | {"dash": "dash"}, 

296 zorder=4, 

297 ) 

298 ) 

299 

300 # Get the data for the bounds 

301 def _get_mpc_series(var_type: str, var_name: str) -> pd.Series: 

302 return self.df_baseline[(var_type, var_name)].xs(0, level=1) 

303 

304 def _get_bound(var_name: str) -> Optional[pd.Series]: 

305 if var_name in self.df_baseline.columns.get_level_values(1): 

306 try: 

307 bound = _get_mpc_series(var_type="variable", var_name=var_name) 

308 except KeyError: 

309 bound = _get_mpc_series(var_type="parameter", var_name=var_name) 

310 else: 

311 bound = None 

312 return bound 

313 

314 df_lb = None 

315 df_ub = None 

316 for custom_bound in self.custom_bounds: 

317 if variable == custom_bound.for_variable: 

318 df_lb = _get_bound(custom_bound.lower_bound) 

319 df_ub = _get_bound(custom_bound.upper_bound) 

320 if variable in [ 

321 control.name for control in self.baseline_module_config.controls 

322 ]: 

323 df_lb = _get_mpc_series(var_type="lower", var_name=variable) 

324 df_ub = _get_mpc_series(var_type="upper", var_name=variable) 

325 

326 # Plot bounds 

327 if df_lb is not None: 

328 fig.add_trace( 

329 go.Scatter( 

330 name="Lower bound", 

331 x=df_lb.index, 

332 y=df_lb, 

333 mode="lines", 

334 line=self.LINE_PROPERTIES[self.bounds_key], 

335 zorder=1, 

336 ) 

337 ) 

338 if df_ub is not None: 

339 fig.add_trace( 

340 go.Scatter( 

341 name="Upper bound", 

342 x=df_ub.index, 

343 y=df_ub, 

344 mode="lines", 

345 line=self.LINE_PROPERTIES[self.bounds_key], 

346 zorder=1, 

347 ) 

348 ) 

349 

350 return fig 

351 

352 def plot_flexibility_kpi(fig: go.Figure, variable: str) -> go.Figure: 

353 """Plot the flexibility kpi. 

354 

355 Args: 

356 fig: the figure to be updated 

357 variable: the kpi variable to be plotted 

358 

359 Returns: 

360 The updated figure 

361 

362 """ 

363 df_ind = self.df_indicator.xs(0, level=1) 

364 # if the variable only has NaN, don't plot 

365 if df_ind[self.kpi_names_pos[variable]].isna().all(): 

366 return 

367 fig.add_trace( 

368 go.Scatter( 

369 name=self.label_positive, 

370 x=df_ind.index, 

371 y=df_ind[self.kpi_names_pos[variable]], 

372 mode="lines+markers", 

373 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id], 

374 ) 

375 ) 

376 fig.add_trace( 

377 go.Scatter( 

378 name=self.label_negative, 

379 x=df_ind.index, 

380 y=df_ind[self.kpi_names_neg[variable]], 

381 mode="lines+markers", 

382 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id], 

383 ) 

384 ) 

385 return fig 

386 

387 def plot_market_results(fig: go.Figure, variable: str) -> go.Figure: 

388 """Plot the market results. 

389 

390 Args: 

391 fig: the figure to be updated 

392 variable: the variable to be plotted 

393 

394 Returns: 

395 The updated figure 

396 

397 """ 

398 df_flex_market_index = self.df_market.index.droplevel("time") 

399 if variable in self.df_market.columns: 

400 fig.add_trace( 

401 go.Scatter( 

402 x=df_flex_market_index, 

403 y=self.df_market[variable], 

404 mode="lines+markers", 

405 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id], 

406 ) 

407 ) 

408 else: 

409 pos_var = f"pos_{variable}" 

410 neg_var = f"neg_{variable}" 

411 fig.add_trace( 

412 go.Scatter( 

413 name=self.label_positive, 

414 x=df_flex_market_index, 

415 y=self.df_market[pos_var], 

416 mode="lines+markers", 

417 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id], 

418 ) 

419 ) 

420 fig.add_trace( 

421 go.Scatter( 

422 name=self.label_negative, 

423 x=df_flex_market_index, 

424 y=self.df_market[neg_var], 

425 mode="lines+markers", 

426 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id], 

427 ) 

428 ) 

429 return fig 

430 

431 # Marking times 

432 def get_characteristic_times(at_time_step: float) -> (float, float, float): 

433 """Get the characteristic times. 

434 

435 Args: 

436 at_time_step: the time at which we want to get the characteristic times 

437 

438 Returns: 

439 market_time, prep_time and flex_event_duration 

440 

441 """ 

442 df_characteristic_times = self.df_indicator.xs(0, level="time") 

443 rel_market_time = ( 

444 df_characteristic_times.loc[at_time_step, glbs.MARKET_TIME] 

445 / TIME_CONVERSION[self.current_timescale_of_data] 

446 ) 

447 rel_prep_time = ( 

448 df_characteristic_times.loc[at_time_step, glbs.PREP_TIME] 

449 / TIME_CONVERSION[self.current_timescale_of_data] 

450 ) 

451 flex_event_duration = ( 

452 df_characteristic_times.loc[at_time_step, glbs.FLEX_EVENT_DURATION] 

453 / TIME_CONVERSION[self.current_timescale_of_data] 

454 ) 

455 return rel_market_time, rel_prep_time, flex_event_duration 

456 

457 def mark_time(fig: go.Figure, at_time_step: float, line_prop: dict) -> go.Figure: 

458 fig.add_vline(x=at_time_step, line=line_prop, layer="below") 

459 return fig 

460 

461 def mark_characteristic_times(fig: go.Figure, offer_time: Union[float, int] = 0, line_prop: dict = None) -> go.Figure: 

462 """Add markers of the characteristic times to the plot for a time step. 

463 

464 Args: 

465 fig: the figure to plot the results into 

466 offer_time: When to show the markers 

467 line_prop: the graphic properties of the lines as in plotly 

468 

469 Returns: 

470 The updated figure 

471 

472 """ 

473 if line_prop is None: 

474 line_prop = self.LINE_PROPERTIES[self.characteristic_times_current_key] 

475 try: 

476 rel_market_time, rel_prep_time, flex_event_duration = ( 

477 get_characteristic_times(offer_time) 

478 ) 

479 mark_time(fig=fig, at_time_step=offer_time, line_prop=line_prop) 

480 mark_time( 

481 fig=fig, 

482 at_time_step=offer_time + rel_market_time, 

483 line_prop=line_prop, 

484 ) 

485 mark_time( 

486 fig=fig, 

487 at_time_step=offer_time + rel_prep_time + rel_market_time, 

488 line_prop=line_prop, 

489 ) 

490 mark_time( 

491 fig=fig, 

492 at_time_step=offer_time 

493 + rel_prep_time 

494 + rel_market_time 

495 + flex_event_duration, 

496 line_prop=line_prop, 

497 ) 

498 except KeyError: 

499 pass # No data of characteristic times available, e.g. if offer accepted 

500 return fig 

501 

502 def mark_characteristic_times_of_accepted_offers(fig: go.Figure) -> go.Figure: 

503 """Add markers of the characteristic times for accepted offers to the plot.""" 

504 if self.df_market is not None: 

505 if (self.df_market["status"].isin([ 

506 OfferStatus.accepted_negative.value, 

507 OfferStatus.accepted_positive.value, 

508 ] 

509 ).any()): 

510 df_accepted_offers = self.df_market["status"].str.contains( 

511 pat="OfferStatus.accepted" 

512 ) 

513 for i in df_accepted_offers.index.to_list(): 

514 if df_accepted_offers[i]: 

515 fig = mark_characteristic_times( 

516 fig=fig, 

517 offer_time=i[0], 

518 line_prop=self.LINE_PROPERTIES[ 

519 self.characteristic_times_accepted_key 

520 ], 

521 ) 

522 return fig 

523 

524 # Master plotting function 

525 def create_plot( 

526 variable: str, 

527 at_time_step: float, 

528 show_accepted_characteristic_times: bool = True, 

529 show_current_characteristic_times: bool = True, 

530 zoom_to_offer_window: bool = False, 

531 zoom_to_prediction_interval: bool = False, 

532 ) -> go.Figure: 

533 """Create a plot for one variable 

534 

535 Args: 

536 variable: the variable to plot 

537 at_time_step: the time_step to show the mpc predictions and the characteristic times 

538 show_accepted_characteristic_times: whether to show the accepted characteristic times 

539 show_current_characteristic_times: whether to show the current characteristic times 

540 zoom_to_offer_window: whether to zoom to offer window 

541 zoom_to_prediction_interval: wether to zoom to prediction interval 

542 

543 Returns: 

544 The created figure 

545 

546 """ 

547 # Create the figure 

548 fig = go.Figure() 

549 mark_time(fig=fig, at_time_step=at_time_step, line_prop={"color": "green"}) 

550 if show_accepted_characteristic_times: 

551 mark_characteristic_times_of_accepted_offers(fig=fig) 

552 

553 # Plot variable 

554 if variable in self.df_baseline_stats.columns: 

555 plot_mpc_stats(fig=fig, variable=variable) 

556 elif variable in self.intersection_mpcs_sim.keys(): 

557 plot_one_mpc_variable( 

558 fig=fig, variable=variable, time_step=at_time_step 

559 ) 

560 if show_current_characteristic_times: 

561 mark_characteristic_times(fig=fig, offer_time=at_time_step) 

562 elif any(variable in label for label in self.df_indicator.columns): 

563 plot_flexibility_kpi(fig=fig, variable=variable) 

564 elif any(variable in label for label in self.df_market.columns): 

565 plot_market_results(fig=fig, variable=variable) 

566 else: 

567 raise ValueError(f"No plotting function found for variable {variable}") 

568 

569 # Set layout 

570 if zoom_to_offer_window: 

571 rel_market_time, rel_prep_time, flex_event_duration = ( 

572 get_characteristic_times(at_time_step) 

573 ) 

574 ts = ( 

575 self.baseline_module_config.time_step 

576 / TIME_CONVERSION[self.current_timescale_of_data] 

577 ) 

578 

579 xlim_left = at_time_step 

580 xlim_right = ( 

581 at_time_step 

582 + rel_market_time 

583 + rel_prep_time 

584 + flex_event_duration 

585 + 4 * ts 

586 ) 

587 elif zoom_to_prediction_interval: 

588 xlim_left = at_time_step 

589 xlim_right = at_time_step + self.df_baseline.index[-1][-1] 

590 else: 

591 xlim_left = self.df_simulation.index[0] 

592 xlim_right = ( 

593 self.df_simulation.index[-1] + self.df_baseline.index[-1][-1] 

594 ) 

595 

596 fig.update_layout( 

597 yaxis_title=variable, 

598 xaxis_title=f"Time in {self.current_timescale_of_data}", 

599 xaxis_range=[xlim_left, xlim_right], 

600 height=350, 

601 margin=dict(t=20, b=20), 

602 ) 

603 fig.update_xaxes( 

604 dtick=round(self.baseline_module_config.prediction_horizon / 6) 

605 * self.baseline_module_config.time_step 

606 / TIME_CONVERSION[self.current_timescale_of_data] 

607 ) 

608 fig.update_yaxes(tickformat="~r") 

609 return fig 

610 

611 # Create the app 

612 app = Dash(__name__ + "_flexibility", title="Flexibility Results") 

613 app.layout = [ 

614 html.H1("Results"), 

615 html.H3("Settings"), 

616 html.Div( 

617 children=[ 

618 html.Code(f"{option}: {setting}, ") 

619 for option, setting in self.baseline_module_config.optimization_backend[ 

620 "discretization_options" 

621 ].items() 

622 ] 

623 ), 

624 # Options 

625 html.Div( 

626 children=[ 

627 html.H3("Options"), 

628 html.Div( 

629 children=[ 

630 dcc.Checklist( 

631 id="accepted_characteristic_times", 

632 options=[ 

633 { 

634 "label": "Show characteristic times (accepted)", 

635 "value": True, 

636 } 

637 ], 

638 value=[True], 

639 style={ 

640 "display": "inline-block", 

641 "padding-right": "10px", 

642 }, 

643 ), 

644 dcc.Checklist( 

645 id="current_characteristic_times", 

646 options=[ 

647 { 

648 "label": "Show characteristic times (current)", 

649 "value": True, 

650 } 

651 ], 

652 value=[True], 

653 style={ 

654 "display": "inline-block", 

655 "padding-right": "10px", 

656 }, 

657 ), 

658 ], 

659 ), 

660 html.Div( 

661 children=[ 

662 dcc.Checklist( 

663 id="zoom_to_offer_window", 

664 options=[ 

665 { 

666 "label": "Zoom to flexibility offer window", 

667 "value": False, 

668 } 

669 ], 

670 style={ 

671 "display": "inline-block", 

672 "padding-right": "10px", 

673 }, 

674 ), 

675 dcc.Checklist( 

676 id="zoom_to_prediction_interval", 

677 options=[ 

678 { 

679 "label": "Zoom to mpc prediction interval", 

680 "value": False, 

681 } 

682 ], 

683 style={"display": "inline-block"}, 

684 ), 

685 ], 

686 ), 

687 # Time input 

688 html.Div( 

689 children=[ 

690 html.H3( 

691 children=f"Time:", 

692 style={ 

693 "display": "inline-block", 

694 "padding-right": "10px", 

695 }, 

696 ), 

697 dcc.Input( 

698 id="time_typing", 

699 type="number", 

700 min=0, 

701 max=1, 

702 value=0, # will be updated in the callback 

703 style={"display": "inline-block"}, 

704 ), 

705 dcc.Dropdown( 

706 id="time_unit", 

707 options=get_args(TimeConversionTypes), 

708 value=self.current_timescale_input, 

709 style={ 

710 "display": "inline-block", 

711 "verticalAlign": "middle", 

712 "padding-left": "10px", 

713 "width": "100px", 

714 }, 

715 ), 

716 ], 

717 ), 

718 dcc.Slider( 

719 id="time_slider", 

720 min=0, 

721 max=1, 

722 value=0, # will be updated in the callback 

723 tooltip={"placement": "bottom", "always_visible": True}, 

724 marks=None, 

725 updatemode="drag", 

726 ), 

727 ], 

728 style={ 

729 "width": "88%", 

730 "padding-left": "0%", 

731 "padding-right": "12%", 

732 # Make the options sticky to the top of the page 

733 "position": "sticky", 

734 "top": "0", 

735 "overflow-y": "visible", 

736 "z-index": "100", 

737 "background-color": "white", 

738 }, 

739 ), 

740 # Container for the graphs, will be updated in the callback 

741 html.Div(id="graphs_container_variables", children=[]), 

742 ] 

743 

744 # Callbacks 

745 # Update the time value or the time unit 

746 @callback( 

747 Output(component_id="time_slider", component_property="value"), 

748 Output(component_id="time_slider", component_property="min"), 

749 Output(component_id="time_slider", component_property="max"), 

750 Output(component_id="time_slider", component_property="step"), 

751 Output(component_id="time_typing", component_property="value"), 

752 Output(component_id="time_typing", component_property="min"), 

753 Output(component_id="time_typing", component_property="max"), 

754 Output(component_id="time_typing", component_property="step"), 

755 Input(component_id="time_typing", component_property="value"), 

756 Input(component_id="time_slider", component_property="value"), 

757 Input(component_id="time_unit", component_property="value"), 

758 ) 

759 def update_time_index_of_input( 

760 time_typing: float, time_slider: float, time_unit: TimeConversionTypes 

761 ) -> (int, float, float, float, int, float, float, float): 

762 # get trigger id 

763 trigger_id = ctx.triggered[0]["prop_id"].split(".")[0] 

764 

765 # Get the value for the sliders 

766 if trigger_id == "time_slider": 

767 value = time_slider 

768 elif trigger_id == "time_unit": 

769 value = ( 

770 time_typing 

771 * TIME_CONVERSION[self.current_timescale_input] 

772 / TIME_CONVERSION[time_unit] 

773 ) 

774 else: 

775 value = time_typing 

776 

777 # Convert the index to the given time unit if necessary 

778 if trigger_id == "time_unit": 

779 self.convert_timescale_of_dataframe_index(to_timescale=time_unit) 

780 

781 # Get the index for the slider types 

782 times = self.df_baseline.index.get_level_values(0).unique() 

783 minimum = times[0] 

784 maximum = times[-1] 

785 step = times[1] - times[0] 

786 

787 self.current_timescale_input = time_unit 

788 

789 return (value, minimum, maximum, step, value, minimum, maximum, step) 

790 

791 # Update the graphs 

792 @callback( 

793 Output( 

794 component_id="graphs_container_variables", component_property="children" 

795 ), 

796 Input(component_id="time_typing", component_property="value"), 

797 Input( 

798 component_id="accepted_characteristic_times", component_property="value" 

799 ), 

800 Input( 

801 component_id="current_characteristic_times", component_property="value" 

802 ), 

803 Input(component_id="zoom_to_offer_window", component_property="value"), 

804 Input( 

805 component_id="zoom_to_prediction_interval", component_property="value" 

806 ), 

807 ) 

808 def update_graph( 

809 at_time_step: float, 

810 show_accepted_characteristic_times: bool, 

811 show_current_characteristic_times: bool, 

812 zoom_to_offer_window: bool, 

813 zoom_to_prediction_interval: bool, 

814 ) -> list[dcc.Graph]: 

815 """Update all graphs based on the options and slider values""" 

816 figs = [] 

817 for variable in self.plotting_variables: 

818 fig = create_plot( 

819 variable=variable, 

820 at_time_step=at_time_step, 

821 show_accepted_characteristic_times=show_accepted_characteristic_times, 

822 show_current_characteristic_times=show_current_characteristic_times, 

823 zoom_to_offer_window=zoom_to_offer_window, 

824 zoom_to_prediction_interval=zoom_to_prediction_interval, 

825 ) 

826 figs.append(dcc.Graph(id=f"graph_{variable}", figure=fig)) 

827 return figs 

828 

829 # Run the app 

830 if self.port: 

831 port = self.port 

832 else: 

833 port = get_port() 

834 webbrowser.open_new_tab(f"http://localhost:{port}") 

835 app.run(debug=False, port=port)