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
« 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
5import webbrowser
6from dash import Dash, html, dcc, callback, Output, Input, ctx
7from plotly import graph_objects as go
9from agentlib.core.agent import AgentConfig
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
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
22class CustomBound:
23 """
24 Dataclass to let the user define custom bounds for the mpc variables
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 """
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 """
48 Class for the dashboard of flexquant
49 """
51 # Constants for plotting variables
52 MPC_ITERATIONS: str = "iter_count"
54 # Label for the positive and negative flexibilities
55 label_positive: str = "positive"
56 label_negative: str = "negative"
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"
63 # Custom settings
64 custom_bounds: list[CustomBound] = []
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")
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 }
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()
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)
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
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
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
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 )
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
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)
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 )
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)
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
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)
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 )
336 return fig
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
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
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
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
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
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
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
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
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)
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}")
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 )
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 )
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
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 ]
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]
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
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)
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]
741 self.current_timescale_input = time_unit
743 return (value, minimum, maximum, step, value, minimum, maximum, step)
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
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)