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

212 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-08-15 15:25 +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 # Plotting functions 

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

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

150 

151 Args: 

152 fig: the figure to be updated 

153 variable: the statics variable to be plotted 

154 

155 Returns: 

156 The updated figure 

157 

158 """ 

159 fig.add_trace( 

160 go.Scatter( 

161 name=self.baseline_agent_config.id, 

162 x=self.df_baseline_stats.index, 

163 y=self.df_baseline_stats[variable], 

164 mode="markers", 

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

166 ) 

167 ) 

168 fig.add_trace( 

169 go.Scatter( 

170 name=self.pos_flex_agent_config.id, 

171 x=self.df_pos_flex_stats.index, 

172 y=self.df_pos_flex_stats[variable], 

173 mode="markers", 

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

175 ) 

176 ) 

177 fig.add_trace( 

178 go.Scatter( 

179 name=self.neg_flex_agent_config.id, 

180 x=self.df_neg_flex_stats.index, 

181 y=self.df_neg_flex_stats[variable], 

182 mode="markers", 

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

184 ) 

185 ) 

186 return fig 

187 

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

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

190 

191 Args: 

192 fig: the figure to be updated 

193 variable: the variable to be plotted 

194 time_step: the time step to be plotted 

195 

196 Returns: 

197 The updated figure 

198 

199 """ 

200 # Get the mpc data for the plot 

201 series_neg = mpc_at_time_step( 

202 data=self.df_neg_flex, 

203 time_step=time_step, 

204 variable=self.intersection_mpcs_sim[variable][ 

205 self.neg_flex_module_config.module_id 

206 ], 

207 index_offset=False, 

208 ) 

209 series_pos = mpc_at_time_step( 

210 data=self.df_pos_flex, 

211 time_step=time_step, 

212 variable=self.intersection_mpcs_sim[variable][ 

213 self.pos_flex_module_config.module_id 

214 ], 

215 index_offset=False, 

216 ) 

217 series_bas = mpc_at_time_step( 

218 data=self.df_baseline, 

219 time_step=time_step, 

220 variable=self.intersection_mpcs_sim[variable][ 

221 self.baseline_module_config.module_id 

222 ], 

223 index_offset=False, 

224 ) 

225 

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

227 """ shift the index of the series """ 

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

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

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

231 s[ind] = val 

232 s.sort_index(inplace=True) 

233 return s 

234 

235 # Manage nans 

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

237 if variable in [ 

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

239 ]: 

240 series.dropna(inplace=True) 

241 series = _add_step_to_data(s=series) 

242 series.dropna(inplace=True) 

243 

244 # Plot the data 

245 try: 

246 df_sim = self.df_simulation[ 

247 self.intersection_mpcs_sim[variable][ 

248 self.simulator_module_config.module_id 

249 ] 

250 ] 

251 fig.add_trace( 

252 go.Scatter( 

253 name=self.simulator_agent_config.id, 

254 x=df_sim.index, 

255 y=df_sim, 

256 mode="lines", 

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

258 zorder=2, 

259 ) 

260 ) 

261 except KeyError: 

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

263 pass 

264 

265 fig.add_trace( 

266 go.Scatter( 

267 name=self.baseline_agent_config.id, 

268 x=series_bas.index, 

269 y=series_bas, 

270 mode="lines", 

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

272 | {"dash": "dash"}, 

273 zorder=3, 

274 ) 

275 ) 

276 fig.add_trace( 

277 go.Scatter( 

278 name=self.neg_flex_agent_config.id, 

279 x=series_neg.index, 

280 y=series_neg, 

281 mode="lines", 

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

283 | {"dash": "dash"}, 

284 zorder=4, 

285 ) 

286 ) 

287 fig.add_trace( 

288 go.Scatter( 

289 name=self.pos_flex_agent_config.id, 

290 x=series_pos.index, 

291 y=series_pos, 

292 mode="lines", 

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

294 | {"dash": "dash"}, 

295 zorder=4, 

296 ) 

297 ) 

298 

299 # Get the data for the bounds 

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

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

302 

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

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

305 try: 

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

307 except KeyError: 

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

309 else: 

310 bound = None 

311 return bound 

312 

313 df_lb = None 

314 df_ub = None 

315 for custom_bound in self.custom_bounds: 

