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

212 statements  

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

1from typing import get_args, Union, Optional 

2from pydantic import FilePath 

3import pandas as pd 

4 

5import webbrowser 

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

7from plotly import graph_objects as go 

8 

9from agentlib.core.agent import AgentConfig 

10 

11from agentlib_mpc.utils import TimeConversionTypes, TIME_CONVERSION 

12from agentlib_mpc.utils.analysis import mpc_at_time_step 

13from agentlib_mpc.utils.plotting.interactive import get_port 

14 

15import agentlib_flexquant.data_structures.globals as glbs 

16import agentlib_flexquant.data_structures.flex_results as flex_results 

17from agentlib_flexquant.data_structures.flexquant import FlexQuantConfig 

18from agentlib_flexquant.data_structures.flex_kpis import FlexibilityKPIs 

19from agentlib_flexquant.data_structures.flex_offer import OfferStatus 

20 

21 

22class CustomBound: 

23 """ 

24 Dataclass to let the user define custom bounds for the mpc variables 

25 

26 var_name -- The name of the variable to plot the bounds into 

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

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

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 """ 

48 Class for the dashboard of flexquant 

49 """ 

50 

51 # Constants for plotting variables 

52 MPC_ITERATIONS: str = "iter_count" 

53 

54 # Label for the positive and negative flexibilities 

55 label_positive: str = "positive" 

56 label_negative: str = "negative" 

57 

58 # Keys for line properties 

59 bounds_key: str = "bounds" 

60 characteristic_times_current_key: str = "characteristic_times_current" 

61 characteristic_times_accepted_key: str = "characteristic_times_accepted" 

62 

63 # Custom settings 

64 custom_bounds: list[CustomBound] = [] 

65 

66 def __init__( 

67 self, 

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

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

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

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

72 to_timescale: TimeConversionTypes = "hours", 

73 port: int = None 

74 ): 

75 super().__init__( 

76 flex_config=flex_config, 

77 simulator_agent_config=simulator_agent_config, 

78 generated_flex_files_base_path=generated_flex_files_base_path, 

79 results=results, 

80 to_timescale=to_timescale, 

81 ) 

82 self.port = port 

83 self.current_timescale_input = self.current_timescale_of_data 

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

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

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

87 delattr(self.simulator_module_config, "model") 

88 

89 # Define line properties 

90 self.LINE_PROPERTIES: dict = { 

91 self.simulator_agent_config.id: { 

92 "color": "black", 

93 }, 

94 self.baseline_agent_config.id: { 

95 "color": "black", 

96 }, 

97 self.neg_flex_agent_config.id: { 

98 "color": "red", 

99 }, 

100 self.pos_flex_agent_config.id: { 

101 "color": "blue", 

102 }, 

103 self.bounds_key: { 

104 "color": "grey", 

105 }, 

106 self.characteristic_times_current_key: { 

107 "color": "grey", 

108 "dash": "dash", 

109 }, 

110 self.characteristic_times_accepted_key: { 

111 "color": "yellow", 

112 }, 

113 } 

114 

115 # KPIS 

116 kpis_pos = FlexibilityKPIs(direction="positive") 

117 self.kpi_names_pos = kpis_pos.get_name_dict() 

118 kpis_neg = FlexibilityKPIs(direction="negative") 

119 self.kpi_names_neg = kpis_neg.get_name_dict() 

120 

121 # Get variables for plotting 

122 # MPC stats 

123 self.plotting_variables = [self.MPC_ITERATIONS] 

124 # MPC and sim variables 

125 self.intersection_mpcs_sim = self.get_intersection_mpcs_sim() 

126 self.plotting_variables.extend( 

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

128 ) 

129 # Flexibility kpis 

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

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

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

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

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

135 

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

137 """ 

138 Shows the dashboard in a web browser containing: 

139 -- Statistics of the MPCs solver 

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

141 -- KPIs of the flexibility quantification 

142 -- Markings of the characteristic flexibility times 

143 

144 Optional arguments to show the comfort bounds: 

145 -- temperature_var_name: The name of the temperature variable in the MPC to plot the comfort bounds into 

146 -- ub_comfort_var_name: The name of the upper comfort bound variable in the MPC 

