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

212 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-10-20 14:09 +0000

1import webbrowser 

2from typing import Optional, Union, get_args 

3 

4import pandas as pd 

5from agentlib.core.agent import AgentConfig 

6from agentlib_mpc.utils import TIME_CONVERSION, TimeConversionTypes 

7from agentlib_mpc.utils.analysis import mpc_at_time_step 

8from agentlib_mpc.utils.plotting.interactive import get_port 

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

10from plotly import graph_objects as go 

11from pydantic import FilePath 

12 

13import agentlib_flexquant.data_structures.flex_results as flex_results 

14import agentlib_flexquant.data_structures.globals as glbs 

15from agentlib_flexquant.data_structures.flex_kpis import FlexibilityKPIs 

16from agentlib_flexquant.data_structures.flex_offer import OfferStatus 

17from agentlib_flexquant.data_structures.flexquant import FlexQuantConfig 

18 

19 

20class CustomBound: 

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

22 

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

24 lower bound -- The lower bound of the variable as the name of the lower bound variable 

25 in the MPC 

26 upper bound -- The upper bound of the variable as the name of the upper bound variable 

27 in the MPC 

28 

29 """ 

30 

31 for_variable: str 

32 lower_bound: Optional[str] 

33 upper_bound: Optional[str] 

34 

35 def __init__( 

36 self, 

37 for_variable: str, 

38 lb_name: Optional[str] = None, 

39 ub_name: Optional[str] = None, 

40 ): 

41 self.for_variable = for_variable 

42 self.lower_bound = lb_name 

43 self.upper_bound = ub_name 

44 

45 

46class Dashboard(flex_results.Results): 

47 """Class for the dashboard of flexquant""" 

48 

49 # Constants for plotting variables 

50 MPC_ITERATIONS: str = "iter_count" 

51 

52 # Label for the positive and negative flexibilities 

53 label_positive: str = "positive" 

54 label_negative: str = "negative" 

55 

56 # Keys for line properties 

57 bounds_key: str = "bounds" 

58 characteristic_times_current_key: str = "characteristic_times_current" 

59 characteristic_times_accepted_key: str = "characteristic_times_accepted" 

60 

61 # Custom settings 

62 custom_bounds: list[CustomBound] = [] 

63 

64 def __init__( 

65 self, 

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

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

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

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

70 to_timescale: TimeConversionTypes = "hours", 

71 port: int = None, 

72 ): 

73 super().__init__( 

74 flex_config=flex_config, 

75 simulator_agent_config=simulator_agent_config, 

76 generated_flex_files_base_path=generated_flex_files_base_path, 

77 results=results, 

78 to_timescale=to_timescale, 

79 ) 

80 self.port = port 

81 self.current_timescale_input = self.current_timescale_of_data 

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

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

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

85 delattr(self.simulator_module_config, "model") 

86 

87 # Define line properties 

88 self.LINE_PROPERTIES: dict = { 

89 self.simulator_agent_config.id: {"color": "black",}, 

90 self.baseline_agent_config.id: {"color": "black",}, 

91 self.neg_flex_agent_config.id: {"color": "red",}, 

92 self.pos_flex_agent_config.id: {"color": "blue",}, 

93 self.bounds_key: {"color": "grey",}, 

94 self.characteristic_times_current_key: {"color": "grey", "dash": "dash",}, 

95 self.characteristic_times_accepted_key: {"color": "yellow",}, 

96 } 

97 

98 # KPIS 

99 kpis_pos = FlexibilityKPIs(direction="positive") 

100 self.kpi_names_pos = kpis_pos.get_name_dict() 

101 kpis_neg = FlexibilityKPIs(direction="negative") 

102 self.kpi_names_neg = kpis_neg.get_name_dict() 

103 

104 # Get variables for plotting 

105 # MPC stats 

106 self.plotting_variables = [self.MPC_ITERATIONS] 

107 # MPC and sim variables 

108 self.intersection_mpcs_sim = self.get_intersection_mpcs_sim() 

109 self.plotting_variables.extend( 

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

111 ) 

112 # Flexibility kpis 

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

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

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

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

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

118 

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

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

121 -- Statistics of the MPCs solver 

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

123 -- KPIs of the flexibility quantification 

124 -- Markings of the characteristic flexibility times 

125 

126 Args: 

127 custom_bounds: optional arguments to show the comfort bounds 

128 

129 """ 

130 if custom_bounds is None: 

131 self.custom_bounds = [] 

132 elif isinstance(custom_bounds, CustomBound): 