316 if variable == custom_bound.for_variable: 

317 df_lb = _get_bound(custom_bound.lower_bound) 

318 df_ub = _get_bound(custom_bound.upper_bound) 

319 if variable in [ 

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

321 ]: 

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

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

324 

325 # Plot bounds 

326 if df_lb is not None: 

327 fig.add_trace( 

328 go.Scatter( 

329 name="Lower bound", 

330 x=df_lb.index, 

331 y=df_lb, 

332 mode="lines", 

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

334 zorder=1, 

335 ) 

336 ) 

337 if df_ub is not None: 

338 fig.add_trace( 

339 go.Scatter( 

340 name="Upper bound", 

341 x=df_ub.index, 

342 y=df_ub, 

343 mode="lines", 

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

345 zorder=1, 

346 ) 

347 ) 

348 

349 return fig 

350 

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

352 """Plot the flexibility kpi. 

353 

354 Args: 

355 fig: the figure to be updated 

356 variable: the kpi variable to be plotted 

357 

358 Returns: 

359 The updated figure 

360 

361 """ 

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

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

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

365 return 

366 fig.add_trace( 

367 go.Scatter( 

368 name=self.label_positive, 

369 x=df_ind.index, 

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

371 mode="lines+markers", 

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

373 ) 

374 ) 

375 fig.add_trace( 

376 go.Scatter( 

377 name=self.label_negative, 

378 x=df_ind.index, 

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

380 mode="lines+markers", 

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

382 ) 

383 ) 

384 return fig 

385 

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

387 """Plot the market results. 

388 

389 Args: 

390 fig: the figure to be updated 

391 variable: the variable to be plotted 

392 

393 Returns: 

394 The updated figure 

395 

396 """ 

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

398 if variable in self.df_market.columns: 

399 fig.add_trace( 

400 go.Scatter( 

401 x=df_flex_market_index, 

402 y=self.df_market[variable], 

403 mode="lines+markers", 

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

405 ) 

406 ) 

407 else: 

408 pos_var = f"pos_{variable}" 

409 neg_var = f"neg_{variable}" 

410 fig.add_trace( 

411 go.Scatter( 

412 name=self.label_positive, 

413 x=df_flex_market_index, 

414 y=self.df_market[pos_var], 

415 mode="lines+markers", 

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

417 ) 

418 ) 

419 fig.add_trace( 

420 go.Scatter( 

421 name=self.label_negative, 

422 x=df_flex_market_index, 

423 y=self.df_market[neg_var], 

424 mode="lines+markers", 

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

426 ) 

427 ) 

428 return fig 

429 

430 # Marking times 

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

432 """Get the characteristic times. 

433 

434 Args: 

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

436 

437 Returns: 

438 market_time, prep_time and flex_event_duration 

439 

440 """ 

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

442 rel_market_time = ( 

443 df_characteristic_times.loc[at_time_step, glbs.MARKET_TIME] 

444 / TIME_CONVERSION[self.current_timescale_of_data] 

445 ) 

446 rel_prep_time = ( 

447 df_characteristic_times.loc[at_time_step, glbs.PREP_TIME] 

448 / TIME_CONVERSION[self.current_timescale_of_data] 

449 ) 

450 flex_event_duration = ( 

451 df_characteristic_times.loc[at_time_step, glbs.FLEX_EVENT_DURATION] 

452 / TIME_CONVERSION[self.current_timescale_of_data] 

453 ) 

454 return rel_market_time, rel_prep_time, flex_event_duration 

455 

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

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

458 return fig 

459 

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

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

462 

463 Args: 

464 fig: the figure to plot the results into 

465 offer_time: When to show the markers 

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

467 

468 Returns: 

469 The updated figure 

470 

