Coverage for agentlib_flexquant/utils/interactive.py: 14%
212 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-10-20 14:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2025-10-20 14:09 +0000
1import webbrowser
2from typing import Optional, Union, get_args
4import pandas as pd
5from agentlib.core.agent import AgentConfig
6from agentlib_mpc.utils import TIME_CONVERSION, TimeConversionTypes
7from agentlib_mpc.utils.analysis import mpc_at_time_step
8from agentlib_mpc.utils.plotting.interactive import get_port
9from dash import Dash, Input, Output, callback, ctx, dcc, html
10from plotly import graph_objects as go
11from pydantic import FilePath
13import agentlib_flexquant.data_structures.flex_results as flex_results
14import agentlib_flexquant.data_structures.globals as glbs
15from agentlib_flexquant.data_structures.flex_kpis import FlexibilityKPIs
16from agentlib_flexquant.data_structures.flex_offer import OfferStatus
17from agentlib_flexquant.data_structures.flexquant import FlexQuantConfig
20class CustomBound:
21 """Dataclass to let the user define custom bounds for the mpc variables.
23 for_variable -- The name of the variable to plot the bounds into
24 lower bound -- The lower bound of the variable as the name of the lower bound variable
25 in the MPC
26 upper bound -- The upper bound of the variable as the name of the upper bound variable
27 in the MPC
29 """
31 for_variable: str
32 lower_bound: Optional[str]
33 upper_bound: Optional[str]
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
46class Dashboard(flex_results.Results):
47 """Class for the dashboard of flexquant"""
49 # Constants for plotting variables
50 MPC_ITERATIONS: str = "iter_count"
52 # Label for the positive and negative flexibilities
53 label_positive: str = "positive"
54 label_negative: str = "negative"
56 # Keys for line properties
57 bounds_key: str = "bounds"
58 characteristic_times_current_key: str = "characteristic_times_current"
59 characteristic_times_accepted_key: str = "characteristic_times_accepted"
61 # Custom settings
62 custom_bounds: list[CustomBound] = []
64 def __init__(
65 self,
66 flex_config: Optional[Union[str, FilePath, FlexQuantConfig]] = None,
67 simulator_agent_config: Optional[Union[str, FilePath, AgentConfig]] = None,
68 generated_flex_files_base_path: Optional[Union[str, FilePath]] = None,
69 results: Union[str, FilePath, dict[str, dict[str, pd.DataFrame]]] = None,
70 to_timescale: TimeConversionTypes = "hours",
71 port: int = None,
72 ):
73 super().__init__(
74 flex_config=flex_config,
75 simulator_agent_config=simulator_agent_config,
76 generated_flex_files_base_path=generated_flex_files_base_path,
77 results=results,
78 to_timescale=to_timescale,
79 )
80 self.port = port
81 self.current_timescale_input = self.current_timescale_of_data
82 # remove FMU from dataclass to enable pickling/multiprocessing of Dashboard
83 if hasattr(self.simulator_module_config, "model"):
84 self.simulator_module_config.model_config["frozen"] = False
85 delattr(self.simulator_module_config, "model")
87 # Define line properties
88 self.LINE_PROPERTIES: dict = {
89 self.simulator_agent_config.id: {"color": "black",},
90 self.baseline_agent_config.id: {"color": "black",},
91 self.neg_flex_agent_config.id: {"color": "red",},
92 self.pos_flex_agent_config.id: {"color": "blue",},
93 self.bounds_key: {"color": "grey",},
94 self.characteristic_times_current_key: {"color": "grey", "dash": "dash",},
95 self.characteristic_times_accepted_key: {"color": "yellow",},
96 }
98 # KPIS
99 kpis_pos = FlexibilityKPIs(direction="positive")
100 self.kpi_names_pos = kpis_pos.get_name_dict()
101 kpis_neg = FlexibilityKPIs(direction="negative")
102 self.kpi_names_neg = kpis_neg.get_name_dict()
104 # Get variables for plotting
105 # MPC stats
106 self.plotting_variables = [self.MPC_ITERATIONS]
107 # MPC and sim variables
108 self.intersection_mpcs_sim = self.get_intersection_mpcs_sim()
109 self.plotting_variables.extend(
110 [key for key in self.intersection_mpcs_sim.keys()]
111 )
112 # Flexibility kpis
113 self.plotting_variables.append(kpis_pos.energy_flex.name)
114 self.plotting_variables.append(kpis_pos.costs.name)
115 # for kpi in kpis_pos.get_kpi_dict(direction_name=False).values():
116 # if not isinstance(kpi.value, pd.Series):
117 # self.plotting_variables.append(kpi.name)
119 def show(self, custom_bounds: Union[CustomBound, list[CustomBound]] = None):
120 """Show the dashboard in a web browser containing:
121 -- Statistics of the MPCs solver
122 -- The states, controls, and the power variable of the MPCs and the simulator
123 -- KPIs of the flexibility quantification
124 -- Markings of the characteristic flexibility times
126 Args:
127 custom_bounds: optional arguments to show the comfort bounds
129 """
130 if custom_bounds is None:
131 self.custom_bounds = []
132 elif isinstance(custom_bounds, CustomBound):
133 self.custom_bounds = [custom_bounds]
134 else:
135 self.custom_bounds = custom_bounds
137 # Plotting functions
138 def plot_mpc_stats(fig: go.Figure, variable: str) -> go.Figure:
139 """ plot the statics of the baseline and shadow mpcs.
141 Args:
142 fig: the figure to be updated
143 variable: the statics variable to be plotted
145 Returns:
146 The updated figure
148 """
149 fig.add_trace(
150 go.Scatter(
151 name=self.baseline_agent_config.id,
152 x=self.df_baseline_stats.index,
153 y=self.df_baseline_stats[variable],
154 mode="markers",
155 line=self.LINE_PROPERTIES[self.baseline_agent_config.id],
156 )
157 )
158 fig.add_trace(
159 go.Scatter(
160 name=self.pos_flex_agent_config.id,
161 x=self.df_pos_flex_stats.index,
162 y=self.df_pos_flex_stats[variable],
163 mode="markers",
164 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id],
165 )
166 )
167 fig.add_trace(
168 go.Scatter(
169 name=self.neg_flex_agent_config.id,
170 x=self.df_neg_flex_stats.index,
171 y=self.df_neg_flex_stats[variable],
172 mode="markers",
173 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id],
174 )
175 )
176 return fig
178 def plot_one_mpc_variable(
179 fig: go.Figure, variable: str, time_step: float
180 ) -> go.Figure:
181 """Plot the mpc series for the specified variable at the specified time step.
183 Args:
184 fig: the figure to be updated
185 variable: the variable to be plotted
186 time_step: the time step to be plotted
188 Returns:
189 The updated figure
191 """
192 # Get the mpc data for the plot
193 series_neg = mpc_at_time_step(
194 data=self.df_neg_flex,
195 time_step=time_step,
196 variable=self.intersection_mpcs_sim[variable][
197 self.neg_flex_module_config.module_id
198 ],
199 index_offset=False,
200 )
201 series_pos = mpc_at_time_step(
202 data=self.df_pos_flex,
203 time_step=time_step,
204 variable=self.intersection_mpcs_sim[variable][
205 self.pos_flex_module_config.module_id
206 ],
207 index_offset=False,
208 )
209 series_bas = mpc_at_time_step(
210 data=self.df_baseline,
211 time_step=time_step,
212 variable=self.intersection_mpcs_sim[variable][
213 self.baseline_module_config.module_id
214 ],
215 index_offset=False,
216 )
218 def _add_step_to_data(s: pd.Series) -> pd.Series:
219 """ shift the index of the series """
220 s_concat = s.copy().shift(periods=1)
221 s_concat.index = s.index - 0.01 * (s.index[1] - s.index[0])
222 for ind, val in s_concat.items():
223 s[ind] = val
224 s.sort_index(inplace=True)
225 return s
227 # Manage nans
228 for series in [series_neg, series_pos, series_bas]:
229 if variable in [
230 control.name for control in self.baseline_module_config.controls
231 ]:
232 series.dropna(inplace=True)
233 series = _add_step_to_data(s=series)
234 series.dropna(inplace=True)
236 # Plot the data
237 try:
238 df_sim = self.df_simulation[
239 self.intersection_mpcs_sim[variable][
240 self.simulator_module_config.module_id
241 ]
242 ]
243 fig.add_trace(
244 go.Scatter(
245 name=self.simulator_agent_config.id,
246 x=df_sim.index,
247 y=df_sim,
248 mode="lines",
249 line=self.LINE_PROPERTIES[self.simulator_agent_config.id],
250 zorder=2,
251 )
252 )
253 except KeyError:
254 # E.g. when the simulator variable name was not found from the intersection
255 pass
257 fig.add_trace(
258 go.Scatter(
259 name=self.baseline_agent_config.id,
260 x=series_bas.index,
261 y=series_bas,
262 mode="lines",
263 line=self.LINE_PROPERTIES[self.baseline_agent_config.id]
264 | {"dash": "dash"},
265 zorder=3,
266 )
267 )
268 fig.add_trace(
269 go.Scatter(
270 name=self.neg_flex_agent_config.id,
271 x=series_neg.index,
272 y=series_neg,
273 mode="lines",
274 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id]
275 | {"dash": "dash"},
276 zorder=4,
277 )
278 )
279 fig.add_trace(
280 go.Scatter(
281 name=self.pos_flex_agent_config.id,
282 x=series_pos.index,
283 y=series_pos,
284 mode="lines",
285 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id]
286 | {"dash": "dash"},
287 zorder=4,
288 )
289 )
291 # Get the data for the bounds
292 def _get_mpc_series(var_type: str, var_name: str) -> pd.Series:
293 return self.df_baseline[(var_type, var_name)].xs(0, level=1)
295 def _get_bound(var_name: str) -> Optional[pd.Series]:
296 if var_name in self.df_baseline.columns.get_level_values(1):
297 try:
298 bound = _get_mpc_series(var_type="variable", var_name=var_name)
299 except KeyError:
300 bound = _get_mpc_series(var_type="parameter", var_name=var_name)
301 else:
302 bound = None
303 return bound
305 df_lb = None
306 df_ub = None
307 for custom_bound in self.custom_bounds:
308 if variable == custom_bound.for_variable:
309 df_lb = _get_bound(custom_bound.lower_bound)
310 df_ub = _get_bound(custom_bound.upper_bound)
311 if variable in [
312 control.name for control in self.baseline_module_config.controls
313 ]:
314 df_lb = _get_mpc_series(var_type="lower", var_name=variable)
315 df_ub = _get_mpc_series(var_type="upper", var_name=variable)
317 # Plot bounds
318 if df_lb is not None:
319 fig.add_trace(
320 go.Scatter(
321 name="Lower bound",
322 x=df_lb.index,
323 y=df_lb,
324 mode="lines",
325 line=self.LINE_PROPERTIES[self.bounds_key],
326 zorder=1,
327 )
328 )
329 if df_ub is not None:
330 fig.add_trace(
331 go.Scatter(
332 name="Upper bound",
333 x=df_ub.index,
334 y=df_ub,
335 mode="lines",
336 line=self.LINE_PROPERTIES[self.bounds_key],
337 zorder=1,
338 )
339 )
341 return fig
343 def plot_flexibility_kpi(fig: go.Figure, variable: str) -> go.Figure:
344 """Plot the flexibility kpi.
346 Args:
347 fig: the figure to be updated
348 variable: the kpi variable to be plotted
350 Returns:
351 The updated figure
353 """
354 df_ind = self.df_indicator.xs(0, level=1)
355 # if the variable only has NaN, don't plot
356 if df_ind[self.kpi_names_pos[variable]].isna().all():
357 return
358 fig.add_trace(
359 go.Scatter(
360 name=self.label_positive,
361 x=df_ind.index,
362 y=df_ind[self.kpi_names_pos[variable]],
363 mode="lines+markers",
364 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id],
365 )
366 )
367 fig.add_trace(
368 go.Scatter(
369 name=self.label_negative,
370 x=df_ind.index,
371 y=df_ind[self.kpi_names_neg[variable]],
372 mode="lines+markers",
373 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id],
374 )
375 )
376 return fig
378 def plot_market_results(fig: go.Figure, variable: str) -> go.Figure:
379 """Plot the market results.
381 Args:
382 fig: the figure to be updated
383 variable: the variable to be plotted
385 Returns:
386 The updated figure
388 """
389 df_flex_market_index = self.df_market.index.droplevel("time")
390 if variable in self.df_market.columns:
391 fig.add_trace(
392 go.Scatter(
393 x=df_flex_market_index,
394 y=self.df_market[variable],
395 mode="lines+markers",
396 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id],
397 )
398 )
399 else:
400 pos_var = f"pos_{variable}"
401 neg_var = f"neg_{variable}"
402 fig.add_trace(
403 go.Scatter(
404 name=self.label_positive,
405 x=df_flex_market_index,
406 y=self.df_market[pos_var],
407 mode="lines+markers",
408 line=self.LINE_PROPERTIES[self.pos_flex_agent_config.id],
409 )
410 )
411 fig.add_trace(
412 go.Scatter(
413 name=self.label_negative,
414 x=df_flex_market_index,
415 y=self.df_market[neg_var],
416 mode="lines+markers",
417 line=self.LINE_PROPERTIES[self.neg_flex_agent_config.id],
418 )
419 )
420 return fig
422 # Marking times
423 def get_characteristic_times(at_time_step: float) -> (float, float, float):
424 """Get the characteristic times.
426 Args:
427 at_time_step: the time at which we want to get the characteristic times
429 Returns:
430 market_time, prep_time and flex_event_duration
432 """
433 df_characteristic_times = self.df_indicator.xs(0, level="time")
434 rel_market_time = (
435 df_characteristic_times.loc[at_time_step, glbs.MARKET_TIME]
436 / TIME_CONVERSION[self.current_timescale_of_data]
437 )
438 rel_prep_time = (
439 df_characteristic_times.loc[at_time_step, glbs.PREP_TIME]
440 / TIME_CONVERSION[self.current_timescale_of_data]
441 )
442 flex_event_duration = (
443 df_characteristic_times.loc[at_time_step, glbs.FLEX_EVENT_DURATION]
444 / TIME_CONVERSION[self.current_timescale_of_data]
445 )
446 return rel_market_time, rel_prep_time, flex_event_duration
448 def mark_time(
449 fig: go.Figure, at_time_step: float, line_prop: dict
450 ) -> go.Figure:
451 fig.add_vline(x=at_time_step, line=line_prop, layer="below")
452 return fig
454 def mark_characteristic_times(
455 fig: go.Figure, offer_time: Union[float, int] = 0, line_prop: dict = None
456 ) -> go.Figure:
457 """Add markers of the characteristic times to the plot for a time step.
459 Args:
460 fig: the figure to plot the results into
461 offer_time: When to show the markers
462 line_prop: the graphic properties of the lines as in plotly
464 Returns:
465 The updated figure
467 """
468 if line_prop is None:
469 line_prop = self.LINE_PROPERTIES[self.characteristic_times_current_key]
470 try:
471 (
472 rel_market_time,
473 rel_prep_time,
474 flex_event_duration,
475 ) = get_characteristic_times(offer_time)
476 mark_time(fig=fig, at_time_step=offer_time, line_prop=line_prop)
477 mark_time(
478 fig=fig,
479 at_time_step=offer_time + rel_market_time,
480 line_prop=line_prop,
481 )
482 mark_time(
483 fig=fig,
484 at_time_step=offer_time + rel_prep_time + rel_market_time,
485 line_prop=line_prop,
486 )
487 mark_time(
488 fig=fig,
489 at_time_step=offer_time
490 + rel_prep_time
491 + rel_market_time
492 + flex_event_duration,
493 line_prop=line_prop,
494 )
495 except KeyError:
496 pass # No data of characteristic times available, e.g. if offer accepted
497 return fig
499 def mark_characteristic_times_of_accepted_offers(fig: go.Figure) -> go.Figure:
500 """Add markers of the characteristic times for accepted offers to the plot."""
501 if self.df_market is not None:
502 if (
503 self.df_market["status"]
504 .isin(
505 [
506 OfferStatus.ACCEPTED_NEGATIVE.value,
507 OfferStatus.ACCEPTED_POSITIVE.value,
508 ]
509 )
510 .any()
511 ):
512 df_accepted_offers = self.df_market["status"].str.contains(
513 pat="OfferStatus.accepted"
514 )
515 for i in df_accepted_offers.index.to_list():
516 if df_accepted_offers[i]:
517 fig = mark_characteristic_times(
518 fig=fig,
519 offer_time=i[0],
520 line_prop=self.LINE_PROPERTIES[
521 self.characteristic_times_accepted_key
522 ],
523 )
524 return fig
526 # Master plotting function
527 def create_plot(
528 variable: str,
529 at_time_step: float,
530 show_accepted_characteristic_times: bool = True,
531 show_current_characteristic_times: bool = True,
532 zoom_to_offer_window: bool = False,
533 zoom_to_prediction_interval: bool = False,
534 ) -> go.Figure:
535 """Create a plot for one variable
537 Args:
538 variable: the variable to plot
539 at_time_step: the time_step to show the mpc predictions
540 and the characteristic times
541 show_accepted_characteristic_times: whether to show
542 the accepted characteristic times
543 show_current_characteristic_times: whether to show
544 the current characteristic times
545 zoom_to_offer_window: whether to zoom to offer window
546 zoom_to_prediction_interval: wether to zoom to prediction interval
548 Returns:
549 The created figure
551 """
552 # Create the figure
553 fig = go.Figure()
554 mark_time(fig=fig, at_time_step=at_time_step, line_prop={"color": "green"})
555 if show_accepted_characteristic_times:
556 mark_characteristic_times_of_accepted_offers(fig=fig)
558 # Plot variable
559 if variable in self.df_baseline_stats.columns:
560 plot_mpc_stats(fig=fig, variable=variable)
561 elif variable in self.intersection_mpcs_sim.keys():
562 plot_one_mpc_variable(
563 fig=fig, variable=variable, time_step=at_time_step
564 )
565 if show_current_characteristic_times:
566 mark_characteristic_times(fig=fig, offer_time=at_time_step)
567 elif any(variable in label for label in self.df_indicator.columns):
568 plot_flexibility_kpi(fig=fig, variable=variable)
569 elif any(variable in label for label in self.df_market.columns):
570 plot_market_results(fig=fig, variable=variable)
571 else:
572 raise ValueError(f"No plotting function found for variable {variable}")
574 # Set layout
575 if zoom_to_offer_window:
576 (
577 rel_market_time,
578 rel_prep_time,
579 flex_event_duration,
580 ) = get_characteristic_times(at_time_step)
581 ts = (
582 self.baseline_module_config.time_step
583 / TIME_CONVERSION[self.current_timescale_of_data]
584 )
586 xlim_left = at_time_step
587 xlim_right = (
588 at_time_step
589 + rel_market_time
590 + rel_prep_time
591 + flex_event_duration
592 + 4 * ts
593 )
594 elif zoom_to_prediction_interval:
595 xlim_left = at_time_step
596 xlim_right = at_time_step + self.df_baseline.index[-1][-1]
597 else:
598 xlim_left = self.df_simulation.index[0]
599 xlim_right = (
600 self.df_simulation.index[-1] + self.df_baseline.index[-1][-1]
601 )
603 fig.update_layout(
604 yaxis_title=variable,
605 xaxis_title=f"Time in {self.current_timescale_of_data}",
606 xaxis_range=[xlim_left, xlim_right],
607 height=350,
608 margin=dict(t=20, b=20),
609 )
610 fig.update_xaxes(
611 dtick=round(self.baseline_module_config.prediction_horizon / 6)
612 * self.baseline_module_config.time_step
613 / TIME_CONVERSION[self.current_timescale_of_data]
614 )
615 fig.update_yaxes(tickformat="~r")
616 return fig
618 # Create the app
619 app = Dash(__name__ + "_flexibility", title="Flexibility Results")
620 app.layout = [
621 html.H1("Results"),
622 html.H3("Settings"),
623 html.Div(
624 children=[
625 html.Code(f"{option}: {setting}, ")
626 for option, setting in self.baseline_module_config.optimization_backend[
627 "discretization_options"
628 ].items()
629 ]
630 ),
631 # Options
632 html.Div(
633 children=[
634 html.H3("Options"),
635 html.Div(
636 children=[
637 dcc.Checklist(
638 id="accepted_characteristic_times",
639 options=[
640 {
641 "label": "Show characteristic times (accepted)",
642 "value": True,
643 }
644 ],
645 value=[True],
646 style={
647 "display": "inline-block",
648 "padding-right": "10px",
649 },
650 ),
651 dcc.Checklist(
652 id="current_characteristic_times",
653 options=[
654 {
655 "label": "Show characteristic times (current)",
656 "value": True,
657 }
658 ],
659 value=[True],
660 style={
661 "display": "inline-block",
662 "padding-right": "10px",
663 },
664 ),
665 ],
666 ),
667 html.Div(
668 children=[
669 dcc.Checklist(
670 id="zoom_to_offer_window",
671 options=[
672 {
673 "label": "Zoom to flexibility offer window",
674 "value": False,
675 }
676 ],
677 style={
678 "display": "inline-block",
679 "padding-right": "10px",
680 },
681 ),
682 dcc.Checklist(
683 id="zoom_to_prediction_interval",
684 options=[
685 {
686 "label": "Zoom to mpc prediction interval",
687 "value": False,
688 }
689 ],
690 style={"display": "inline-block"},
691 ),
692 ],
693 ),
694 # Time input
695 html.Div(
696 children=[
697 html.H3(
698 children=f"Time:",
699 style={
700 "display": "inline-block",
701 "padding-right": "10px",
702 },
703 ),
704 dcc.Input(
705 id="time_typing",
706 type="number",
707 min=0,
708 max=1,
709 value=0, # will be updated in the callback
710 style={"display": "inline-block"},
711 ),
712 dcc.Dropdown(
713 id="time_unit",
714 options=get_args(TimeConversionTypes),
715 value=self.current_timescale_input,
716 style={
717 "display": "inline-block",
718 "verticalAlign": "middle",
719 "padding-left": "10px",
720 "width": "100px",
721 },
722 ),
723 ],
724 ),
725 dcc.Slider(
726 id="time_slider",
727 min=0,
728 max=1,
729 value=0, # will be updated in the callback
730 tooltip={"placement": "bottom", "always_visible": True},
731 marks=None,
732 updatemode="drag",
733 ),
734 ],
735 style={
736 "width": "88%",
737 "padding-left": "0%",
738 "padding-right": "12%",
739 # Make the options sticky to the top of the page
740 "position": "sticky",
741 "top": "0",
742 "overflow-y": "visible",
743 "z-index": "100",
744 "background-color": "white",
745 },
746 ),
747 # Container for the graphs, will be updated in the callback
748 html.Div(id="graphs_container_variables", children=[]),
749 ]
751 # Callbacks
752 # Update the time value or the time unit
753 @callback(
754 Output(component_id="time_slider", component_property="value"),
755 Output(component_id="time_slider", component_property="min"),
756 Output(component_id="time_slider", component_property="max"),
757 Output(component_id="time_slider", component_property="step"),
758 Output(component_id="time_typing", component_property="value"),
759 Output(component_id="time_typing", component_property="min"),
760 Output(component_id="time_typing", component_property="max"),
761 Output(component_id="time_typing", component_property="step"),
762 Input(component_id="time_typing", component_property="value"),
763 Input(component_id="time_slider", component_property="value"),
764 Input(component_id="time_unit", component_property="value"),
765 )
766 def update_time_index_of_input(
767 time_typing: float, time_slider: float, time_unit: TimeConversionTypes
768 ) -> (int, float, float, float, int, float, float, float):
769 # get trigger id
770 trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
772 # Get the value for the sliders
773 if trigger_id == "time_slider":
774 value = time_slider
775 elif trigger_id == "time_unit":
776 value = (
777 time_typing
778 * TIME_CONVERSION[self.current_timescale_input]
779 / TIME_CONVERSION[time_unit]
780 )
781 else:
782 value = time_typing
784 # Convert the index to the given time unit if necessary
785 if trigger_id == "time_unit":
786 self.convert_timescale_of_dataframe_index(to_timescale=time_unit)
788 # Get the index for the slider types
789 times = self.df_baseline.index.get_level_values(0).unique()
790 minimum = times[0]
791 maximum = times[-1]
792 step = times[1] - times[0]
794 self.current_timescale_input = time_unit
796 return (value, minimum, maximum, step, value, minimum, maximum, step)
798 # Update the graphs
799 @callback(
800 Output(
801 component_id="graphs_container_variables", component_property="children"
802 ),
803 Input(component_id="time_typing", component_property="value"),
804 Input(
805 component_id="accepted_characteristic_times", component_property="value"
806 ),
807 Input(
808 component_id="current_characteristic_times", component_property="value"
809 ),
810 Input(component_id="zoom_to_offer_window", component_property="value"),
811 Input(
812 component_id="zoom_to_prediction_interval", component_property="value"
813 ),
814 )
815 def update_graph(
816 at_time_step: float,
817 show_accepted_characteristic_times: bool,
818 show_current_characteristic_times: bool,
819 zoom_to_offer_window: bool,
820 zoom_to_prediction_interval: bool,
821 ) -> list[dcc.Graph]:
822 """Update all graphs based on the options and slider values"""
823 figs = []
824 for variable in self.plotting_variables:
825 fig = create_plot(
826 variable=variable,
827 at_time_step=at_time_step,
828 show_accepted_characteristic_times=show_accepted_characteristic_times,
829 show_current_characteristic_times=show_current_characteristic_times,
830 zoom_to_offer_window=zoom_to_offer_window,
831 zoom_to_prediction_interval=zoom_to_prediction_interval,
832 )
833 figs.append(dcc.Graph(id=f"graph_{variable}", figure=fig))
834 return figs
836 # Run the app
837 if self.port:
838 port = self.port
839 else:
840 port = get_port()
841 webbrowser.open_new_tab(f"http://localhost:{port}")
842 app.run(debug=False, port=port)