133 self.custom_bounds = [custom_bounds] 

134 else: 

135 self.custom_bounds = custom_bounds 

136 

137 # Plotting functions 

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

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

140 

141 Args: 

142 fig: the figure to be updated 

143 variable: the statics variable to be plotted 

144 

145 Returns: 

146 The updated figure 

147 

148 """ 

149 fig.add_trace( 

150 go.Scatter( 

151 name=self.baseline_agent_config.id, 

152 x=self.df_baseline_stats.index, 

153 y=self.df_baseline_stats[variable], 

154 mode="markers", 

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

156 ) 

157 ) 

158 fig.add_trace( 

159 go.Scatter( 

160 name=self.pos_flex_agent_config.id, 

161 x=self.df_pos_flex_stats.index, 

162 y=self.df_pos_flex_stats[variable], 

163 mode="markers", 

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

165 ) 

166 ) 

167 fig.add_trace( 

168 go.Scatter( 

169 name=self.neg_flex_agent_config.id, 

170 x=self.df_neg_flex_stats.index, 

171 y=self.df_neg_flex_stats[variable], 

172 mode="markers", 

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

174 ) 

175 ) 

176 return fig 

177 

178 def plot_one_mpc_variable( 

179 fig: go.Figure, variable: str, time_step: float 

180 ) -> go.Figure: 

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

182 

183 Args: 

184 fig: the figure to be updated 

185 variable: the variable to be plotted 

186 time_step: the time step to be plotted 

187 

188 Returns: 

189 The updated figure 

190 

191 """ 

192 # Get the mpc data for the plot 

193 series_neg = mpc_at_time_step( 

194 data=self.df_neg_flex, 

195 time_step=time_step, 

196 variable=self.intersection_mpcs_sim[variable][ 

197 self.neg_flex_module_config.module_id 

198 ], 

199 index_offset=False, 

200 ) 

201 series_pos = mpc_at_time_step( 

202 data=self.df_pos_flex, 

203 time_step=time_step, 

204 variable=self.intersection_mpcs_sim[variable][ 

205 self.pos_flex_module_config.module_id 

206 ], 

207 index_offset=False, 

208 ) 

209 series_bas = mpc_at_time_step( 

210 data=self.df_baseline, 

211 time_step=time_step, 

212 variable=self.intersection_mpcs_sim[variable][ 

213 self.baseline_module_config.module_id 

214 ], 

215 index_offset=False, 

216 ) 

217 

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

219 """ shift the index of the series """ 

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

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

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

223 s[ind] = val 

224 s.sort_index(inplace=True) 

225 return s 

226 

227 # Manage nans 

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

229 if variable in [ 

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

231 ]: 

232 series.dropna(inplace=True) 

233 series = _add_step_to_data(s=series) 

234 series.dropna(inplace=True) 

235 

236 # Plot the data 

237 try: 

238 df_sim = self.df_simulation[ 

239 self.intersection_mpcs_sim[variable][ 

240 self.simulator_module_config.module_id 

241 ] 

242 ] 

243 fig.add_trace( 

244 go.Scatter( 

245 name=self.simulator_agent_config.id, 

246 x=df_sim.index, 

247 y=df_sim, 

248 mode="lines", 

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

250 zorder=2, 

251 ) 

252 ) 

253 except KeyError: 

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

255 pass 

256 

257 fig.add_trace( 

258 go.Scatter( 

259 name=self.baseline_agent_config.id, 

260 x=series_bas.index, 

261 y=series_bas, 

262 mode="lines", 

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

264 | {"dash": "dash"}, 

265 zorder=3, 

266 ) 

267 ) 

268 fig.add_trace( 

269 go.Scatter( 

270 name=self.neg_flex_agent_config.id, 

271 x=series_neg.index, 

272 y=series_neg, 

273 mode="lines", 

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

275 | {"dash": "dash"}, 

276 zorder=4, 

277 ) 

278 ) 

279 fig.add_trace( 

280 go.Scatter( 

281 name=self.pos_flex_agent_config.id, 

282 x=series_pos.index, 

283 y=series_pos, 

284 mode="lines", 

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

286 | {"dash": "dash"}, 

287 zorder=4, 

288 ) 

289 ) 

290 

291 # Get the data for the bounds 

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

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

294 

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

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

297 try: 

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

299 except KeyError: 

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

301 else: 

302 bound = None 

303 return bound 

304 

305 df_lb = None 

306 df_ub = None 