471 """ 

472 if line_prop is None: 

473 line_prop = self.LINE_PROPERTIES[self.characteristic_times_current_key] 

474 try: 

475 rel_market_time, rel_prep_time, flex_event_duration = ( 

476 get_characteristic_times(offer_time) 

477 ) 

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

479 mark_time( 

480 fig=fig, 

481 at_time_step=offer_time + rel_market_time, 

482 line_prop=line_prop, 

483 ) 

484 mark_time( 

485 fig=fig, 

486 at_time_step=offer_time + rel_prep_time + rel_market_time, 

487 line_prop=line_prop, 

488 ) 

489 mark_time( 

490 fig=fig, 

491 at_time_step=offer_time 

492 + rel_prep_time 

493 + rel_market_time 

494 + flex_event_duration, 

495 line_prop=line_prop, 

496 ) 

497 except KeyError: 

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

499 return fig 

500 

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

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

503 if self.df_market is not None: 

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

505 OfferStatus.accepted_negative.value, 

506 OfferStatus.accepted_positive.value, 

507 ] 

508 ).any()): 

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

510 pat="OfferStatus.accepted" 

511 ) 

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

513 if df_accepted_offers[i]: 

514 fig = mark_characteristic_times( 

515 fig=fig, 

516 offer_time=i[0], 

517 line_prop=self.LINE_PROPERTIES[ 

518 self.characteristic_times_accepted_key 

519 ], 

520 ) 

521 return fig 

522 

523 # Master plotting function 

524 def create_plot( 

525 variable: str, 

526 at_time_step: float, 

527 show_accepted_characteristic_times: bool = True, 

528 show_current_characteristic_times: bool = True, 

529 zoom_to_offer_window: bool = False, 

530 zoom_to_prediction_interval: bool = False, 

531 ) -> go.Figure: 

532 """Create a plot for one variable 

533 

534 Args: 

535 variable: the variable to plot 

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

537 show_accepted_characteristic_times: whether to show the accepted characteristic times 

538 show_current_characteristic_times: whether to show the current characteristic times 

539 zoom_to_offer_window: whether to zoom to offer window 

540 zoom_to_prediction_interval: wether to zoom to prediction interval 

541 

542 Returns: 

543 The created figure 

544 

