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
« 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
18class CustomBound:
19 """Dataclass to let the user define custom bounds for the mpc variables.
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
25 """
27 for_variable: str
28 lower_bound: Optional[str]
29 upper_bound: Optional[str]
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
42class Dashboard(flex_results.Results):
43 """Class for the dashboard of flexquant"""
44 # Constants for plotting variables
45 MPC_ITERATIONS: str = "iter_count"
47 # Label for the positive and negative flexibilities
48 label_positive: str = "positive"
49 label_negative: str = "negative"
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"
56 # Custom settings
57 custom_bounds: list[CustomBound] = []
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")
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 }
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()
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)
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
136 Args:
137 custom_bounds: optional arguments to show the comfort bounds
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
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.
151 Args:
152 fig: the figure to be updated
153 variable: the statics variable to be plotted
155 Returns:
156 The updated figure
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
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.
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
196 Returns:
197 The updated figure
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 )
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
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)
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
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 )
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)
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
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)
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 )
349 return fig
351 def plot_flexibility_kpi(fig: go.Figure, variable: str) -> go.Figure:
352 """Plot the flexibility kpi.
354 Args:
355 fig: the figure to be updated
356 variable: the kpi variable to be plotted
358 Returns:
359 The updated figure
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
386 def plot_market_results(fig: go.Figure, variable: str) -> go.Figure:
387 """Plot the market results.
389 Args:
390 fig: the figure to be updated
391 variable: the variable to be plotted
393 Returns:
394 The updated figure
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
430 # Marking times
431 def get_characteristic_times(at_time_step: float) -> (float, float, float):
432 """Get the characteristic times.
434 Args:
435 at_time_step: the time at which we want to get the characteristic times
437 Returns:
438 market_time, prep_time and flex_event_duration
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
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
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.
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
468 Returns:
469 The updated figure
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
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
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
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
542 Returns:
543 The created figure
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)
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}")
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 )
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 )
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
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 ]
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]
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
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)
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]
786 self.current_timescale_input = time_unit
788 return (value, minimum, maximum, step, value, minimum, maximum, step)
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
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)