307 for custom_bound in self.custom_bounds: 

308 if variable == custom_bound.for_variable: 

309 df_lb = _get_bound(custom_bound.lower_bound) 

310 df_ub = _get_bound(custom_bound.upper_bound) 

311 if variable in [ 

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

313 ]: 

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

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

316 

317 # Plot bounds 

318 if df_lb is not None: 

319 fig.add_trace( 

320 go.Scatter( 

321 name="Lower bound", 

322 x=df_lb.index, 

323 y=df_lb, 

324 mode="lines", 

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

326 zorder=1, 

327 ) 

328 ) 

329 if df_ub is not None: 

330 fig.add_trace( 

331 go.Scatter( 

332 name="Upper bound", 

333 x=df_ub.index, 

334 y=df_ub, 

335 mode="lines", 

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

337 zorder=1, 

338 ) 

339 ) 

340 

341 return fig 

342 

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

344 """Plot the flexibility kpi. 

345 

346 Args: 

347 fig: the figure to be updated 

348 variable: the kpi variable to be plotted 

349 

350 Returns: 

351 The updated figure 

352 

353 """ 

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

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

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

357 return 

358 fig.add_trace( 

359 go.Scatter( 

360 name=self.label_positive, 

361 x=df_ind.index, 

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

363 mode="lines+markers", 

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

365 ) 

366 ) 

367 fig.add_trace( 

368 go.Scatter( 

369 name=self.label_negative, 

370 x=df_ind.index, 

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

372 mode="lines+markers", 

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

374 ) 

375 ) 

376 return fig 

377 

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

379 """Plot the market results. 

380 

381 Args: 

382 fig: the figure to be updated 

383 variable: the variable to be plotted 

384 

385 Returns: 

386 The updated figure 

387 

388 """ 

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

390 if variable in self.df_market.columns: 

391 fig.add_trace( 

392 go.Scatter( 

393 x=df_flex_market_index, 

394 y=self.df_market[variable], 

395 mode="lines+markers", 

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

397 ) 

398 ) 

399 else: 

400 pos_var = f"pos_{variable}" 

401 neg_var = f"neg_{variable}" 

402 fig.add_trace( 

403 go.Scatter( 

404 name=self.label_positive, 

405 x=df_flex_market_index, 

406 y=self.df_market[pos_var], 

407 mode="lines+markers", 

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

409 ) 

410 ) 

411 fig.add_trace( 

412 go.Scatter( 

413 name=self.label_negative, 

414 x=df_flex_market_index, 

415 y=self.df_market[neg_var], 

416 mode="lines+markers", 

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

418 ) 

419 ) 

420 return fig 

421 

422 # Marking times 

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

424 """Get the characteristic times. 

425 

426 Args: 

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

428 

429 Returns: 

430 market_time, prep_time and flex_event_duration 

431 

432 """ 

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

434 rel_market_time = ( 

435 df_characteristic_times.loc[at_time_step, glbs.MARKET_TIME] 

436 / TIME_CONVERSION[self.current_timescale_of_data] 

437 ) 

438 rel_prep_time = ( 

439 df_characteristic_times.loc[at_time_step, glbs.PREP_TIME] 

440 / TIME_CONVERSION[self.current_timescale_of_data] 

441 ) 

442 flex_event_duration = ( 

443 df_characteristic_times.loc[at_time_step, glbs.FLEX_EVENT_DURATION] 

444 / TIME_CONVERSION[self.current_timescale_of_data] 

445 ) 

446 return rel_market_time, rel_prep_time, flex_event_duration 

447 

448 def mark_time( 

449 fig: go.Figure, at_time_step: float, line_prop: dict 

450 ) -> go.Figure: 

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

452 return fig 

453 

454 def mark_characteristic_times( 

455 fig: go.Figure, offer_time: Union[float, int] = 0, line_prop: dict = None 

456 ) -> go.Figure: 

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

458 

459 Args: 

460 fig: the figure to plot the results into 

461 offer_time: When to show the markers 

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

463 

464 Returns: 

465 The updated figure 

466 