545 """ 

546 # Create the figure 

547 fig = go.Figure() 

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

549 if show_accepted_characteristic_times: 

550 mark_characteristic_times_of_accepted_offers(fig=fig) 

551 

552 # Plot variable 

553 if variable in self.df_baseline_stats.columns: 

554 plot_mpc_stats(fig=fig, variable=variable) 

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

556 plot_one_mpc_variable( 

557 fig=fig, variable=variable, time_step=at_time_step 

558 ) 

559 if show_current_characteristic_times: 

560 mark_characteristic_times(fig=fig, offer_time=at_time_step) 

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

562 plot_flexibility_kpi(fig=fig, variable=variable) 

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

564 plot_market_results(fig=fig, variable=variable) 

565 else: 

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

567 

568 # Set layout 

569 if zoom_to_offer_window: 

570 rel_market_time, rel_prep_time, flex_event_duration = ( 

571 get_characteristic_times(at_time_step) 

572 ) 

573 ts = ( 

574 self.baseline_module_config.time_step 

575 / TIME_CONVERSION[self.current_timescale_of_data] 

576 ) 

577 

578 xlim_left = at_time_step 

579 xlim_right = ( 

580 at_time_step 

581 + rel_market_time 

582 + rel_prep_time 

583 + flex_event_duration 

584 + 4 * ts 

585 ) 

586 elif zoom_to_prediction_interval: 

587 xlim_left = at_time_step 

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

589 else: 

590 xlim_left = self.df_simulation.index[0] 

591 xlim_right = ( 

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

593 ) 

594 

595 fig.update_layout( 

596 yaxis_title=variable, 

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

598 xaxis_range=[xlim_left, xlim_right], 

599 height=350, 

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

601 ) 

602 fig.update_xaxes( 

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

604 * self.baseline_module_config.time_step 

605 / TIME_CONVERSION[self.current_timescale_of_data] 

606 ) 

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

608 return fig 

609 

610 # Create the app 

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

612 app.layout = [ 

613 html.H1("Results"), 

614 html.H3("Settings"), 

615 html.Div( 

616 children=[ 

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

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

619 "discretization_options" 

620 ].items() 

621 ] 

622 ), 

623 # Options 

624 html.Div( 

625 children=[ 

626 html.H3("Options"), 

627 html.Div( 

628 children=[ 

629 dcc.Checklist( 

630 id="accepted_characteristic_times", 

631 options=[ 

632 { 

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

634 "value": True, 

635 } 

636 ], 

637 value=[True], 

638 style={ 

639 "display": "inline-block", 

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

641 }, 

642 ), 

643 dcc.Checklist( 

644 id="current_characteristic_times", 

645 options=[ 

646 { 

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

648 "value": True, 

649 } 

650 ], 

651 value=[True], 

652 style={ 

653 "display": "inline-block", 

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

655 }, 

656 ), 

657 ], 

658 ), 

659 html.Div( 

660 children=[ 

661 dcc.Checklist( 

662 id="zoom_to_offer_window", 

663 options=[ 

664 { 

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

666 "value": False, 

667 } 

668 ], 

669 style={ 

670 "display": "inline-block", 

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

672 }, 

673 ), 

674 dcc.Checklist( 

675 id="zoom_to_prediction_interval", 

676 options=[ 

677 { 

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

679 "value": False, 

680 } 

681 ], 

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

683 ), 

684 ], 

685 ), 

686 # Time input 

687 html.Div( 

688 children=[ 

689 html.H3( 

690 children=f"Time:", 

691 style={ 

692 "display": "inline-block", 

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

694 }, 

695 ), 

696 dcc.Input( 

697 id="time_typing", 

698 type="number", 

699 min=0, 

700 max=1, 

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

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

703 ), 

704 dcc.Dropdown( 

705 id="time_unit", 

706 options=get_args(TimeConversionTypes), 

707 value=self.current_timescale_input, 

708 style={ 

709 "display": "inline-block", 

710 "verticalAlign": "middle", 

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

712 "width": "100px", 

713 }, 

714 ), 

715 ], 

716 ), 

717 dcc.Slider( 

718 id="time_slider", 

719 min=0, 

720 max=1, 

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

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

723 marks=None, 

724 updatemode="drag", 

725 ), 

726 ], 

727 style={ 

728 "width": "88%", 

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

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

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

732 "position": "sticky", 

733 "top": "0", 

734 "overflow-y": "visible", 

735 "z-index": "100", 

736 "background-color": "white", 

737 }, 

738 ), 

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

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

741 ] 

742 

743 # Callbacks 

744 # Update the time value or the time unit 

745 @callback( 

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

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

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

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

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

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

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

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

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

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

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

757 ) 

758 def update_time_index_of_input( 

759 time_typing: float, time_slider: float, time_unit: TimeConversionTypes 

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

761 # get trigger id 

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

763 

764 # Get the value for the sliders 

765 if trigger_id == "time_slider": 

766 value = time_slider 

767 elif trigger_id == "time_unit": 

768 value = ( 

769 time_typing 

770 * TIME_CONVERSION[self.current_timescale_input] 

771 / TIME_CONVERSION[time_unit] 

772 ) 

773 else: 

774 value = time_typing 

775 

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

777 if trigger_id == "time_unit": 

778 self.convert_timescale_of_dataframe_index(to_timescale=time_unit) 

779 

780 # Get the index for the slider types 

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

782 minimum = times[0] 

783 maximum = times[-1] 

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

785 

786 self.current_timescale_input = time_unit 

787 

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

789 

790 # Update the graphs 

791 @callback( 

792 Output( 

793 component_id="graphs_container_variables", component_property="children" 

794 ), 

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

796 Input( 

797 component_id="accepted_characteristic_times", component_property="value" 

798 ), 

799 Input( 

800 component_id="current_characteristic_times", component_property="value" 

801 ), 

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

803 Input( 

804 component_id="zoom_to_prediction_interval", component_property="value" 

805 ), 

806 ) 

807 def update_graph( 

808 at_time_step: float, 

809 show_accepted_characteristic_times: bool, 

810 show_current_characteristic_times: bool, 

811 zoom_to_offer_window: bool, 

812 zoom_to_prediction_interval: bool, 

813 ) -> list[dcc.Graph]: 

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

815 figs = [] 

816 for variable in self.plotting_variables: 

817 fig = create_plot( 

818 variable=variable, 

819 at_time_step=at_time_step, 

820 show_accepted_characteristic_times=show_accepted_characteristic_times, 

821 show_current_characteristic_times=show_current_characteristic_times, 

822 zoom_to_offer_window=zoom_to_offer_window, 

823 zoom_to_prediction_interval=zoom_to_prediction_interval, 

824 ) 

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

826 return figs 

827 

828 # Run the app 

829 if self.port: 

830 port = self.port 

831 else: 

832 port = get_port() 

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

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