Coverage for agentlib_flexquant/utils/interactive.py: 14%
212 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-09-19 15:08 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2025-09-19 15:08 +0000
1import webbrowser
2import pandas as pd
3from typing import get_args, Union, Optional
4from pydantic import FilePath
5from dash import Dash, html, dcc, callback, Output, Input, ctx
6from plotly import graph_objects as go
7from agentlib.core.agent import AgentConfig
8from agentlib_mpc.utils import TimeConversionTypes, TIME_CONVERSION
9from agentlib_mpc.utils.analysis import mpc_at_time_step
10from agentlib_mpc.utils.plotting.interactive import get_port
11import agentlib_flexquant.data_structures.globals as glbs
12import agentlib_flexquant.data_structures.flex_results as flex_results
13from agentlib_flexquant.data_structures.flexquant import FlexQuantConfig
14from agentlib_flexquant.data_structures.flex_kpis import FlexibilityKPIs
15from agentlib_flexquant.data_structures.flex_offer import OfferStatus
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
148 # Plotting functions
149 def plot_mpc_stats(fig: go.Figure, variable: str) -> go.Figure:
150 """ plot the statics of the baseline and shadow mpcs.
152 Args:
153 fig: the figure to be updated
154 variable: the statics variable to be plotted
156 Returns:
157 The updated figure
159 """
160 fig.add_trace(
161 go.Scatter(
162 name=self.baseline_agent_config.id,
163 x=self.df_baseline_stats.index,
164 y=self.df_baseline_stats[variable],
165 mode="markers",
166 line=self.LINE_PROPERTIES[self.baseline_agent_config.id],
167 )
168 )
169 fig.add_trace(
170 go.Scatter(
171 name=self.pos_flex_agent_config.id,
172 x=self.df_pos_flex_stats.index,
173 y=self.df_pos_flex_stats[variable],
174 mode="markers",
175 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id],
176 )
177 )
178 fig.add_trace(
179 go.Scatter(
180 name=self.neg_flex_agent_config.id,
181 x=self.df_neg_flex_stats.index,
182 y=self.df_neg_flex_stats[variable],
183 mode="markers",
184 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id],
185 )
186 )
187 return fig
189 def plot_one_mpc_variable(fig: go.Figure, variable: str, time_step: float) -> go.Figure:
190 """Plot the mpc series for the specified variable at the specified time step.
192 Args:
193 fig: the figure to be updated
194 variable: the variable to be plotted
195 time_step: the time step to be plotted
197 Returns:
198 The updated figure
200 """
201 # Get the mpc data for the plot
202 series_neg = mpc_at_time_step(
203 data=self.df_neg_flex,
204 time_step=time_step,
205 variable=self.intersection_mpcs_sim[variable][
206 self.neg_flex_module_config.module_id
207 ],
208 index_offset=False,
209 )
210 series_pos = mpc_at_time_step(
211 data=self.df_pos_flex,
212 time_step=time_step,
213 variable=self.intersection_mpcs_sim[variable][
214 self.pos_flex_module_config.module_id
215 ],
216 index_offset=False,
217 )
218 series_bas = mpc_at_time_step(
219 data=self.df_baseline,
220 time_step=time_step,
221 variable=self.intersection_mpcs_sim[variable][
222 self.baseline_module_config.module_id
223 ],
224 index_offset=False,
225 )
227 def _add_step_to_data(s: pd.Series) -> pd.Series:
228 """ shift the index of the series """
229 s_concat = s.copy().shift(periods=1)
230 s_concat.index = s.index - 0.01 * (s.index[1] - s.index[0])
231 for ind, val in s_concat.items():
232 s[ind] = val
233 s.sort_index(inplace=True)
234 return s
236 # Manage nans
237 for series in [series_neg, series_pos, series_bas]:
238 if variable in [
239 control.name for control in self.baseline_module_config.controls
240 ]:
241 series.dropna(inplace=True)
242 series = _add_step_to_data(s=series)
243 series.dropna(inplace=True)
245 # Plot the data
246 try:
247 df_sim = self.df_simulation[
248 self.intersection_mpcs_sim[variable][
249 self.simulator_module_config.module_id
250 ]
251 ]
252 fig.add_trace(
253 go.Scatter(
254 name=self.simulator_agent_config.id,
255 x=df_sim.index,
256 y=df_sim,
257 mode="lines",
258 line=self.LINE_PROPERTIES[self.simulator_agent_config.id],
259 zorder=2,
260 )
261 )
262 except KeyError:
263 # E.g. when the simulator variable name was not found from the intersection
264 pass
266 fig.add_trace(
267 go.Scatter(
268 name=self.baseline_agent_config.id,
269 x=series_bas.index,
270 y=series_bas,
271 mode="lines",
272 line=self.LINE_PROPERTIES[self.baseline_agent_config.id]
273 | {"dash": "dash"},
274 zorder=3,
275 )
276 )
277 fig.add_trace(
278 go.Scatter(
279 name=self.neg_flex_agent_config.id,
280 x=series_neg.index,
281 y=series_neg,
282 mode="lines",
283 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id]
284 | {"dash": "dash"},
285 zorder=4,
286 )
287 )
288 fig.add_trace(
289 go.Scatter(
290 name=self.pos_flex_agent_config.id,
291 x=series_pos.index,
292 y=series_pos,
293 mode="lines",
294 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id]
295 | {"dash": "dash"},
296 zorder=4,
297 )
298 )
300 # Get the data for the bounds
301 def _get_mpc_series(var_type: str, var_name: str) -> pd.Series:
302 return self.df_baseline[(var_type, var_name)].xs(0, level=1)
304 def _get_bound(var_name: str) -> Optional[pd.Series]:
305 if var_name in self.df_baseline.columns.get_level_values(1):
306 try:
307 bound = _get_mpc_series(var_type="variable", var_name=var_name)
308 except KeyError:
309 bound = _get_mpc_series(var_type="parameter", var_name=var_name)
310 else:
311 bound = None
312 return bound
314 df_lb = None
315 df_ub = None
316 for custom_bound in self.custom_bounds:
317 if variable == custom_bound.for_variable:
318 df_lb = _get_bound(custom_bound.lower_bound)
319 df_ub = _get_bound(custom_bound.upper_bound)
320 if variable in [
321 control.name for control in self.baseline_module_config.controls
322 ]:
323 df_lb = _get_mpc_series(var_type="lower", var_name=variable)
324 df_ub = _get_mpc_series(var_type="upper", var_name=variable)
326 # Plot bounds
327 if df_lb is not None:
328 fig.add_trace(
329 go.Scatter(
330 name="Lower bound",
331 x=df_lb.index,
332 y=df_lb,
333 mode="lines",
334 line=self.LINE_PROPERTIES[self.bounds_key],
335 zorder=1,
336 )
337 )
338 if df_ub is not None:
339 fig.add_trace(
340 go.Scatter(
341 name="Upper bound",
342 x=df_ub.index,
343 y=df_ub,
344 mode="lines",
345 line=self.LINE_PROPERTIES[self.bounds_key],
346 zorder=1,
347 )
348 )
350 return fig
352 def plot_flexibility_kpi(fig: go.Figure, variable: str) -> go.Figure:
353 """Plot the flexibility kpi.
355 Args:
356 fig: the figure to be updated
357 variable: the kpi variable to be plotted
359 Returns:
360 The updated figure
362 """
363 df_ind = self.df_indicator.xs(0, level=1)
364 # if the variable only has NaN, don't plot
365 if df_ind[self.kpi_names_pos[variable]].isna().all():
366 return
367 fig.add_trace(
368 go.Scatter(
369 name=self.label_positive,
370 x=df_ind.index,
371 y=df_ind[self.kpi_names_pos[variable]],
372 mode="lines+markers",
373 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id],
374 )
375 )
376 fig.add_trace(
377 go.Scatter(
378 name=self.label_negative,
379 x=df_ind.index,
380 y=df_ind[self.kpi_names_neg[variable]],
381 mode="lines+markers",
382 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id],
383 )
384 )
385 return fig
387 def plot_market_results(fig: go.Figure, variable: str) -> go.Figure:
388 """Plot the market results.
390 Args:
391 fig: the figure to be updated
392 variable: the variable to be plotted
394 Returns:
395 The updated figure
397 """
398 df_flex_market_index = self.df_market.index.droplevel("time")
399 if variable in self.df_market.columns:
400 fig.add_trace(
401 go.Scatter(
402 x=df_flex_market_index,
403 y=self.df_market[variable],
404 mode="lines+markers",
405 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id],
406 )
407 )
408 else:
409 pos_var = f"pos_{variable}"
410 neg_var = f"neg_{variable}"
411 fig.add_trace(
412 go.Scatter(
413 name=self.label_positive,
414 x=df_flex_market_index,
415 y=self.df_market[pos_var],
416 mode="lines+markers",
417 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id],
418 )
419 )
420 fig.add_trace(
421 go.Scatter(
422 name=self.label_negative,
423 x=df_flex_market_index,
424 y=self.df_market[neg_var],
425 mode="lines+markers",
426 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id],
427 )
428 )
429 return fig
431 # Marking times
432 def get_characteristic_times(at_time_step: float) -> (float, float, float):
433 """Get the characteristic times.
435 Args:
436 at_time_step: the time at which we want to get the characteristic times
438 Returns:
439 market_time, prep_time and flex_event_duration
441 """
442 df_characteristic_times = self.df_indicator.xs(0, level="time")
443 rel_market_time = (
444 df_characteristic_times.loc[at_time_step, glbs.MARKET_TIME]
445 / TIME_CONVERSION[self.current_timescale_of_data]
446 )
447 rel_prep_time = (
448 df_characteristic_times.loc[at_time_step, glbs.PREP_TIME]
449 / TIME_CONVERSION[self.current_timescale_of_data]
450 )
451 flex_event_duration = (
452 df_characteristic_times.loc[at_time_step, glbs.FLEX_EVENT_DURATION]
453 / TIME_CONVERSION[self.current_timescale_of_data]
454 )
455 return rel_market_time, rel_prep_time, flex_event_duration
457 def mark_time(fig: go.Figure, at_time_step: float, line_prop: dict) -> go.Figure:
458 fig.add_vline(x=at_time_step, line=line_prop, layer="below")
459 return fig
461 def mark_characteristic_times(fig: go.Figure, offer_time: Union[float, int] = 0, line_prop: dict = None) -> go.Figure:
462 """Add markers of the characteristic times to the plot for a time step.
464 Args:
465 fig: the figure to plot the results into
466 offer_time: When to show the markers
467 line_prop: the graphic properties of the lines as in plotly
469 Returns:
470 The updated figure
472 """
473 if line_prop is None:
474 line_prop = self.LINE_PROPERTIES[self.characteristic_times_current_key]
475 try:
476 rel_market_time, rel_prep_time, flex_event_duration = (
477 get_characteristic_times(offer_time)
478 )
479 mark_time(fig=fig, at_time_step=offer_time, line_prop=line_prop)
480 mark_time(
481 fig=fig,
482 at_time_step=offer_time + rel_market_time,
483 line_prop=line_prop,
484 )
485 mark_time(
486 fig=fig,
487 at_time_step=offer_time + rel_prep_time + rel_market_time,
488 line_prop=line_prop,
489 )
490 mark_time(
491 fig=fig,
492 at_time_step=offer_time
493 + rel_prep_time
494 + rel_market_time
495 + flex_event_duration,
496 line_prop=line_prop,
497 )
498 except KeyError:
499 pass # No data of characteristic times available, e.g. if offer accepted
500 return fig
502 def mark_characteristic_times_of_accepted_offers(fig: go.Figure) -> go.Figure:
503 """Add markers of the characteristic times for accepted offers to the plot."""
504 if self.df_market is not None:
505 if (self.df_market["status"].isin([
506 OfferStatus.accepted_negative.value,
507 OfferStatus.accepted_positive.value,
508 ]
509 ).any()):
510 df_accepted_offers = self.df_market["status"].str.contains(
511 pat="OfferStatus.accepted"
512 )
513 for i in df_accepted_offers.index.to_list():
514 if df_accepted_offers[i]:
515 fig = mark_characteristic_times(
516 fig=fig,
517 offer_time=i[0],
518 line_prop=self.LINE_PROPERTIES[
519 self.characteristic_times_accepted_key
520 ],
521 )
522 return fig
524 # Master plotting function
525 def create_plot(
526 variable: str,
527 at_time_step: float,
528 show_accepted_characteristic_times: bool = True,
529 show_current_characteristic_times: bool = True,
530 zoom_to_offer_window: bool = False,
531 zoom_to_prediction_interval: bool = False,
532 ) -> go.Figure:
533 """Create a plot for one variable
535 Args:
536 variable: the variable to plot
537 at_time_step: the time_step to show the mpc predictions and the characteristic times
538 show_accepted_characteristic_times: whether to show the accepted characteristic times
539 show_current_characteristic_times: whether to show the current characteristic times
540 zoom_to_offer_window: whether to zoom to offer window
541 zoom_to_prediction_interval: wether to zoom to prediction interval
543 Returns:
544 The created figure
546 """
547 # Create the figure
548 fig = go.Figure()
549 mark_time(fig=fig, at_time_step=at_time_step, line_prop={"color": "green"})
550 if show_accepted_characteristic_times:
551 mark_characteristic_times_of_accepted_offers(fig=fig)
553 # Plot variable
554 if variable in self.df_baseline_stats.columns:
555 plot_mpc_stats(fig=fig, variable=variable)
556 elif variable in self.intersection_mpcs_sim.keys():
557 plot_one_mpc_variable(
558 fig=fig, variable=variable, time_step=at_time_step
559 )
560 if show_current_characteristic_times:
561 mark_characteristic_times(fig=fig, offer_time=at_time_step)
562 elif any(variable in label for label in self.df_indicator.columns):
563 plot_flexibility_kpi(fig=fig, variable=variable)
564 elif any(variable in label for label in self.df_market.columns):
565 plot_market_results(fig=fig, variable=variable)
566 else:
567 raise ValueError(f"No plotting function found for variable {variable}")
569 # Set layout
570 if zoom_to_offer_window:
571 rel_market_time, rel_prep_time, flex_event_duration = (
572 get_characteristic_times(at_time_step)
573 )
574 ts = (
575 self.baseline_module_config.time_step
576 / TIME_CONVERSION[self.current_timescale_of_data]
577 )
579 xlim_left = at_time_step
580 xlim_right = (
581 at_time_step
582 + rel_market_time
583 + rel_prep_time
584 + flex_event_duration
585 + 4 * ts
586 )
587 elif zoom_to_prediction_interval:
588 xlim_left = at_time_step
589 xlim_right = at_time_step + self.df_baseline.index[-1][-1]
590 else:
591 xlim_left = self.df_simulation.index[0]
592 xlim_right = (
593 self.df_simulation.index[-1] + self.df_baseline.index[-1][-1]
594 )
596 fig.update_layout(
597 yaxis_title=variable,
598 xaxis_title=f"Time in {self.current_timescale_of_data}",
599 xaxis_range=[xlim_left, xlim_right],
600 height=350,
601 margin=dict(t=20, b=20),
602 )
603 fig.update_xaxes(
604 dtick=round(self.baseline_module_config.prediction_horizon / 6)
605 * self.baseline_module_config.time_step
606 / TIME_CONVERSION[self.current_timescale_of_data]
607 )
608 fig.update_yaxes(tickformat="~r")
609 return fig
611 # Create the app
612 app = Dash(__name__ + "_flexibility", title="Flexibility Results")
613 app.layout = [
614 html.H1("Results"),
615 html.H3("Settings"),
616 html.Div(
617 children=[
618 html.Code(f"{option}: {setting}, ")
619 for option, setting in self.baseline_module_config.optimization_backend[
620 "discretization_options"
621 ].items()
622 ]
623 ),
624 # Options
625 html.Div(
626 children=[
627 html.H3("Options"),
628 html.Div(
629 children=[
630 dcc.Checklist(
631 id="accepted_characteristic_times",
632 options=[
633 {
634 "label": "Show characteristic times (accepted)",
635 "value": True,
636 }
637 ],
638 value=[True],
639 style={
640 "display": "inline-block",
641 "padding-right": "10px",
642 },
643 ),
644 dcc.Checklist(
645 id="current_characteristic_times",
646 options=[
647 {
648 "label": "Show characteristic times (current)",
649 "value": True,
650 }
651 ],
652 value=[True],
653 style={
654 "display": "inline-block",
655 "padding-right": "10px",
656 },
657 ),
658 ],
659 ),
660 html.Div(
661 children=[
662 dcc.Checklist(
663 id="zoom_to_offer_window",
664 options=[
665 {
666 "label": "Zoom to flexibility offer window",
667 "value": False,
668 }
669 ],
670 style={
671 "display": "inline-block",
672 "padding-right": "10px",
673 },
674 ),
675 dcc.Checklist(
676 id="zoom_to_prediction_interval",
677 options=[
678 {
679 "label": "Zoom to mpc prediction interval",
680 "value": False,
681 }
682 ],
683 style={"display": "inline-block"},
684 ),
685 ],
686 ),
687 # Time input
688 html.Div(
689 children=[
690 html.H3(
691 children=f"Time:",
692 style={
693 "display": "inline-block",
694 "padding-right": "10px",
695 },
696 ),
697 dcc.Input(
698 id="time_typing",
699 type="number",
700 min=0,
701 max=1,
702 value=0, # will be updated in the callback
703 style={"display": "inline-block"},
704 ),
705 dcc.Dropdown(
706 id="time_unit",
707 options=get_args(TimeConversionTypes),
708 value=self.current_timescale_input,
709 style={
710 "display": "inline-block",
711 "verticalAlign": "middle",
712 "padding-left": "10px",
713 "width": "100px",
714 },
715 ),
716 ],
717 ),
718 dcc.Slider(
719 id="time_slider",
720 min=0,
721 max=1,
722 value=0, # will be updated in the callback
723 tooltip={"placement": "bottom", "always_visible": True},
724 marks=None,
725 updatemode="drag",
726 ),
727 ],
728 style={
729 "width": "88%",
730 "padding-left": "0%",
731 "padding-right": "12%",
732 # Make the options sticky to the top of the page
733 "position": "sticky",
734 "top": "0",
735 "overflow-y": "visible",
736 "z-index": "100",
737 "background-color": "white",
738 },
739 ),
740 # Container for the graphs, will be updated in the callback
741 html.Div(id="graphs_container_variables", children=[]),
742 ]
744 # Callbacks
745 # Update the time value or the time unit
746 @callback(
747 Output(component_id="time_slider", component_property="value"),
748 Output(component_id="time_slider", component_property="min"),
749 Output(component_id="time_slider", component_property="max"),
750 Output(component_id="time_slider", component_property="step"),
751 Output(component_id="time_typing", component_property="value"),
752 Output(component_id="time_typing", component_property="min"),
753 Output(component_id="time_typing", component_property="max"),
754 Output(component_id="time_typing", component_property="step"),
755 Input(component_id="time_typing", component_property="value"),
756 Input(component_id="time_slider", component_property="value"),
757 Input(component_id="time_unit", component_property="value"),
758 )
759 def update_time_index_of_input(
760 time_typing: float, time_slider: float, time_unit: TimeConversionTypes
761 ) -> (int, float, float, float, int, float, float, float):
762 # get trigger id
763 trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
765 # Get the value for the sliders
766 if trigger_id == "time_slider":
767 value = time_slider
768 elif trigger_id == "time_unit":
769 value = (
770 time_typing
771 * TIME_CONVERSION[self.current_timescale_input]
772 / TIME_CONVERSION[time_unit]
773 )
774 else:
775 value = time_typing
777 # Convert the index to the given time unit if necessary
778 if trigger_id == "time_unit":
779 self.convert_timescale_of_dataframe_index(to_timescale=time_unit)
781 # Get the index for the slider types
782 times = self.df_baseline.index.get_level_values(0).unique()
783 minimum = times[0]
784 maximum = times[-1]
785 step = times[1] - times[0]
787 self.current_timescale_input = time_unit
789 return (value, minimum, maximum, step, value, minimum, maximum, step)
791 # Update the graphs
792 @callback(
793 Output(
794 component_id="graphs_container_variables", component_property="children"
795 ),
796 Input(component_id="time_typing", component_property="value"),
797 Input(
798 component_id="accepted_characteristic_times", component_property="value"
799 ),
800 Input(
801 component_id="current_characteristic_times", component_property="value"
802 ),
803 Input(component_id="zoom_to_offer_window", component_property="value"),
804 Input(
805 component_id="zoom_to_prediction_interval", component_property="value"
806 ),
807 )
808 def update_graph(
809 at_time_step: float,
810 show_accepted_characteristic_times: bool,
811 show_current_characteristic_times: bool,
812 zoom_to_offer_window: bool,
813 zoom_to_prediction_interval: bool,
814 ) -> list[dcc.Graph]:
815 """Update all graphs based on the options and slider values"""
816 figs = []
817 for variable in self.plotting_variables:
818 fig = create_plot(
819 variable=variable,
820 at_time_step=at_time_step,
821 show_accepted_characteristic_times=show_accepted_characteristic_times,
822 show_current_characteristic_times=show_current_characteristic_times,
823 zoom_to_offer_window=zoom_to_offer_window,
824 zoom_to_prediction_interval=zoom_to_prediction_interval,
825 )
826 figs.append(dcc.Graph(id=f"graph_{variable}", figure=fig))
827 return figs
829 # Run the app
830 if self.port:
831 port = self.port
832 else:
833 port = get_port()
834 webbrowser.open_new_tab(f"http://localhost:{port}")
835 app.run(debug=False, port=port)