467 """ 

468 if line_prop is None: 

469 line_prop = self.LINE_PROPERTIES[self.characteristic_times_current_key] 

470 try: 

471 ( 

472 rel_market_time, 

473 rel_prep_time, 

474 flex_event_duration, 

475 ) = get_characteristic_times(offer_time) 

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

477 mark_time( 

478 fig=fig, 

479 at_time_step=offer_time + rel_market_time, 

480 line_prop=line_prop, 

481 ) 

482 mark_time( 

483 fig=fig, 

484 at_time_step=offer_time + rel_prep_time + rel_market_time, 

485 line_prop=line_prop, 

486 ) 

487 mark_time( 

488 fig=fig, 

489 at_time_step=offer_time 

490 + rel_prep_time 

491 + rel_market_time 

492 + flex_event_duration, 

493 line_prop=line_prop, 

494 ) 

495 except KeyError: 

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

497 return fig 

498 

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

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

501 if self.df_market is not None: 

502 if ( 

503 self.df_market["status"] 

504 .isin( 

505 [ 

506 OfferStatus.ACCEPTED_NEGATIVE.value, 

507 OfferStatus.ACCEPTED_POSITIVE.value, 

508 ] 

509 ) 

510 .any() 

511 ): 

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

513 pat="OfferStatus.accepted" 

514 ) 

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

516 if df_accepted_offers[i]: 

517 fig = mark_characteristic_times( 

518 fig=fig, 

519 offer_time=i[0], 

520 line_prop=self.LINE_PROPERTIES[ 

521 self.characteristic_times_accepted_key 

522 ], 

523 ) 

524 return fig 

525 

526 # Master plotting function 

527 def create_plot( 

528 variable: str, 

529 at_time_step: float, 

530 show_accepted_characteristic_times: bool = True, 

531 show_current_characteristic_times: bool = True, 

532 zoom_to_offer_window: bool = False, 

533 zoom_to_prediction_interval: bool = False, 

534 ) -> go.Figure: 

535 """Create a plot for one variable 

536 

537 Args: 

538 variable: the variable to plot 

539 at_time_step: the time_step to show the mpc predictions 

540 and the characteristic times 

541 show_accepted_characteristic_times: whether to show 

542 the accepted characteristic times 

543 show_current_characteristic_times: whether to show 

544 the current characteristic times 

545 zoom_to_offer_window: whether to zoom to offer window 

546 zoom_to_prediction_interval: wether to zoom to prediction interval 

547 

548 Returns: 

549 The created figure 

550 