147 -- lb_comfort_var_name: The name of the lower comfort bound variable in the MPC 

148 """ 

149 if custom_bounds is None: 

150 self.custom_bounds = [] 

151 elif isinstance(custom_bounds, CustomBound): 

152 self.custom_bounds = [custom_bounds] 

153 else: 

154 self.custom_bounds = custom_bounds 

155 

156 # Plotting functions 

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

158 fig.add_trace( 

159 go.Scatter( 

160 name=self.baseline_agent_config.id, 

161 x=self.df_baseline_stats.index, 

162 y=self.df_baseline_stats[variable], 

163 mode="markers", 

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

165 ) 

166 ) 

167 fig.add_trace( 

168 go.Scatter( 

169 name=self.pos_flex_agent_config.id, 

170 x=self.df_pos_flex_stats.index, 

171 y=self.df_pos_flex_stats[variable], 

172 mode="markers", 

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

174 ) 

175 ) 

176 fig.add_trace( 

177 go.Scatter( 

178 name=self.neg_flex_agent_config.id, 

179 x=self.df_neg_flex_stats.index, 

180 y=self.df_neg_flex_stats[variable], 

181 mode="markers", 

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

183 ) 

184 ) 

185 return fig 

186 

187 def plot_one_mpc_variable( 

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

189 ) -> go.Figure: 

190 # Get the mpc data for the plot 

191 series_neg = mpc_at_time_step( 

192 data=self.df_neg_flex, 

193 time_step=time_step, 

194 variable=self.intersection_mpcs_sim[variable][ 

195 self.neg_flex_module_config.module_id 

196 ], 

197 index_offset=False, 

198 ) 

199 series_pos = mpc_at_time_step( 

200 data=self.df_pos_flex, 

201 time_step=time_step, 

202 variable=self.intersection_mpcs_sim[variable][ 

203 self.pos_flex_module_config.module_id 

204 ], 

205 index_offset=False, 

206 ) 

207 series_bas = mpc_at_time_step( 

208 data=self.df_baseline, 

209 time_step=time_step, 

210 variable=self.intersection_mpcs_sim[variable][ 

211 self.baseline_module_config.module_id 

212 ], 

213 index_offset=False, 

214 ) 

215 

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

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

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

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

220 s[ind] = val 

221 s.sort_index(inplace=True) 

222 return s 

223 

224 # Manage nans 

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

226 if variable in [ 

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

228 ]: 

229 series.dropna(inplace=True) 

230 series = _add_step_to_data(s=series) 

231 series.dropna(inplace=True) 

232 

233 # Plot the data 

234 try: 

235 df_sim = self.df_simulation[ 

236 self.intersection_mpcs_sim[variable][ 

237 self.simulator_module_config.module_id 

238 ] 

239 ] 

240 fig.add_trace( 

241 go.Scatter( 

242 name=self.simulator_agent_config.id, 

243 x=df_sim.index, 

244 y=df_sim, 

245 mode="lines", 

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

247 zorder=2, 

248 ) 

249 ) 

250 except KeyError: 

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

252 fig.add_trace( 

253 go.Scatter( 

254 name=self.baseline_agent_config.id, 

255 x=series_bas.index, 

256 y=series_bas, 

257 mode="lines", 

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

259 | {"dash": "dash"}, 

260 zorder=3, 

261 ) 

262 ) 

263 fig.add_trace( 

264 go.Scatter( 

265 name=self.neg_flex_agent_config.id, 

266 x=series_neg.index, 

267 y=series_neg, 

268 mode="lines", 

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

270 | {"dash": "dash"}, 

271 zorder=4, 

272 ) 

273 ) 

274 fig.add_trace( 

275 go.Scatter( 

276 name=self.pos_flex_agent_config.id, 

277 x=series_pos.index, 

278 y=series_pos, 

279 mode="lines", 

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

281 | {"dash": "dash"}, 

282 zorder=4, 

283 ) 

284 ) 

285 

286 # Get the data for the bounds 

287 def _get_mpc_series(var_type: str, var_name: str): 

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

289 

290 def _get_bound(var_name: str): 

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

292 try: 

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

294 except KeyError: 

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

296 else: 

297 bound = None 

298 return bound 

299 

300 df_lb = None 

301 df_ub = None 

302 for custom_bound in self.custom_bounds: 

303 if variable == custom_bound.for_variable: 

304 df_lb = _get_bound(custom_bound.lower_bound) 

305 df_ub = _get_bound(custom_bound.upper_bound) 

306 if variable in [ 

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

308 ]: 

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

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

311 

312 # Plot bounds 

313 if df_lb is not None: 

314 fig.add_trace( 

315 go.Scatter( 

316 name="Lower bound", 

317 x=df_lb.index, 

318 y=df_lb, 

319 mode="lines", 

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

321 zorder=1, 

322 ) 

323 ) 

324 if df_ub is not None: 

325 fig.add_trace( 

326 go.Scatter( 

327 name="Upper bound", 

328 x=df_ub.index, 

329 y=df_ub, 

330 mode="lines", 

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

332 zorder=1, 

333 ) 

334 ) 

335 

336 return fig 

337 

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

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

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

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

342 return 

343 fig.add_trace( 

344 go.Scatter( 

345 name=self.label_positive, 

346 x=df_ind.index, 

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

348 mode="lines+markers", 

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

350 ) 

351 ) 

352 fig.add_trace( 

353 go.Scatter( 

354 name=self.label_negative, 

355 x=df_ind.index, 

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

357 mode="lines+markers", 

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

359 ) 

360 ) 

361 return fig 

362 

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

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

365 if variable in self.df_market.columns: 

366 fig.add_trace( 

367 go.Scatter( 

368 x=df_flex_market_index, 

369 y=self.df_market[variable], 

370 mode="lines+markers", 

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

372 ) 

373 ) 

374 else: 

375 pos_var = f"pos_{variable}" 

376 neg_var = f"neg_{variable}" 

377 fig.add_trace( 

378 go.Scatter( 

379 name=self.label_positive, 

380 x=df_flex_market_index, 

381 y=self.df_market[pos_var], 

382 mode="lines+markers", 

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

384 ) 

385 ) 

386 fig.add_trace( 

387 go.Scatter( 

388 name=self.label_negative, 

389 x=df_flex_market_index, 

390 y=self.df_market[neg_var], 

391 mode="lines+markers", 

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

393 ) 

394 ) 

395 return fig 

396 

397 # Marking times 

398 def get_characteristic_times(at_time_step: float) -> [int, int, int]: 

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

400 rel_market_time = ( 

401 df_characteristic_times.loc[at_time_step, glbs.MARKET_TIME] 

402 / TIME_CONVERSION[self.current_timescale_of_data] 

403 ) 

404 rel_prep_time = ( 

405 df_characteristic_times.loc[at_time_step, glbs.PREP_TIME] 

406 / TIME_CONVERSION[self.current_timescale_of_data] 

407 ) 

408 flex_event_duration = ( 

409 df_characteristic_times.loc[at_time_step, glbs.FLEX_EVENT_DURATION] 

410 / TIME_CONVERSION[self.current_timescale_of_data] 

411 ) 

412 return rel_market_time, rel_prep_time, flex_event_duration 

413 

414 def mark_time( 

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

416 ) -> go.Figure: 

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

418 return fig 

419 

420 def mark_characteristic_times( 

421 fig: go.Figure, offer_time: float, line_prop: dict = None 

422 ) -> go.Figure: 

423 """ 

