def init_fig(self, fig, reward, done, timestamp): if fig is None: fig, ax = plt.subplots(1, 1, figsize=self.figsize) elif isinstance(fig, tuple): if len(fig) != 2: raise PlotError( "PlotMatplotlib \"fig\" argument should be, if a tuple, a tuple containing a figure " "and an axe, for example the results of `plt.subplots(1, 1)`. You provided " "a tuple of length {}".format(len(fig))) fig, ax = fig if not isinstance(fig, self.accepted_figure_class): raise PlotError( "PlotMatplotlib \"fig\" argument should be an object of type \"{}\" and not \"{}\"." "".format(self.accepted_figure_class, type(fig))) if not isinstance(ax, self.accepted_figure_class_tuple[1]): raise PlotError( "PlotMatplotlib \"fig\" argument should be an object of type \"{}\" and not \"{}\"." "".format(self.accepted_figure_class, type(ax))) elif isinstance(fig, self.accepted_figure_class): ax = fig.gca() else: raise PlotError( "PlotMatplotlib \"fig\" argument should be an object of type \"{}\" and not \"{}\"." "".format(self.accepted_figure_class, type(fig))) return (fig, ax)
def init_fig(self, fig, reward, done, timestamp): if fig is None: fig = go.Figure() elif not isinstance(fig, self.type_fig_allowed): raise PlotError( "PlotPlotly cannot plot on figure of type {}. The accepted type is {}. You provided an " "invalid argument for \"fig\"".format(type(fig), self.type_fig_allowed)) return fig
def compute_grid_layout(self, observation_space, grid_layout=None): """ Compute the grid layout from the observation space This should return a native python ``dict`` in the same format as observation_space.grid_layout : .. code-block:: python { "substation1_name": [x_coord, y_coord], "substation2_name": [x_coord, y_coord], [...], "load1_name": [x_coord, y_coord], [...], "gen1_name": [x_coord, y_coord], [...] } Note that is must contain at least the positions for the substations. The loads and generators will be skipped if missing. By default, if `grid_layout` is provided this is returned, otherwise returns observation_space.grid_layout Parameters ---------- observation_space: ``grid2op.Observation.ObservationSpace`` The observation space of the environment grid_layout: ``dict`` or ``None`` A dictionary containing the coordinates for each substation. """ # We need an intial layout to work with use_grid_layout = None if grid_layout is not None: use_grid_layout = grid_layout elif observation_space.grid_layout is not None: use_grid_layout = observation_space.grid_layout else: raise PlotError("No grid layout provided for plotting") # Compute loads and gens positions using a default implementation observation_space.grid_layout = use_grid_layout return layout_obs_sub_load_and_gen(observation_space, scale=self.scale, use_initial=True)
def __init__(self, observation_space, display_mod="plotly", substation_layout=None, radius_sub=20., load_prod_dist=70., bus_radius=6.): if display_mod not in self.allwed_display_mod: raise PlotError( "Only avaible plot mod are \"{}\". You specified \"{}\" which is not supported." "".format(self.allwed_display_mod, display_mod)) cls_ = self.allwed_display_mod[display_mod] self.displ_backend = cls_(observation_space, substation_layout=substation_layout, radius_sub=radius_sub, load_prod_dist=load_prod_dist, bus_radius=bus_radius) self.display_mod = display_mod
def __init__(self, observation_space, substation_layout=None, radius_sub=25., load_prod_dist=70., bus_radius=4.): """ Parameters ---------- substation_layout: ``list`` List of tupe given the position of each of the substation of the powergrid. observation_space: :class:`grid2op.Observation.ObservationSpace` BaseObservation space """ BasePlot.__init__(self, substation_layout=substation_layout, observation_space=observation_space, radius_sub=radius_sub, load_prod_dist=load_prod_dist, bus_radius=bus_radius) if not can_plot: raise PlotError( "Impossible to plot as plotly cannot be imported. Please install \"plotly\" and " "\"seaborn\" with \"pip install --update plotly seaborn\"") # define a color palette, whatever... sns.set() pal = sns.light_palette("darkred", 8) self.cols = pal.as_hex()[1:] self.col_line = "royalblue" self.col_sub = "red" self.col_load = "black" self.col_gen = "darkgreen" self.default_color = "black" self.type_fig_allowed = go.Figure
def __init__(self, observation_space, substation_layout=None, radius_sub=20., load_prod_dist=70., bus_radius=6.): if substation_layout is None: if observation_space.grid_layout is None: # if no layout is provided, and observation_space has no layout, then it fails raise PlotError( "Impossible to use plotting abilities without specifying a layout (coordinates) " "of the substations.") # if no layout is provided, use the one in the observation_space substation_layout = [] for el in observation_space.name_sub: substation_layout.append(observation_space.grid_layout[el]) if len(substation_layout) != observation_space.n_sub: raise PlotError( "You provided a layout with {} elements while there are {} substations on the powergrid. " "Your layout is invalid".format(len(substation_layout), observation_space.n_sub)) GridObjects.__init__(self) self.init_grid(observation_space) self.observation_space = observation_space self._layout = {} self._layout["substations"] = self._get_sub_layout(substation_layout) self.radius_sub = radius_sub self.load_prod_dist = load_prod_dist # distance between load and generator to the center of the substation self.bus_radius = bus_radius self.subs_elements = [None for _ in self.observation_space.sub_info] # get the element in each substation for sub_id in range(self.observation_space.sub_info.shape[0]): this_sub = {} objs = self.observation_space.get_obj_connect_to( substation_id=sub_id) for c_id in objs["loads_id"]: c_nm = self._get_load_name(sub_id, c_id) this_load = {} this_load["type"] = "load" this_load["sub_pos"] = self.observation_space.load_to_sub_pos[ c_id] this_sub[c_nm] = this_load for g_id in objs["generators_id"]: g_nm = self._get_gen_name(sub_id, g_id) this_gen = {} this_gen["type"] = "gen" this_gen["sub_pos"] = self.observation_space.gen_to_sub_pos[ g_id] this_sub[g_nm] = this_gen for lor_id in objs["lines_or_id"]: ext_id = self.observation_space.line_ex_to_subid[lor_id] l_nm = self._get_line_name(sub_id, ext_id, lor_id) this_line = {} this_line["type"] = "line" this_line[ "sub_pos"] = self.observation_space.line_or_to_sub_pos[ lor_id] this_sub[l_nm] = this_line for lex_id in objs["lines_ex_id"]: or_id = self.observation_space.line_or_to_subid[lex_id] l_nm = self._get_line_name(or_id, sub_id, lex_id) this_line = {} this_line["type"] = "line" this_line[ "sub_pos"] = self.observation_space.line_ex_to_sub_pos[ lex_id] this_sub[l_nm] = this_line self.subs_elements[sub_id] = this_sub self._compute_layout()
def plot_info(self, figure=None, redraw=True, line_values=None, line_unit="", load_values=None, load_unit="", gen_values=None, gen_unit="", observation=None): """ Plot an observation with custom values Parameters ---------- figure: ``object`` The figure on which to plot the observation. If figure is ``None`` a new figure is created. line_values: ``list`` information to be displayed for the powerlines [must have the same size as observation.n_line and convertible to float] line_unit: ``str`` Unit string for the :line_values: argument, displayed after the line value load_info: ``list`` information to display for the loads [must have the same size as observation.n_load and convertible to float] load_unit: ``str`` Unit string for the :load_values: argument, displayed after the load value gen_info: ``list`` information to display in the generators [must have the same size as observation.n_gen and convertible to float] gen_unit: ``str`` Unit string for the :gen_values: argument, displayed after the generator value observation: :class:`grid2op.Observation.BaseObservation` An observation to plot, can be None if no values are drawn from the observation Returns ------- res: ``object`` The figure updated with the data from the new observation. """ # Check values are in the correct format if line_values is not None and len( line_values) != self.observation_space.n_line: raise PlotError( "Impossible to display these values on the powerlines: there are {} values" "provided for {} powerlines in the grid".format( len(line_values), self.observation_space.n_line)) if load_values is not None and len( load_values) != self.observation_space.n_load: raise PlotError( "Impossible to display these values on the loads: there are {} values" "provided for {} loads in the grid".format( len(load_values), self.observation_space.n_load)) if gen_values is not None and len( gen_values) != self.observation_space.n_gen: raise PlotError( "Impossible to display these values on the generators: there are {} values" "provided for {} generators in the grid".format( len(gen_info), self.observation_space.n_gen)) # Get a valid figure to draw into if figure is None: fig = self.create_figure() redraw = True elif redraw: self.clear_figure(figure) fig = figure else: fig = figure # Get a valid Observation if observation is None: # See dummy data added in the constructor observation = self.observation_space # Trigger draw calls self._plot_lines(fig, observation, line_values, line_unit, redraw) self._plot_loads(fig, observation, load_values, load_unit, redraw) self._plot_gens(fig, observation, gen_values, gen_unit, redraw) self._plot_subs(fig, observation, redraw) self._plot_legend(fig, observation, redraw) # Some implementations may need postprocessing self.plot_postprocess(fig, observation, not redraw) # Return updated figure return fig
def plot_obs(self, observation, figure=None, redraw=True, line_info="rho", load_info="p", gen_info="p"): """ Plot an observation. Parameters ---------- observation: :class:`grid2op.Observation.BaseObservation` The observation to plot figure: ``object`` The figure on which to plot the observation. If figure is ``None``, a new figure is created. line_info: ``str`` One of "rho", "a", or "p" or "v" The information that will be plotted on the powerline. By default "rho". All flow are taken "origin" side. load_info: ``str`` One of "p" or "v" the information displayed on the load. (default to "p"). gen_info: ``str`` One of "p" or "v" the information displayed on the generators (default to "p"). Returns ------- res: ``object`` The figure updated with the data from the new observation. """ # Start by checking arguments are valid if not isinstance(observation, BaseObservation): err_msg = "Observation is not a derived type of " \ "grid2op.Observation.BaseObservation" raise PlotError(err_msg) if line_info not in self._lines_info: err_msg = "Impossible to plot line info \"{}\" for line." \ " Possible values are {}" raise PlotError(err_msg.format(line_info, str(self._lines_info))) if load_info not in self._loads_info: err_msg = "Impossible to plot load info \"{}\" for line." \ " Possible values are {}" raise PlotError(err_msg.format(load_info, str(self._loads_info))) if gen_info not in self._gens_info: err_msg = "Impossible to plot gen info \"{}\" for line." \ " Possible values are {}" raise PlotError(err_msg.format(gen_info, str(self._gens_info))) line_values = None line_unit = "" if line_info is not None: line_unit = self._info_to_units[line_info] if line_info == "rho": line_values = observation.rho if line_info == "p": line_values = observation.p_or if line_info == "a": line_values = observation.a_or if line_info == "v": line_values = observation.v_or load_values = None load_unit = "" if load_info is not None: load_unit = self._info_to_units[load_info] if load_info == "p": load_values = copy.copy(observation.load_p) * -1.0 if load_info == "v": load_values = observation.load_v gen_values = None gen_unit = "" if gen_info is not None: gen_unit = self._info_to_units[gen_info] if gen_info == "p": gen_values = observation.prod_p if gen_info == "v": gen_values = observation.prod_v return self.plot_info(observation=observation, figure=figure, redraw=redraw, line_values=line_values, line_unit=line_unit, load_values=load_values, load_unit=load_unit, gen_values=gen_values, gen_unit=gen_unit)
def plot_info(self, line_info=None, load_info=None, gen_info=None, sub_info=None, colormap=None): """ Plot some information on the powergrid. For now, only numeric data are supported. Parameters ---------- line_info: ``list`` information to be displayed in the powerlines, in place of their name and id (for example their thermal limit) [must have the same size as the number of powerlines] load_info: ``list`` information to display in the generators, in place of their name and id [must have the same size as the number of loads] gen_info: ``list`` information to display in the generators, in place of their name and id (for example their pmax) [must have the same size as the number of generators] sub_info: ``list`` information to display in the substation, in place of their name and id (for example the number of different topologies possible at this substation) [must have the same size as the number of substations] colormap: ``str`` If not None, one of "line", "load", "gen" or "sub". If None, default colors will be used for each elements (default color is the coloring of If not None, all elements will be black, and the selected element will be highlighted. """ fig, ax = plt.subplots(1, 1, figsize=(15, 15)) if colormap is None: legend_help = [ Line2D([0], [0], color=self.col_line, lw=4), Line2D([0], [0], color=self.col_sub, lw=4), Line2D([0], [0], color=self.col_load, lw=4), Line2D([0], [0], color=self.col_gen, lw=4) ] # draw powerline texts_line = None if line_info is not None: texts_line = [ "{:.2f}".format(el) if el is not None else None for el in line_info ] if len(texts_line) != self.n_line: raise PlotError( "Impossible to display these information on the powerlines: there are {} elements" "provided while {} powerlines on this grid".format( len(texts_line), self.n_line)) self._draw_powerlines(ax, texts_line, colormap=colormap) # draw substation texts_sub = None if sub_info is not None: texts_sub = [ "{:.2f}".format(el) if el is not None else None for el in sub_info ] if len(texts_sub) != self.n_sub: raise PlotError( "Impossible to display these information on the substations: there are {} elements" "provided while {} substations on this grid".format( len(texts_sub), self.n_sub)) self._draw_subs(ax, texts_sub, colormap=colormap) # draw loads texts_load = None if load_info is not None: texts_load = [ "{:.2f}".format(el) if el is not None else None for el in load_info ] if len(texts_load) != self.n_load: raise PlotError( "Impossible to display these information on the loads: there are {} elements" "provided while {} loads on this grid".format( len(texts_load), self.n_load)) self._draw_loads(ax, texts_load, colormap=colormap) # draw gens texts_gen = None if gen_info is not None: texts_gen = [ "{:.2f}".format(el) if el is not None else None for el in gen_info ] if len(texts_gen) != self.n_gen: raise PlotError( "Impossible to display these information on the generators: there are {} elements" "provided while {} generators on this grid".format( len(texts_gen), self.n_gen)) self._draw_gens(ax, texts_gen, colormap=colormap) if colormap is None: ax.legend(legend_help, ["powerline", "substation", "load", "generator"]) return fig
def plot_obs(self, observation, fig=None, reward=None, done=None, timestamp=None, line_info="rho", load_info="p", gen_info="p", colormap="line"): """ .. warning:: /!\\\\ This module is deprecated /!\\\\ Prefer using the module `grid2op.PlotGrid Plot the given observation in the given figure. For now it represents information about load and generator active values. It also display dashed powerlines when they are disconnected and the color of each powerlines depends on its relative flow (its flow in amperes divided by its maximum capacity). If a substation counts only 1 bus, nothing specific is display. If it counts more, then buses are materialized by colored dot and lines will connect every object to its appropriate bus (with the proper color). Names of substation and objects are NOT displayed on this figure to lower the amount of information. Parameters ---------- observation: :class:`grid2op.Observation.Observation` The observation to plot fig: :class:`plotly.graph_objects.Figure` The figure on which to plot the observation. Possibly ``None``, in this case a new figure is made. line_info: ``str`` One of "rho", "a", or "p" or "v" the information that will be plotted on the powerline By default "rho". All flow are taken "origin" side. load_info: ``str`` One of "p" or "v" the information displayed on the load (défault to "p"). gen_info: ``str`` One of "p" or "v" the information displayed on the generators (default to "p"). Returns ------- res: :class:`plotly.graph_objects.Figure` The figure updated with the data from the new observation. """ fig = self.init_fig(fig, reward, done, timestamp) # draw substation subs = self._draw_subs(fig=fig, vals=[None for el in range(self.n_sub)]) # draw powerlines if line_info == "rho": line_vals = [observation.rho] line_units = "%" elif line_info == "a": line_vals = [observation.a_or] line_units = "A" elif line_info == "p": line_vals = [observation.p_or] line_units = "MW" elif line_info == "v": line_vals = [observation.v_or] line_units = "kV" else: raise PlotError( "Impossible to plot value \"{}\" for line. Possible values are \"rho\", \"p\", \"v\" and \"a\"." ) line_vals.append(observation.line_status) line_vals.append(observation.p_or) lines = self._draw_powerlines(fig, vals=line_vals, unit=line_units, colormap=colormap) # draw the loads if load_info == "p": loads_vals = -observation.load_p load_units = "MW" elif load_info == "v": loads_vals = observation.load_v load_units = "kV" else: raise PlotError( "Impossible to plot value \"{}\" for load. Possible values are \"p\" and \"v\"." ) loads = self._draw_loads(fig, vals=loads_vals, unit=load_units, colormap=colormap) # draw the generators if gen_info == "p": gen_vals = observation.prod_p gen_units = "MW" elif gen_info == "v": gen_vals = observation.prod_v gen_units = "kV" else: raise PlotError( "Impossible to plot value \"{}\" for generators. Possible values are \"p\" and \"v\"." ) gens = self._draw_gens(fig, vals=gen_vals, unit=gen_units, colormap=colormap) # draw the topologies topos = self._draw_topos(fig=fig, observation=observation) self._post_process_obs(fig, reward, done, timestamp, subs, lines, loads, gens, topos) return fig
def plot_info(self, fig=None, line_info=None, load_info=None, gen_info=None, sub_info=None, colormap=None, unit=None): """ .. warning:: /!\\\\ This module is deprecated /!\\\\ Prefer using the module `grid2op.PlotGrid Plot some information on the powergrid. For now, only numeric data are supported. Parameters ---------- line_info: ``list`` information to be displayed in the powerlines, in place of their name and id (for example their thermal limit) [must have the same size as the number of powerlines and convertible to float] load_info: ``list`` information to display in the generators, in place of their name and id [must have the same size as the number of loads and convertible to float] gen_info: ``list`` information to display in the generators, in place of their name and id (for example their pmax) [must have the same size as the number of generators and convertible to float] sub_info: ``list`` information to display in the substation, in place of their name and id (for example the number of different topologies possible at this substation) [must have the same size as the number of substations, and convertible to float] colormap: ``str`` If not None, one of "line", "load", "gen" or "sub". If None, default colors will be used for each elements (default color is the coloring of If not None, all elements will be black, and the selected element will be highlighted. fig: ``matplotlib figure`` The figure on which to draw. It is created by the method if ``None``. unit: ``str``, optional The unit in which the data are provided. For example, if you provide in `line_info` some data in mega-watt (MW) you can add `unit="MW"` to have the unit display on the screen. """ fig = self.init_fig(fig, reward=None, done=None, timestamp=None) # draw powerline unit_line = None if line_info is not None: unit_line = unit if len(line_info) != self.n_line: raise PlotError( "Impossible to display these information on the powerlines: there are {} elements" "provided while {} powerlines on this grid".format( len(line_info), self.n_line)) line_info = np.array(line_info).astype(np.float) line_info = [line_info, line_info, line_info] lines = self._draw_powerlines(fig, vals=line_info, colormap=colormap, unit=unit_line) # draw substation unit_sub = None if sub_info is not None: unit_sub = unit if len(sub_info) != self.n_sub: raise PlotError( "Impossible to display these information on the substations: there are {} elements" "provided while {} substations on this grid".format( len(sub_info), self.n_sub)) sub_info = np.array(sub_info).astype(np.float) subs = self._draw_subs(fig, vals=sub_info, colormap=colormap, unit=unit_sub) # draw loads unit_load = None if load_info is not None: unit_load = unit if len(load_info) != self.n_load: raise PlotError( "Impossible to display these information on the loads: there are {} elements" "provided while {} loads on this grid".format( len(load_info), self.n_load)) load_info = np.array(load_info).astype(np.float) loads = self._draw_loads(fig, vals=load_info, colormap=colormap, unit=unit_load) # draw gens unit_gen = None if gen_info is not None: unit_gen = unit if len(gen_info) != self.n_gen: raise PlotError( "Impossible to display these information on the generators: there are {} elements" "provided while {} generators on this grid".format( len(gen_info), self.n_gen)) gen_info = np.array(gen_info).astype(np.float) gens = self._draw_gens(fig, vals=gen_info, colormap=colormap, unit=unit_gen) self._post_process_obs(fig, reward=None, done=None, timestamp=None, subs=subs, lines=lines, loads=loads, gens=gens, topos=[]) return fig
def plot_info(self, figure=None, redraw=True, line_values=None, line_unit="", load_values=None, load_unit="", storage_values=None, storage_unit="", gen_values=None, gen_unit="", observation=None, coloring=None): """ Plot an observation with custom values Parameters ---------- figure: ``object`` The figure on which to plot the observation. If figure is ``None`` a new figure is created. line_values: ``list`` information to be displayed for the powerlines [must have the same size as observation.n_line and convertible to float] line_unit: ``str`` Unit string for the :line_values: argument, displayed after the line value load_values: ``list`` information to display for the loads [must have the same size as observation.n_load and convertible to float] load_unit: ``str`` Unit string for the :load_values: argument, displayed after the load value storage_values: ``list`` information to display for the storage units [must have the same size as observation.n_storage and convertible to float] storage_unit: ``str`` Unit string for the :storage_values: argument, displayed after the storage value gen_values: ``list`` information to display in the generators [must have the same size as observation.n_gen and convertible to float] gen_unit: ``str`` Unit string for the :gen_values: argument, displayed after the generator value observation: :class:`grid2op.Observation.BaseObservation` An observation to plot, can be None if no values are drawn from the observation coloring: ``None`` for no special coloring, or "line" to color the powerline based on the value ("gen" and "load" coming soon) Examples -------- More examples on how to use this function is given in the "8_PlottingCapabilities.ipynb" notebook. The basic concept is: .. code-block:: python import grid2op from grid2op.PlotGrid import PlotMatplot env = grid2op.make() plot_helper = PlotMatplot(env.observation_space) # plot the layout (position of each elements) of the powergrid plot_helper.plot_layout() # project some data on the grid line_values = env.get_thermal_limit() plot_helper.plot_info(line_values=line_values) # to plot an observation obs = env.reset() plot_helper.plot_obs(obs) Returns ------- res: ``object`` The figure updated with the data from the new observation. """ # Check values are in the correct format if line_values is not None and len( line_values) != self.observation_space.n_line: raise PlotError( "Impossible to display these values on the powerlines: there are {} values" "provided for {} powerlines in the grid".format( len(line_values), self.observation_space.n_line)) if load_values is not None and len( load_values) != self.observation_space.n_load: raise PlotError( "Impossible to display these values on the loads: there are {} values" "provided for {} loads in the grid".format( len(load_values), self.observation_space.n_load)) if gen_values is not None and len( gen_values) != self.observation_space.n_gen: raise PlotError( "Impossible to display these values on the generators: there are {} values" "provided for {} generators in the grid".format( len(gen_values), self.observation_space.n_gen)) if storage_values is not None and len( storage_values) != self.observation_space.n_storage: raise PlotError( "Impossible to display these values on the storage units: there are {} values" "provided for {} generators in the grid".format( len(storage_values), self.observation_space.n_storage)) # Get a valid figure to draw into if figure is None: fig = self.create_figure() redraw = True elif redraw: self.clear_figure(figure) fig = figure else: fig = figure # Get a valid Observation if observation is None: # See dummy data added in the constructor observation = self.observation_space if coloring is not None: observation = copy.deepcopy(observation) if coloring == "line": if line_values is None: raise PlotError( "Impossible to color the grid based on the line information (key word argument " "\"line_values\") if this argument is None.") observation.rho = copy.deepcopy(line_values) try: observation.rho = np.array( observation.rho).astype(dt_float) except: raise PlotError( "Impossible to convert the input values (line_values) to floating point" ) # rescaling to have range 0 - 1.0 tmp = observation.rho[np.isfinite(observation.rho)] observation.rho -= ( np.min(tmp) - 1e-1 ) # so the min is 1e-1 otherwise 0.0 is plotted as black tmp = observation.rho[np.isfinite(observation.rho)] observation.rho /= np.max(tmp) elif coloring == "load": # TODO warnings.warn( "ooloring = loads is not available at the moment") elif coloring == "gen": if gen_values is None: raise PlotError( "Impossible to color the grid based on the gen information (key word argument " "\"gen_values\") if this argument is None.") observation.prod_p = copy.deepcopy(gen_values) try: observation.prod_p = np.array( observation.prod_p).astype(dt_float) except: raise PlotError( "Impossible to convert the input values (gen_values) to floating point" ) # rescaling to have range 0 - 1.0 tmp = observation.prod_p[np.isfinite(observation.prod_p)] if np.any(np.isfinite(observation.prod_p)): observation.prod_p -= ( np.min(tmp) - 1e-1 ) # so the min is 1e-1 otherwise 0.0 is plotted as black tmp = observation.prod_p[np.isfinite( observation.prod_p)] observation.prod_p /= np.max(tmp) else: raise PlotError( "coloring must be one of \"line\", \"load\" or \"gen\"" ) # Trigger draw calls self._plot_lines(fig, observation, line_values, line_unit, redraw) self._plot_loads(fig, observation, load_values, load_unit, redraw) self._plot_storages(fig, observation, storage_values, storage_unit, redraw) self._plot_gens(fig, observation, gen_values, gen_unit, redraw) self._plot_subs(fig, observation, redraw) self._plot_legend(fig, observation, redraw) # Some implementations may need postprocessing self.plot_postprocess(fig, observation, not redraw) # Return updated figure return fig