551 """ 

552 # Create the figure 

553 fig = go.Figure() 

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

555 if show_accepted_characteristic_times: 

556 mark_characteristic_times_of_accepted_offers(fig=fig) 

557 

558 # Plot variable 

559 if variable in self.df_baseline_stats.columns: 

560 plot_mpc_stats(fig=fig, variable=variable) 

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

562 plot_one_mpc_variable( 

563 fig=fig, variable=variable, time_step=at_time_step 

564 ) 

565 if show_current_characteristic_times: 

566 mark_characteristic_times(fig=fig, offer_time=at_time_step) 

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

568 plot_flexibility_kpi(fig=fig, variable=variable) 

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

570 plot_market_results(fig=fig, variable=variable) 

571 else: 

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

573 

574 # Set layout 

575 if zoom_to_offer_window: 

576 ( 

577 rel_market_time, 

578 rel_prep_time, 

579 flex_event_duration, 

580 ) = get_characteristic_times(at_time_step) 

581 ts = ( 

582 self.baseline_module_config.time_step 

583 / TIME_CONVERSION[self.current_timescale_of_data] 

584 ) 

585 

586 xlim_left = at_time_step 

587 xlim_right = ( 

588 at_time_step 

589 + rel_market_time 

590 + rel_prep_time 

591 + flex_event_duration 

592 + 4 * ts 

593 ) 

594 elif zoom_to_prediction_interval: 

595 xlim_left = at_time_step 

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

597 else: 

598 xlim_left = self.df_simulation.index[0] 

599 xlim_right = ( 

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

601 ) 

602 

603 fig.update_layout( 

604 yaxis_title=variable, 

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

606 xaxis_range=[xlim_left, xlim_right], 

607 height=350, 

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

609 ) 

610 fig.update_xaxes( 

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

612 * self.baseline_module_config.time_step 

613 / TIME_CONVERSION[self.current_timescale_of_data] 

614 ) 

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

616 return fig 

617 

618 # Create the app 

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

620 app.layout = [ 

621 html.H1("Results"), 

622 html.H3("Settings"), 

623 html.Div( 

624 children=[ 

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

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

627 "discretization_options" 

628 ].items() 

629 ] 

630 ), 

631 # Options 

632 html.Div( 

633 children=[ 

634 html.H3("Options"), 

635 html.Div( 

636 children=[ 

637 dcc.Checklist( 

638 id="accepted_characteristic_times", 

639 options=[ 

640 { 

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

642 "value": True, 

643 } 

644 ], 

645 value=[True], 

646 style={ 

647 "display": "inline-block", 

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

649 }, 

650 ), 

651 dcc.Checklist( 

652 id="current_characteristic_times", 

653 options=[ 

654 { 

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

656 "value": True, 

657 } 

658 ], 

659 value=[True], 

660 style={ 

661 "display": "inline-block", 

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

663 }, 

664 ), 

665 ], 

666 ), 

667 html.Div( 

668 children=[ 

669 dcc.Checklist( 

670 id="zoom_to_offer_window", 

671 options=[ 

672 { 

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

674 "value": False, 

675 } 

676 ], 

677 style={ 

678 "display": "inline-block", 

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

680 }, 

681 ), 

682 dcc.Checklist( 

683 id="zoom_to_prediction_interval", 

684 options=[ 

685 { 

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

687 "value": False, 

688 } 

689 ], 

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

691 ), 

692 ], 

693 ), 

694 # Time input 

695 html.Div( 

696 children=[ 

697 html.H3( 

698 children=f"Time:", 

699 style={ 

700 "display": "inline-block", 

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

702 }, 

703 ), 

704 dcc.Input( 

705 id="time_typing", 

706 type="number", 

707 min=0, 

708 max=1, 

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

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

711 ), 

712 dcc.Dropdown( 

713 id="time_unit", 

714 options=get_args(TimeConversionTypes), 

715 value=self.current_timescale_input, 

716 style={ 

717 "display": "inline-block", 

718 "verticalAlign": "middle", 

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

720 "width": "100px", 

721 }, 

722 ), 

723 ], 

724 ), 

725 dcc.Slider( 

726 id="time_slider", 

727 min=0, 

728 max=1, 

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

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

731 marks=None, 

732 updatemode="drag", 

733 ), 

734 ], 

735 style={ 

736 "width": "88%", 

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

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

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

740 "position": "sticky", 

741 "top": "0", 

742 "overflow-y": "visible", 

743 "z-index": "100", 

744 "background-color": "white", 

745 }, 

746 ), 

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

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

749 ] 

750 

751 # Callbacks 

752 # Update the time value or the time unit 

753 @callback( 

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

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

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

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

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

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

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

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

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

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

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

765 ) 

766 def update_time_index_of_input( 

767 time_typing: float, time_slider: float, time_unit: TimeConversionTypes 

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

769 # get trigger id 

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

771 

772 # Get the value for the sliders 

773 if trigger_id == "time_slider": 

774 value = time_slider 

775 elif trigger_id == "time_unit": 

776 value = ( 

777 time_typing 

778 * TIME_CONVERSION[self.current_timescale_input] 

779 / TIME_CONVERSION[time_unit] 

780 ) 

781 else: 

782 value = time_typing 

783 

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

785 if trigger_id == "time_unit": 

786 self.convert_timescale_of_dataframe_index(to_timescale=time_unit) 

787 

788 # Get the index for the slider types 

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

790 minimum = times[0] 

791 maximum = times[-1] 

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

793 

794 self.current_timescale_input = time_unit 

795 

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

797 

798 # Update the graphs 

799 @callback( 

800 Output( 

801 component_id="graphs_container_variables", component_property="children" 

802 ), 

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

804 Input( 

805 component_id="accepted_characteristic_times", component_property="value" 

806 ), 

807 Input( 

808 component_id="current_characteristic_times", component_property="value" 

809 ), 

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

811 Input( 

812 component_id="zoom_to_prediction_interval", component_property="value" 

813 ), 

814 ) 

815 def update_graph( 

816 at_time_step: float, 

817 show_accepted_characteristic_times: bool, 

818 show_current_characteristic_times: bool, 

819 zoom_to_offer_window: bool, 

820 zoom_to_prediction_interval: bool, 

821 ) -> list[dcc.Graph]: 

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

823 figs = [] 

824 for variable in self.plotting_variables: 

825 fig = create_plot( 

826 variable=variable, 

827 at_time_step=at_time_step, 

828 show_accepted_characteristic_times=show_accepted_characteristic_times, 

829 show_current_characteristic_times=show_current_characteristic_times, 

830 zoom_to_offer_window=zoom_to_offer_window, 

831 zoom_to_prediction_interval=zoom_to_prediction_interval, 

832 ) 

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

834 return figs 

835 

836 # Run the app 

837 if self.port: 

838 port = self.port 

839 else: 

840 port = get_port() 

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

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