424 Add markers of the characteristic times to the plot for a time step 

425 

426 Keyword arguments: 

427 fig -- The figure to plot the results into 

428 time_step -- When to show the markers 

429 line_prop -- The graphic properties of the lines as in plotly 

430 """ 

431 if line_prop is None: 

432 line_prop = self.LINE_PROPERTIES[self.characteristic_times_current_key] 

433 try: 

434 rel_market_time, rel_prep_time, flex_event_duration = ( 

435 get_characteristic_times(offer_time) 

436 ) 

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

438 mark_time( 

439 fig=fig, 

440 at_time_step=offer_time + rel_market_time, 

441 line_prop=line_prop, 

442 ) 

443 mark_time( 

444 fig=fig, 

445 at_time_step=offer_time + rel_prep_time + rel_market_time, 

446 line_prop=line_prop, 

447 ) 

448 mark_time( 

449 fig=fig, 

450 at_time_step=offer_time 

451 + rel_prep_time 

452 + rel_market_time 

453 + flex_event_duration, 

454 line_prop=line_prop, 

455 ) 

456 except KeyError: 

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

458 return fig 

459 

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

461 """ 

462 Add markers of the characteristic times for accepted offers to the plot 

463 """ 

464 if self.df_market is not None: 

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

466 OfferStatus.accepted_negative.value, 

467 OfferStatus.accepted_positive.value, 

468 ] 

469 ).any()): 

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

471 pat="OfferStatus.accepted" 

472 ) 

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

474 if df_accepted_offers[i]: 

475 fig = mark_characteristic_times( 

476 fig=fig, 

477 offer_time=i[0], 

478 line_prop=self.LINE_PROPERTIES[ 

479 self.characteristic_times_accepted_key 

480 ], 

481 ) 

482 return fig 

483 

484 # Master plotting function 

485 def create_plot( 

486 variable: str, 

487 at_time_step: float, 

488 show_accepted_characteristic_times: bool = True, 

489 show_current_characteristic_times: bool = True, 

490 zoom_to_offer_window: bool = False, 

491 zoom_to_prediction_interval: bool = False, 

492 ) -> go.Figure: 

493 """ 

494 Create a plot for one variable 

495 

496 Keyword arguments: 

497 variable -- The variable to plot 

498 time_step -- The time_step to show the mpc predictions and the characteristic times 

499 show_current_characteristic_times -- Whether to show the characteristic times 

500 """ 

501 # Create the figure 

502 fig = go.Figure() 

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

504 if show_accepted_characteristic_times: 

505 mark_characteristic_times_of_accepted_offers(fig=fig) 

506 

507 # Plot variable 

508 if variable in self.df_baseline_stats.columns: 

509 plot_mpc_stats(fig=fig, variable=variable) 

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

511 plot_one_mpc_variable( 

512 fig=fig, variable=variable, time_step=at_time_step 

513 ) 

514 if show_current_characteristic_times: 

515 mark_characteristic_times(fig=fig, offer_time=at_time_step) 

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

517 plot_flexibility_kpi(fig=fig, variable=variable) 

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

519 plot_market_results(fig=fig, variable=variable) 

520 else: 

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

522 

523 # Set layout 

524 if zoom_to_offer_window: 

525 rel_market_time, rel_prep_time, flex_event_duration = ( 

526 get_characteristic_times(at_time_step) 

527 ) 

528 ts = ( 

529 self.baseline_module_config.time_step 

530 / TIME_CONVERSION[self.current_timescale_of_data] 

531 ) 

532 

533 xlim_left = at_time_step 

534 xlim_right = ( 

535 at_time_step 

536 + rel_market_time 

537 + rel_prep_time 

538 + flex_event_duration 

539 + 4 * ts 

540 ) 

541 elif zoom_to_prediction_interval: 

542 xlim_left = at_time_step 

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

544 else: 

545 xlim_left = self.df_simulation.index[0] 

546 xlim_right = ( 

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

548 ) 

549 

550 fig.update_layout( 

551 yaxis_title=variable, 

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

553 xaxis_range=[xlim_left, xlim_right], 

554 height=350, 

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

556 ) 

557 fig.update_xaxes( 

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

559 * self.baseline_module_config.time_step 

560 / TIME_CONVERSION[self.current_timescale_of_data] 

561 ) 

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

563 return fig 

564 

565 # Create the app 

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

567 app.layout = [ 

568 html.H1("Results"), 

569 html.H3("Settings"), 

570 html.Div( 

571 children=[ 

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

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

574 "discretization_options" 

575 ].items() 

576 ] 

577 ), 

578 # Options 

579 html.Div( 

580 children=[ 

581 html.H3("Options"), 

582 html.Div( 

583 children=[ 

584 dcc.Checklist( 

585 id="accepted_characteristic_times", 

586 options=[ 

587 { 

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

589 "value": True, 

590 } 

591 ], 

592 value=[True], 

593 style={ 

594 "display": "inline-block", 

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

596 }, 

597 ), 

598 dcc.Checklist( 

599 id="current_characteristic_times", 

600 options=[ 

601 { 

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

603 "value": True, 

604 } 

605 ], 

606 value=[True], 

607 style={ 

608 "display": "inline-block", 

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

610 }, 

611 ), 

612 ], 

613 ), 

614 html.Div( 

615 children=[ 

616 dcc.Checklist( 

617 id="zoom_to_offer_window", 

618 options=[ 

619 { 

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

621 "value": False, 

622 } 

623 ], 

624 style={ 

625 "display": "inline-block", 

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

627 }, 

628 ), 

629 dcc.Checklist( 

630 id="zoom_to_prediction_interval", 

631 options=[ 

632 { 

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

634 "value": False, 

635 } 

636 ], 

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

638 ), 

639 ], 

640 ), 

641 # Time input 

642 html.Div( 

643 children=[ 

644 html.H3( 

645 children=f"Time:", 

646 style={ 

647 "display": "inline-block", 

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

649 }, 

650 ), 

651 dcc.Input( 

652 id="time_typing", 

653 type="number", 

654 min=0, 

655 max=1, 

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

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

658 ), 

659 dcc.Dropdown( 

660 id="time_unit", 

661 options=get_args(TimeConversionTypes), 

662 value=self.current_timescale_input, 

663 style={ 

664 "display": "inline-block", 

665 "verticalAlign": "middle", 

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

667 "width": "100px", 

668 }, 

669 ), 

670 ], 

671 ), 

672 dcc.Slider( 

673 id="time_slider", 

674 min=0, 

675 max=1, 

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

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

678 marks=None, 

679 updatemode="drag", 

680 ), 

681 ], 

682 style={ 

683 "width": "88%", 

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

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

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

687 "position": "sticky", 

688 "top": "0", 

689 "overflow-y": "visible", 

690 "z-index": "100", 

691 "background-color": "white", 

692 }, 

693 ), 

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

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

696 ] 

697 

698 # Callbacks 

699 # Update the time value or the time unit 

700 @callback( 

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

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

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

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

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

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

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

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

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

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

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

712 ) 

713 def update_time_index_of_input( 

714 time_typing: float, time_slider: float, time_unit: TimeConversionTypes 

715 ) -> [float]: 

716 # get trigger id 

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

718 

719 # Get the value for the sliders 

720 if trigger_id == "time_slider": 

721 value = time_slider 

722 elif trigger_id == "time_unit": 

723 value = ( 

724 time_typing 

725 * TIME_CONVERSION[self.current_timescale_input] 

726 / TIME_CONVERSION[time_unit] 

727 ) 

728 else: 

729 value = time_typing 

730 

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

732 if trigger_id == "time_unit": 

733 self.convert_timescale_of_dataframe_index(to_timescale=time_unit) 

734 

735 # Get the index for the slider types 

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

737 minimum = times[0] 

738 maximum = times[-1] 

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

740 

741 self.current_timescale_input = time_unit 

742 

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

744 

745 # Update the graphs 

746 @callback( 

747 Output( 

748 component_id="graphs_container_variables", component_property="children" 

749 ), 

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

751 Input( 

752 component_id="accepted_characteristic_times", component_property="value" 

753 ), 

754 Input( 

755 component_id="current_characteristic_times", component_property="value" 

756 ), 

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

758 Input( 

759 component_id="zoom_to_prediction_interval", component_property="value" 

760 ), 

761 ) 

762 def update_graph( 

763 at_time_step: float, 

764 show_accepted_characteristic_times: bool, 

765 show_current_characteristic_times: bool, 

766 zoom_to_offer_window: bool, 

767 zoom_to_prediction_interval: bool, 

768 ): 

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

770 figs = [] 

771 for variable in self.plotting_variables: 

772 fig = create_plot( 

773 variable=variable, 

774 at_time_step=at_time_step, 

775 show_accepted_characteristic_times=show_accepted_characteristic_times, 

776 show_current_characteristic_times=show_current_characteristic_times, 

777 zoom_to_offer_window=zoom_to_offer_window, 

778 zoom_to_prediction_interval=zoom_to_prediction_interval, 

779 ) 

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

781 return figs 

782 

783 # Run the app 

784 if self.port: 

785 port = self.port 

786 else: 

787 port = get_port() 

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

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